Merge remote-tracking branch 'source/master'

This commit is contained in:
Felix Van der Jeugt 2016-01-18 21:45:47 +01:00
commit 7ad871fab1
80 changed files with 2112 additions and 362 deletions

View File

@ -14,4 +14,4 @@ install:
- C:\Python27\python -u scripts\dev\ci_install.py - C:\Python27\python -u scripts\dev\ci_install.py
test_script: test_script:
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml - C:\Python34\Scripts\tox -e %TESTENV% -- -v --junitxml=junit.xml

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ __pycache__
/setuptools-*.egg /setuptools-*.egg
/setuptools-*.zip /setuptools-*.zip
/qutebrowser/git-commit-id /qutebrowser/git-commit-id
/qutebrowser/3rdparty
/doc/*.html /doc/*.html
/README.html /README.html
/CHANGELOG.html /CHANGELOG.html

View File

@ -37,7 +37,7 @@ install:
- python scripts/dev/ci_install.py - python scripts/dev/ci_install.py
script: script:
- tox -e $TESTENV -- -p no:sugar -v --cov-report term tests - tox -e $TESTENV -- -v --cov-report term tests
after_success: after_success:
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov' - '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'

View File

@ -14,6 +14,31 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `Security` to invite users to upgrade in case of vulnerabilities.
v0.6.0 (unreleased)
-------------------
Added
~~~~~
- New `--quiet` argument for the `:debug-pyeval` command to not open a tab with
the results. Note `:debug-pyeval` is still only intended for debugging.
Changed
~~~~~~~
- Pasting multiple lines via `:paste` now opens each line in a new tab.
v0.5.1
------
Fixed
~~~~~
- Fixed completion for various config values when using `:set`.
- Fixed config validation for various config values.
- Prevented an error being logged when a website with HTTP authentication was
opened on Windows.
v0.5.0 v0.5.0
------ ------

View File

@ -152,9 +152,29 @@ $ nix-env -i qutebrowser
On Windows On Windows
---------- ----------
You can either use one of the There are different ways to install qutebrowser on Windows:
https://github.com/The-Compiler/qutebrowser/releases[prebuilt standalone
packages or MSI installers], or install manually: Prebuilt binaries
~~~~~~~~~~~~~~~~~
Prebuilt standalone packages and MSI installers
https://github.com/The-Compiler/qutebrowser/releases[are built] for every
release.
https://chocolatey.org/packages/qutebrowser[Chocolatey package]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* PackageManagement PowerShell module
----
PS C:\> Install-Package qutebrowser
----
* Chocolatey's client
----
C:\> choco install qutebrowser
----
Manual install
~~~~~~~~~~~~~~
* Use the installer from http://www.python.org/downloads[python.org] to get * Use the installer from http://www.python.org/downloads[python.org] to get
Python 3 (be sure to install pip). Python 3 (be sure to install pip).

View File

@ -160,6 +160,7 @@ Contributors, sorted by the number of commits in descending order:
* ZDarian * ZDarian
* John ShaggyTwoDope Jenkins * John ShaggyTwoDope Jenkins
* Peter Vilim * Peter Vilim
* Tarcisio Fedrizzi
* Jonas Schürmann * Jonas Schürmann
* Panagiotis Ktistakis * Panagiotis Ktistakis
* Jimmy * Jimmy
@ -176,6 +177,7 @@ Contributors, sorted by the number of commits in descending order:
* jnphilipp * jnphilipp
* Tobias Patzl * Tobias Patzl
* Peter Michely * Peter Michely
* Link
* Larry Hynes * Larry Hynes
* Johannes Altmanninger * Johannes Altmanninger
* Samir Benmendil * Samir Benmendil
@ -186,6 +188,7 @@ Contributors, sorted by the number of commits in descending order:
* Corentin Jule * Corentin Jule
* zwarag * zwarag
* xd1le * xd1le
* evan
* dylan araps * dylan araps
* Tim Harder * Tim Harder
* Thiago Barroso Perrotta * Thiago Barroso Perrotta

View File

@ -402,6 +402,8 @@ Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
Open a page from the clipboard. Open a page from the clipboard.
If the pasted text contains newlines, each line gets opened in its own tab.
==== optional arguments ==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
@ -1224,6 +1226,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|Command|Description |Command|Description
|<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log. |<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log.
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats. |<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|<<debug-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|<<debug-console,debug-console>>|Show the debugging console. |<<debug-console,debug-console>>|Show the debugging console.
|<<debug-crash,debug-crash>>|Crash for debugging purposes. |<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file. |<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
@ -1239,6 +1242,10 @@ Print a list of all objects to the debug log.
=== debug-cache-stats === debug-cache-stats
Print LRU cache stats. Print LRU cache stats.
[[debug-clear-ssl-errors]]
=== debug-clear-ssl-errors
Clear remembered SSL error answers.
[[debug-console]] [[debug-console]]
=== debug-console === debug-console
Show the debugging console. Show the debugging console.
@ -1266,13 +1273,16 @@ Dump the current page's content to a file.
[[debug-pyeval]] [[debug-pyeval]]
=== debug-pyeval === debug-pyeval
Syntax: +:debug-pyeval 's'+ Syntax: +:debug-pyeval [*--quiet*] 's'+
Evaluate a python string and display the results as a web page. Evaluate a python string and display the results as a web page.
==== positional arguments ==== positional arguments
* +'s'+: The string to evaluate. * +'s'+: The string to evaluate.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show the output in a new tab.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command. * With this command, +;;+ is interpreted literally instead of splitting off a second command.

View File

@ -11,6 +11,9 @@ markers =
not_xvfb: Tests which can't be run with Xvfb. not_xvfb: Tests which can't be run with Xvfb.
frozen: Tests which can only be run if sys.frozen is True. frozen: Tests which can only be run if sys.frozen is True.
integration: Tests which test a bigger portion of code, run without coverage. integration: Tests which test a bigger portion of code, run without coverage.
skip: Always skipped test.
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
xfail_norun: xfail the test with out running it
flakes-ignore = flakes-ignore =
UnusedImport UnusedImport
UnusedVariable UnusedVariable
@ -39,3 +42,6 @@ qt_log_ignore =
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom ^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .* ^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
^QXcbClipboard: SelectionRequest too old ^QXcbClipboard: SelectionRequest too old
^QGeoclueMaster error creating GeoclueMasterClient\.
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
qt_wait_signal_raising = true

View File

@ -809,6 +809,9 @@ class CommandDispatcher:
def paste(self, sel=False, tab=False, bg=False, window=False): def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard. """Open a page from the clipboard.
If the pasted text contains newlines, each line gets opened in its own
tab.
Args: Args:
sel: Use the primary selection instead of the clipboard. sel: Use the primary selection instead of the clipboard.
tab: Open in a new tab. tab: Open in a new tab.
@ -825,12 +828,18 @@ class CommandDispatcher:
text = clipboard.text(mode) text = clipboard.text(mode)
if not text: if not text:
raise cmdexc.CommandError("{} is empty.".format(target)) raise cmdexc.CommandError("{} is empty.".format(target))
log.misc.debug("{} contained: '{}'".format(target, text)) log.misc.debug("{} contained: '{}'".format(target,
try: text.replace('\n', '\\n')))
url = urlutils.fuzzy_url(text) text_urls = enumerate(u for u in text.split('\n') if u)
except urlutils.InvalidUrlError as e: for i, text_url in text_urls:
raise cmdexc.CommandError(e) if not window and i > 0:
self._open(url, tab, bg, window) tab = False
bg = True
try:
url = urlutils.fuzzy_url(text_url)
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count') count='count')
@ -1462,11 +1471,11 @@ class CommandDispatcher:
webview = self._current_widget() webview = self._current_widget()
if not webview.selection_enabled: if not webview.selection_enabled:
act = [QWebPage.MoveToNextWord] act = [QWebPage.MoveToNextWord]
if sys.platform == 'win32': if sys.platform == 'win32': # pragma: no cover
act.append(QWebPage.MoveToPreviousChar) act.append(QWebPage.MoveToPreviousChar)
else: else:
act = [QWebPage.SelectNextWord] act = [QWebPage.SelectNextWord]
if sys.platform == 'win32': if sys.platform == 'win32': # pragma: no cover
act.append(QWebPage.SelectPreviousChar) act.append(QWebPage.SelectPreviousChar)
for _ in range(count): for _ in range(count):
for a in act: for a in act:
@ -1483,11 +1492,11 @@ class CommandDispatcher:
webview = self._current_widget() webview = self._current_widget()
if not webview.selection_enabled: if not webview.selection_enabled:
act = [QWebPage.MoveToNextWord] act = [QWebPage.MoveToNextWord]
if sys.platform != 'win32': if sys.platform != 'win32': # pragma: no branch
act.append(QWebPage.MoveToNextChar) act.append(QWebPage.MoveToNextChar)
else: else:
act = [QWebPage.SelectNextWord] act = [QWebPage.SelectNextWord]
if sys.platform != 'win32': if sys.platform != 'win32': # pragma: no branch
act.append(QWebPage.SelectNextChar) act.append(QWebPage.SelectNextChar)
for _ in range(count): for _ in range(count):
for a in act: for a in act:
@ -1755,3 +1764,10 @@ class CommandDispatcher:
QApplication.postEvent(receiver, press_event) QApplication.postEvent(receiver, press_event)
QApplication.postEvent(receiver, release_event) QApplication.postEvent(receiver, release_event)
@cmdutils.register(instance='command-dispatcher', scope='window',
debug=True)
def debug_clear_ssl_errors(self):
"""Clear remembered SSL error answers."""
nam = self._current_widget().page().networkAccessManager()
nam.clear_all_ssl_errors()

View File

@ -484,8 +484,10 @@ class HintManager(QObject):
mode = QClipboard.Selection if sel else QClipboard.Clipboard mode = QClipboard.Selection if sel else QClipboard.Clipboard
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
QApplication.clipboard().setText(urlstr, mode) QApplication.clipboard().setText(urlstr, mode)
message.info(self._win_id, "URL yanked to {}".format( msg = "Yanked URL to {}: {}".format(
"primary selection" if sel else "clipboard")) "primary selection" if sel else "clipboard",
urlstr)
message.info(self._win_id, msg)
def _run_cmd(self, url, context): def _run_cmd(self, url, context):
"""Run the command based on a hint URL. """Run the command based on a hint URL.

View File

@ -19,6 +19,7 @@
"""Our own QNetworkAccessManager.""" """Our own QNetworkAccessManager."""
import os
import collections import collections
import netrc import netrc
@ -29,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
urlutils) urlutils, debug)
from qutebrowser.browser import cookies from qutebrowser.browser import cookies
from qutebrowser.browser.network import qutescheme, networkreply from qutebrowser.browser.network import qutescheme, networkreply
from qutebrowser.browser.network import filescheme from qutebrowser.browser.network import filescheme
@ -62,6 +63,11 @@ class SslError(QSslError):
except TypeError: except TypeError:
return hash((self.certificate().toDer(), self.error())) return hash((self.certificate().toDer(), self.error()))
def __repr__(self):
return utils.get_repr(
self, error=debug.qenum_key(QSslError, self.error()),
string=self.errorString())
class NetworkManager(QNetworkAccessManager): class NetworkManager(QNetworkAccessManager):
@ -189,44 +195,51 @@ class NetworkManager(QNetworkAccessManager):
""" """
errors = [SslError(e) for e in errors] errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict') ssl_strict = config.get('network', 'ssl-strict')
log.webview.debug("SSL errors {!r}, strict {}".format(
errors, ssl_strict))
try:
host_tpl = urlutils.host_tuple(reply.url())
except ValueError:
host_tpl = None
is_accepted = False
is_rejected = False
else:
is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
return
elif is_accepted:
reply.ignoreSslErrors()
return
if ssl_strict == 'ask': if ssl_strict == 'ask':
try: err_string = '\n'.join('- ' + err.errorString() for err in errors)
host_tpl = urlutils.host_tuple(reply.url()) answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
except ValueError: mode=usertypes.PromptMode.yesno, owner=reply)
host_tpl = None if answer:
is_accepted = False
is_rejected = False
else:
is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
if is_accepted:
reply.ignoreSslErrors() reply.ignoreSslErrors()
elif is_rejected: err_dict = self._accepted_ssl_errors
pass
else: else:
err_string = '\n'.join('- ' + err.errorString() for err in err_dict = self._rejected_ssl_errors
errors) if host_tpl is not None:
answer = self._ask('SSL errors - continue?\n{}'.format( err_dict[host_tpl] += errors
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass
else: else:
for err in errors: for err in errors:
# FIXME we might want to use warn here (non-fatal error) # FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114 # https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id, message.error(self._win_id, 'SSL error: {}'.format(
'SSL error: {}'.format(err.errorString())) err.errorString()))
reply.ignoreSslErrors() reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors
def clear_all_ssl_errors(self):
"""Clear all remembered SSL errors."""
self._accepted_ssl_errors.clear()
self._rejected_ssl_errors.clear()
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url): def clear_rejected_ssl_errors(self, url):
@ -244,7 +257,10 @@ class NetworkManager(QNetworkAccessManager):
def on_authentication_required(self, reply, authenticator): def on_authentication_required(self, reply, authenticator):
"""Called when a website needs authentication.""" """Called when a website needs authentication."""
user, password = None, None user, password = None, None
if not hasattr(reply, "netrc_used"): if not hasattr(reply, "netrc_used") and 'HOME' in os.environ:
# We'll get an OSError by netrc if 'HOME' isn't available in
# os.environ. We don't want to log that, so we prevent it
# altogether.
reply.netrc_used = True reply.netrc_used = True
try: try:
net = netrc.netrc() net = netrc.netrc()

View File

@ -258,6 +258,13 @@ class String(BaseType):
self._basic_validation(value) self._basic_validation(value)
if not value: if not value:
return return
if self.valid_values is not None:
if value not in self.valid_values:
raise configexc.ValidationError(
value, "valid values: {}".format(', '.join(
self.valid_values)))
if self.forbidden is not None and any(c in value if self.forbidden is not None and any(c in value
for c in self.forbidden): for c in self.forbidden):
raise configexc.ValidationError(value, "may not contain the chars " raise configexc.ValidationError(value, "may not contain the chars "
@ -270,7 +277,10 @@ class String(BaseType):
"long!".format(self.maxlen)) "long!".format(self.maxlen))
def complete(self): def complete(self):
return self._completions if self._completions is not None:
return self._completions
else:
return super().complete()
class List(BaseType): class List(BaseType):

View File

@ -417,9 +417,6 @@ class MainWindow(QWidget):
window=self.win_id) window=self.win_id)
download_count = download_manager.rowCount() download_count = download_manager.rowCount()
quit_texts = [] quit_texts = []
# Close if set to never ask for confirmation
if 'never' in confirm_quit:
pass
# Ask if multiple-tabs are open # Ask if multiple-tabs are open
if 'multiple-tabs' in confirm_quit and tab_count > 1: if 'multiple-tabs' in confirm_quit and tab_count > 1:
quit_texts.append("{} {} open.".format( quit_texts.append("{} {} open.".format(

View File

@ -229,8 +229,9 @@ class IPCServer(QObject):
log.ipc.debug("In on_error with None socket!") log.ipc.debug("In on_error with None socket!")
return return
self._timer.stop() self._timer.stop()
log.ipc.debug("Socket error {}: {}".format( log.ipc.debug("Socket 0x{:x}: error {}: {}".format(
self._socket.error(), self._socket.errorString())) id(self._socket), self._socket.error(),
self._socket.errorString()))
if err != QLocalSocket.PeerClosedError: if err != QLocalSocket.PeerClosedError:
raise SocketError("handling IPC connection", self._socket) raise SocketError("handling IPC connection", self._socket)
@ -241,13 +242,14 @@ class IPCServer(QObject):
return return
if self._socket is not None: if self._socket is not None:
log.ipc.debug("Got new connection but ignoring it because we're " log.ipc.debug("Got new connection but ignoring it because we're "
"still handling another one.") "still handling another one (0x{:x}).".format(
id(self._socket)))
return return
socket = self._server.nextPendingConnection() socket = self._server.nextPendingConnection()
if socket is None: if socket is None:
log.ipc.debug("No new connection to handle.") log.ipc.debug("No new connection to handle.")
return return
log.ipc.debug("Client connected.") log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
self._timer.start() self._timer.start()
self._socket = socket self._socket = socket
socket.readyRead.connect(self.on_ready_read) socket.readyRead.connect(self.on_ready_read)
@ -267,7 +269,8 @@ class IPCServer(QObject):
@pyqtSlot() @pyqtSlot()
def on_disconnected(self): def on_disconnected(self):
"""Clean up socket when the client disconnected.""" """Clean up socket when the client disconnected."""
log.ipc.debug("Client disconnected.") log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
id(self._socket)))
self._timer.stop() self._timer.stop()
if self._socket is None: if self._socket is None:
log.ipc.debug("In on_disconnected with None socket!") log.ipc.debug("In on_disconnected with None socket!")
@ -279,7 +282,8 @@ class IPCServer(QObject):
def _handle_invalid_data(self): def _handle_invalid_data(self):
"""Handle invalid data we got from a QLocalSocket.""" """Handle invalid data we got from a QLocalSocket."""
log.ipc.error("Ignoring invalid IPC data.") log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format(
id(self._socket)))
self.got_invalid_data.emit() self.got_invalid_data.emit()
self._socket.error.connect(self.on_error) self._socket.error.connect(self.on_error)
self._socket.disconnectFromServer() self._socket.disconnectFromServer()
@ -292,11 +296,12 @@ class IPCServer(QObject):
# active for some reason. # active for some reason.
log.ipc.warning("In on_ready_read with None socket!") log.ipc.warning("In on_ready_read with None socket!")
return return
self._timer.start() self._timer.stop()
while self._socket is not None and self._socket.canReadLine(): while self._socket is not None and self._socket.canReadLine():
data = bytes(self._socket.readLine()) data = bytes(self._socket.readLine())
self.got_raw.emit(data) self.got_raw.emit(data)
log.ipc.debug("Read from socket: {}".format(data)) log.ipc.debug("Read from socket 0x{:x}: {}".format(
id(self._socket), data))
try: try:
decoded = data.decode('utf-8') decoded = data.decode('utf-8')
@ -337,11 +342,13 @@ class IPCServer(QObject):
cwd = json_data.get('cwd', None) cwd = json_data.get('cwd', None)
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd) self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
self._timer.start()
@pyqtSlot() @pyqtSlot()
def on_timeout(self): def on_timeout(self):
"""Cancel the current connection if it was idle for too long.""" """Cancel the current connection if it was idle for too long."""
log.ipc.error("IPC connection timed out.") log.ipc.error("IPC connection timed out "
"(socket 0x{:x}).".format(id(self._socket)))
self._socket.disconnectFromServer() self._socket.disconnectFromServer()
if self._socket is not None: # pragma: no cover if self._socket is not None: # pragma: no cover
# on_socket_disconnected sets it to None # on_socket_disconnected sets it to None
@ -369,7 +376,8 @@ class IPCServer(QObject):
def shutdown(self): def shutdown(self):
"""Shut down the IPC server cleanly.""" """Shut down the IPC server cleanly."""
log.ipc.debug("Shutting down IPC") log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
id(self._socket)))
if self._socket is not None: if self._socket is not None:
self._socket.deleteLater() self._socket.deleteLater()
self._socket = None self._socket = None

View File

@ -35,6 +35,8 @@ from qutebrowser.config import style
from qutebrowser.misc import consolewidget from qutebrowser.misc import consolewidget
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
# so it's available for :debug-pyeval
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id') @cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
@ -176,18 +178,23 @@ def debug_trace(expr=""):
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True) @cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
def debug_pyeval(s): def debug_pyeval(s, quiet=False):
"""Evaluate a python string and display the results as a web page. """Evaluate a python string and display the results as a web page.
Args: Args:
s: The string to evaluate. s: The string to evaluate.
quiet: Don't show the output in a new tab.
""" """
try: try:
r = eval(s) r = eval(s)
out = repr(r) out = repr(r)
except Exception: except Exception:
out = traceback.format_exc() out = traceback.format_exc()
qutescheme.pyeval_output = out qutescheme.pyeval_output = out
tabbed_browser = objreg.get('tabbed-browser', scope='window', if quiet:
window='last-focused') log.misc.debug("pyeval output: {}".format(out))
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)

View File

@ -168,6 +168,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
Return: Return:
A target QUrl to a search page or the original URL. A target QUrl to a search page or the original URL.
""" """
urlstr = urlstr.strip()
expanded = os.path.expanduser(urlstr) expanded = os.path.expanduser(urlstr)
if os.path.isabs(expanded): if os.path.isabs(expanded):
path = expanded path = expanded
@ -181,11 +182,10 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
else: else:
path = None path = None
stripped = urlstr.strip()
if path is not None and os.path.exists(path): if path is not None and os.path.exists(path):
log.url.debug("URL is a local file") log.url.debug("URL is a local file")
url = QUrl.fromLocalFile(path) url = QUrl.fromLocalFile(path)
elif (not do_search) or is_url(stripped): elif (not do_search) or is_url(urlstr):
# probably an address # probably an address
log.url.debug("URL is a fuzzy address") log.url.debug("URL is a fuzzy address")
url = qurl_from_user_input(urlstr) url = qurl_from_user_input(urlstr)
@ -194,7 +194,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
try: try:
url = _get_search_url(urlstr) url = _get_search_url(urlstr)
except ValueError: # invalid search engine except ValueError: # invalid search engine
url = qurl_from_user_input(stripped) url = qurl_from_user_input(urlstr)
log.url.debug("Converting fuzzy term {} to URL -> {}".format( log.url.debug("Converting fuzzy term {} to URL -> {}".format(
urlstr, url.toDisplayString())) urlstr, url.toDisplayString()))
if do_search and config.get('general', 'auto-search'): if do_search and config.get('general', 'auto-search'):

View File

@ -1,10 +1,8 @@
Jinja2==2.8.0 Jinja2==2.8.0
MarkupSafe==0.23 MarkupSafe==0.23
Pygments==2.0.2 Pygments==2.1
pyPEG2==2.15.2 pyPEG2==2.15.2
PyYAML==3.11 PyYAML==3.11
# "ValueError: I/O operation on closed file" with pytest since 0.3.5 colorama==0.3.6
# WORKAROUND for https://github.com/tartley/colorama/issues/81
colorama==0.3.3 # rq.filter: <=0.3.3
colorlog==2.6.0 colorlog==2.6.0
cssutils==1.0.1 cssutils==1.0.1

View File

@ -100,6 +100,8 @@ PERFECT_FILES = [
'qutebrowser/mainwindow/statusbar/tabindex.py'), 'qutebrowser/mainwindow/statusbar/tabindex.py'),
('tests/unit/mainwindow/statusbar/test_textbase.py', ('tests/unit/mainwindow/statusbar/test_textbase.py',
'qutebrowser/mainwindow/statusbar/textbase.py'), 'qutebrowser/mainwindow/statusbar/textbase.py'),
('tests/unit/mainwindow/statusbar/test_prompt.py',
'qutebrowser/mainwindow/statusbar/prompt.py'),
('tests/unit/config/test_configtypes.py', ('tests/unit/config/test_configtypes.py',
'qutebrowser/config/configtypes.py'), 'qutebrowser/config/configtypes.py'),
@ -133,6 +135,10 @@ PERFECT_FILES = [
] ]
# 100% coverage because of integration tests, but no perfect unit tests yet.
WHITELISTED_FILES = []
class Skipped(Exception): class Skipped(Exception):
"""Exception raised when skipping coverage checks.""" """Exception raised when skipping coverage checks."""
@ -199,7 +205,8 @@ def check(fileobj, perfect_files):
text = "{} has {}% line and {}% branch coverage!".format( text = "{} has {}% line and {}% branch coverage!".format(
filename, line_cov, branch_cov) filename, line_cov, branch_cov)
messages.append(Message(MsgType.insufficent_coverage, text)) messages.append(Message(MsgType.insufficent_coverage, text))
elif filename not in perfect_src_files and not is_bad: elif (filename not in perfect_src_files and not is_bad and
filename not in WHITELISTED_FILES):
text = ("{} has 100% coverage but is not in " text = ("{} has 100% coverage but is not in "
"perfect_files!".format(filename)) "perfect_files!".format(filename))
messages.append(Message(MsgType.perfect_file, text)) messages.append(Message(MsgType.perfect_file, text))

View File

@ -39,13 +39,16 @@ from helpers.messagemock import message_mock
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import objreg from qutebrowser.utils import objreg
from PyQt5.QtCore import QEvent from PyQt5.QtCore import QEvent, QSize, Qt, PYQT_VERSION
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtNetwork import QNetworkCookieJar from PyQt5.QtNetwork import QNetworkCookieJar
import xvfbwrapper import xvfbwrapper
# Set hypothesis settings # Set hypothesis settings
hypothesis.Settings.default.strict = True # pylint: disable=no-member hypothesis.settings.register_profile('default',
hypothesis.settings(strict=True))
hypothesis.settings.load_profile('default')
def _apply_platform_markers(item): def _apply_platform_markers(item):
@ -62,6 +65,9 @@ def _apply_platform_markers(item):
"Can only run when frozen"), "Can only run when frozen"),
('not_xvfb', item.config.xvfb_display is not None, ('not_xvfb', item.config.xvfb_display is not None,
"Can't be run with Xvfb."), "Can't be run with Xvfb."),
('skip', True, "Always skipped."),
('pyqt531_or_newer', PYQT_VERSION < 0x050301,
"Needs PyQt 5.3.1 or newer"),
] ]
for searched_marker, condition, default_reason in markers: for searched_marker, condition, default_reason in markers:
@ -108,7 +114,7 @@ def pytest_collection_modifyitems(items):
item.add_marker('gui') item.add_marker('gui')
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''): if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
if ('CI' in os.environ and if ('CI' in os.environ and
not os.environ.get('QUTE_NO_DISPLAY_OK', '')): not os.environ.get('QUTE_NO_DISPLAY', '')):
raise Exception("No display available on CI!") raise Exception("No display available on CI!")
skip_marker = pytest.mark.skipif( skip_marker = pytest.mark.skipif(
True, reason="No DISPLAY available") True, reason="No DISPLAY available")
@ -124,6 +130,8 @@ def pytest_collection_modifyitems(items):
item.add_marker(pytest.mark.integration) item.add_marker(pytest.mark.integration)
_apply_platform_markers(item) _apply_platform_markers(item)
if item.get_marker('xfail_norun'):
item.add_marker(pytest.mark.xfail(run=False))
def pytest_ignore_collect(path): def pytest_ignore_collect(path):
@ -161,6 +169,41 @@ class WinRegistryHelper:
del objreg.window_registry[win_id] del objreg.window_registry[win_id]
class FakeStatusBar(QWidget):
"""Fake statusbar to test progressbar sizing."""
def __init__(self, parent=None):
super().__init__(parent)
self.hbox = QHBoxLayout(self)
self.hbox.addStretch()
self.hbox.setContentsMargins(0, 0, 0, 0)
self.setAttribute(Qt.WA_StyledBackground, True)
self.setStyleSheet('background-color: red;')
def minimumSizeHint(self):
return QSize(1, self.fontMetrics().height())
@pytest.fixture
def fake_statusbar(qtbot):
"""Fixture providing a statusbar in a container window."""
container = QWidget()
qtbot.add_widget(container)
vbox = QVBoxLayout(container)
vbox.addStretch()
statusbar = FakeStatusBar(container)
# to make sure container isn't GCed
# pylint: disable=attribute-defined-outside-init
statusbar.container = container
vbox.addWidget(statusbar)
container.show()
qtbot.waitForWindowShown(container)
return statusbar
@pytest.yield_fixture @pytest.yield_fixture
def win_registry(): def win_registry():
"""Fixture providing a window registry for win_id 0 and 1.""" """Fixture providing a window registry for win_id 0 and 1."""
@ -373,7 +416,10 @@ def pytest_configure(config):
if os.environ.get('DISPLAY', None) == '': if os.environ.get('DISPLAY', None) == '':
# xvfbwrapper doesn't handle DISPLAY="" correctly # xvfbwrapper doesn't handle DISPLAY="" correctly
del os.environ['DISPLAY'] del os.environ['DISPLAY']
if sys.platform.startswith('linux') and not config.getoption('--no-xvfb'):
if (sys.platform.startswith('linux') and
not config.getoption('--no-xvfb') and
'QUTE_NO_DISPLAY' not in os.environ):
assert 'QUTE_BUILDBOT' not in os.environ assert 'QUTE_BUILDBOT' not in os.environ
try: try:
disp = xvfbwrapper.Xvfb(width=800, height=600, colordepth=16) disp = xvfbwrapper.Xvfb(width=800, height=600, colordepth=16)

View File

@ -21,6 +21,6 @@
"""Things needed for integration testing.""" """Things needed for integration testing."""
from webserver import httpbin, httpbin_after_test from webserver import httpbin, httpbin_after_test, ssl_server
from quteprocess import quteproc_process, quteproc from quteprocess import quteproc_process, quteproc
from testprocess import pytest_runtest_makereport from testprocess import pytest_runtest_makereport

View File

@ -5,7 +5,7 @@
<title>Caret mode</title> <title>Caret mode</title>
</head> </head>
<body> <body>
<p>one two three<br/>eins zwei drei</p> <p><a href="/data/hello.txt">one</a> two three<br/>eins zwei drei</p>
<p>four five six<br/>vier fünf sechs</p> <p>four five six<br/>vier fünf sechs</p>
</body> </body>
</html> </html>

View File

@ -11,8 +11,10 @@ body .c { color: #408080; font-style: italic } /* Comment */
body .err { border: 1px solid #FF0000 } /* Error */ body .err { border: 1px solid #FF0000 } /* Error */
body .k { color: #008000; font-weight: bold } /* Keyword */ body .k { color: #008000; font-weight: bold } /* Keyword */
body .o { color: #666666 } /* Operator */ body .o { color: #666666 } /* Operator */
body .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
body .cp { color: #BC7A00 } /* Comment.Preproc */ body .cp { color: #BC7A00 } /* Comment.Preproc */
body .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
body .c1 { color: #408080; font-style: italic } /* Comment.Single */ body .c1 { color: #408080; font-style: italic } /* Comment.Single */
body .cs { color: #408080; font-style: italic } /* Comment.Special */ body .cs { color: #408080; font-style: italic } /* Comment.Special */
body .gd { color: #A00000 } /* Generic.Deleted */ body .gd { color: #A00000 } /* Generic.Deleted */
@ -75,8 +77,8 @@ body .il { color: #666666 } /* Literal.Number.Integer.Long */
<h2></h2> <h2></h2>
<table class="highlighttable"><tbody><tr><td class="linenos"><div class="linenodiv"><pre>1 <table class="highlighttable"><tbody><tr><td class="linenos"><div class="linenodiv"><pre>1
2</pre></div></td><td class="code"><div class="highlight"><pre><span class="nt">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;pre</span> <span class="na">style=</span><span class="s">"word-wrap: break-word; white-space: pre-wrap;"</span><span class="nt">&gt;</span>Hello World! 2</pre></div></td><td class="code"><div class="highlight"><pre><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;&lt;</span><span class="nt">head</span><span class="p">&gt;&lt;/</span><span class="nt">head</span><span class="p">&gt;&lt;</span><span class="nt">body</span><span class="p">&gt;&lt;</span><span class="nt">pre</span> <span class="na">style</span><span class="o">=</span><span class="s">"word-wrap: break-word; white-space: pre-wrap;"</span><span class="p">&gt;</span>Hello World!
<span class="nt">&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;</span> <span class="p">&lt;/</span><span class="nt">pre</span><span class="p">&gt;&lt;/</span><span class="nt">body</span><span class="p">&gt;&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre></div> </pre></div>
</td></tr></tbody></table> </td></tr></tbody></table>

View File

@ -0,0 +1 @@
ten

View File

@ -0,0 +1 @@
eleven

View File

@ -0,0 +1 @@
twelve

View File

@ -0,0 +1 @@
thirteen

View File

@ -0,0 +1 @@
fourteen

View File

@ -0,0 +1 @@
eight

View File

@ -0,0 +1 @@
nine

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
function on_success(position) {
console.log("geolocation permission granted");
}
function on_error(error) {
switch(error.code) {
case error.PERMISSION_DENIED:
console.log("geolocation permission denied");
break;
case error.POSITION_UNAVAILABLE:
console.log("geolocation position unavailable (ignored)");
break;
default:
console.log("[FAIL] geolocation error " + error.code +
": " + error.message);
break;
}
}
function get_location() {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(on_success, on_error);
} else {
console.log("[SKIP] geolocation unavailable");
}
}
</script>
</head>
<body>
<input type="button" onclick="get_location()" value="Get position">
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
function do_alert() {
alert("js alert");
console.log("Alert done");
}
</script>
</head>
<body>
<input type="button" onclick="do_alert()" value="Show alert">
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
function prompter() {
var reply = confirm("js confirm", "")
console.log("confirm reply: " + reply)
}
</script>
</head>
<body>
<input type="button" onclick="prompter()" value="Show prompt">
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
function prompter() {
var reply = prompt("js prompt", "")
console.log("Prompt reply: " + reply)
}
</script>
</head>
<body>
<input type="button" onclick="prompter()" value="Show prompt">
</body>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
function permission_cb(permission) {
switch (permission) {
case "granted":
console.log("notification permission granted");
break;
case "denied":
console.log("notification permission denied");
break;
case "default":
console.log("notification permission aborted");
break;
default:
console.log("[FAIL] unknown value for permission: " + Notification.permission);
break;
}
}
function get_notification_permission() {
if ("Notification" in window) {
if (Notification.permission === "default") {
Notification.requestPermission(permission_cb);
} else {
console.log("[FAIL] unknown initial value for Notification.permission: " + Notification.permission);
}
} else {
console.log("[FAIL] notifications unavailable");
}
}
</script>
</head>
<body>
<input type="button" onclick="get_notification_permission()" value="Get notification permission">
</body>
</html>

View File

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Searching text on the page</title>
</head>
<body>
<p>
foo<br/>
Foo<br/>
Bar<br/>
bar<br/>
blüb<br/>
baz<br/>
Baz<br/>
BAZ<br/>
space travel<br/>
/slash<br/>
</p>
</body>
</html>

View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICpTCCAY0CAQAwYDElMCMGA1UECgwccXV0ZWJyb3dzZXIgdGVzdCBjZXJ0aWZp
Y2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MSMwIQYJKoZIhvcNAQkBFhRtYWlsQHF1
dGVicm93c2VyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO77
e6QqjeGDjq8tDCGSEi+7m/cDL6PbX8zNNKoVplcoJjoPC/6KmdsLin4SO3iAd5ti
XOpPQqyCBgBUd7axP5Ya6M6rhWJaYUczUMdx8bRr4mdaTbd/UhVM/dI1vS/LvBKH
OY+8k3E6Neb5jeDe2dfXgokURL4c/jIS1MDumvYCAteoHRYvjGcTSDERr0DT0DY4
oPyrImabSHRGXLz0euQsMY4d9ZTakomYH52cRMNEOKArU1ARNZ0UyHzumuSkjIFV
G5PFgMra0tgAPdCA1sx51cQUBOYxnqMdgOBThonrbusYYR17D7TqsvC6R9E0HWhF
b4JJkPB3EDVEzWqQFgcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBC7JrJuHyF
YFiujBlXFZIQrPNW7FF28zqBuXLfviwVBF/sKmNMKwC0nUgmCb/wFPxv3yrj+7az
r29FWSGVhs6k15GVsqSwnbSJDznh/W1elWwpTo2GODMmRY3VeYSY9WiQUhe5KA5x
56p5Kgtl53wZzdl+Pi93xVYAZFWl2O3GFs4f+GCrORjHC7ejZoq6xfRzNLZbLF0a
QyptcnYaZSppDB/nZx4p75GKcj9qWXaJbT8mjqJdgRCFPyUkQjSY6WEEAP3LXrXx
ThZUekv81Jh+kPTZjSd1d24Bd0nFkQdFf8SRn21jnP+PrzipBOdvm+bT8dI/71xg
8ZJ631jogV4L
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPDCCAiQCCQCHskwLQC4vHDANBgkqhkiG9w0BAQsFADBgMSUwIwYDVQQKDBxx
dXRlYnJvd3NlciB0ZXN0IGNlcnRpZmljYXRlMRIwEAYDVQQDDAlsb2NhbGhvc3Qx
IzAhBgkqhkiG9w0BCQEWFG1haWxAcXV0ZWJyb3dzZXIub3JnMB4XDTE2MDExMjE4
NDYyM1oXDTI2MDEwOTE4NDYyM1owYDElMCMGA1UECgwccXV0ZWJyb3dzZXIgdGVz
dCBjZXJ0aWZpY2F0ZTESMBAGA1UEAwwJbG9jYWxob3N0MSMwIQYJKoZIhvcNAQkB
FhRtYWlsQHF1dGVicm93c2VyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAO77e6QqjeGDjq8tDCGSEi+7m/cDL6PbX8zNNKoVplcoJjoPC/6KmdsL
in4SO3iAd5tiXOpPQqyCBgBUd7axP5Ya6M6rhWJaYUczUMdx8bRr4mdaTbd/UhVM
/dI1vS/LvBKHOY+8k3E6Neb5jeDe2dfXgokURL4c/jIS1MDumvYCAteoHRYvjGcT
SDERr0DT0DY4oPyrImabSHRGXLz0euQsMY4d9ZTakomYH52cRMNEOKArU1ARNZ0U
yHzumuSkjIFVG5PFgMra0tgAPdCA1sx51cQUBOYxnqMdgOBThonrbusYYR17D7Tq
svC6R9E0HWhFb4JJkPB3EDVEzWqQFgcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
lTuJK8wseifpepUaWIev+59ulxxMzeippi+xqoYnjrNjINNdk5Wh+Dj7Crb5R8dn
afkC+XE9PMKEvKBmQZj/KVEL/G7bjZBA73oibKpBMWIdxaIwSFN2Xq4zKWLHESrb
2Wy8MiehZiSdgUtnmTPM0BlDmc6u9/0nLdCjsBoKYVOLw2FDcD1P8NOJT0dUjSUu
aYmUakcn+lQEjuBplrsGvL0vCGR/kzG2vwoTuGnx66HURuHU6E7yBTQ2diyhzOQc
sMwwDfrsY19K3IH6AuVcCgGit1LE/zCqMFQuFrIhYB5Mt5bLSeWVBDzKClxZB0Di
OxK2sWZvLdGLsFltKB+IJA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA7vt7pCqN4YOOry0MIZISL7ub9wMvo9tfzM00qhWmVygmOg8L
/oqZ2wuKfhI7eIB3m2Jc6k9CrIIGAFR3trE/lhrozquFYlphRzNQx3HxtGviZ1pN
t39SFUz90jW9L8u8Eoc5j7yTcTo15vmN4N7Z19eCiRREvhz+MhLUwO6a9gIC16gd
Fi+MZxNIMRGvQNPQNjig/KsiZptIdEZcvPR65Cwxjh31lNqSiZgfnZxEw0Q4oCtT
UBE1nRTIfO6a5KSMgVUbk8WAytrS2AA90IDWzHnVxBQE5jGeox2A4FOGietu6xhh
HXsPtOqy8LpH0TQdaEVvgkmQ8HcQNUTNapAWBwIDAQABAoIBADysrryEbVdHLm+9
USooyuNBj5yMO4kvhkgaBXf1XTEdqW7uKQ5sJBnf+T5+5Ih4nWVe+NYoX3Yq4Nku
mOJSaCF1HYxzMb9B0RbhqW2puUMkbOvumnKvKajszjiTmj/LSymtGWkr6IdDzzGg
RGxGSCqrtaGV+soF1GfkLg35xnAUnwk3pfVqGyXl66+bCCWcqXZTUlOB55KEa+5F
9rkMlS6/X3DGZLvON7ZtZqZe7E8Foo9qU1VSHHfxIkS5P4UNxjf7woQogmhNTRT6
tX0SmDQdP59sdFJ09Expr2AfSFxfkGuQf+JSG/JMprg0ub0ksw7UZvaW1uJNKL9I
XQSVPgECgYEA94DlPsGd8wWllMjOIEDkERUP2s4uJjPb6jodqewf9tuyxuwRnpOs
fb5uq7mMJXG3sszqom0q3DBoapNdCX1vTywWHKc1Nik5PT7jbEXFaRLfvA/F8WfF
6Rugm/S+nezTc7XhtDnOpfl+7wFSJy0we0C3RvxJqAaLaQRDobeNiQcCgYEA9y+z
wdXaOcJnC5bPO3ollFewX00WJaAAFpDnfqC3ALJx94/xJVJW6A7TZnKKJmWQ/bFz
0iuyhMe3Nd2yzAhl0qs0lmVe2V2tgJO/CVVP8OQmwlHKSZssDCjaBrHIkNwdL00j
qtSYg/FafLPL24AFSr25+sBn/FfxHTzlWVlWywECgYAUyjX3dIoQ/NtwyQFPgkPm
D2/agFEuElMZtLIDMPtqX///Z5r/SAZINbPUJuzXxFqa4U2gQS1Fe6d5tFEvV+L+
soRU+dKlbwcI1vyBfsbbUaOLh4OoCIB+WTy/fOp6F4eXg6Km4egy1udLqj+9XLVi
1QfQJacGPy58rsgDkIiKBwKBgHtVtd91kNlZAolpyiTnIXEO/9XNZMuJNgIMczVf
g3A5mVvo2m3A09Qd8aUgaYYXD21F6YBohT5zWBrsb5YWapffDPItylGyyCtrjNpf
Uu/jJuO2Y7SuVCANEhxdALIm4fkECFPol+DdwESQgZsYGYvddrqC3l+ukYQBKn6W
cRQBAoGBAMA8tN67zOtZWalkokLHPDDK/TRUI/+Idc7xX7Rx/KZLuhfT26pLbe5Q
onbhe+TSq+4aYfUdcWJE2oM8DQn6CrNZFXKhz/0DLE+leASwwJLNCBbDdLjij2sy
7x2VeGKVG7V2KEhqcDUH/TO0e9PeGnz0vnebzN2+EZue6J9OTfLr
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQISAypJ52ykvkCAggA
MBQGCCqGSIb3DQMHBAgcPyy/O0hXkgSCBMgA4rZIvVKE73SsGCpJou1LgGAuPX1m
qyOPwRGC9T1p1HPaMcucIKpZPp5JSx3B9xwN/V+gpi3XXU1oTLaJhXwOpp8v106l
lR9Us91o4nUWVmo2C6nG1z/GSP573RBqjxChiHQchjT5UufKOi+/0elg6tgpu2cv
k+CLcgKp80dUr+UOPLAqIC2B+ex4BQHPrki+wbsTeMoZaXnPcTbl0OjABXbG6X4l
Gf2xftM7I+Wr/E7dnOEHGwUUH4hAzqflgUTHTZZtUDU7v99ggBBRux5vp9Zi2gWp
ksuAmxfPDMcE1Mpu+ZTZ3+cp4TWuWwKRpCX9USjmwnkhdEhqc/arHID/Db+SNP6z
lrdHY7BeAWcwDTo+4KZAEK7LKpAukRvpLcyvufo/smGaXsYytFz6Un8scSoySuqo
TEKyAioxNsOGJ2Xz5Jt+tdNLO/5W4jCuvwPx1GDlumwPcMHjDrXlZUa0qfoJCcun
lptbxZfqd7ouXLy1OF5FAsLs/iCmBwsyOS/qysFwq442WEwT/qn3ZoGBNkkJahu4
OQ5sA14+nZHsBp1+iXZZxKmAERvQfFIRY0oe+Hmdwvyzb4mbIgFyPzU0CRFb+L1/
x+eyrJymBhUL6FVQtoARcYD9g0ya1q3taJQ+JhGW1Ib+DtZzrV4CfDU6q5hWrOOX
d9/CAPM4NsjxuAfsy8nH+IOmcLyOXgfTgNFYVv5REnLVYOEoE630uBxnrOKchtpk
1iBSSGCPVcNioLQdUS3rPxtgkZkthar22xme7RDuUj1cg9p6Gu+6hyJIB7y41NdM
rLdZeHcRlgy56yb6YBXTnilPDCFhtOx6L8cXnL4CVYtg7ityq5khDSMVrtgiF8wQ
n6hDJbSLdFMQMdm9gIQ6lobZkHi4R3yk9S/rHtl7Gc3Set/2rqnxpyt5WsNHcBoy
uNkvGZuP9Pb6n4k7eR0/qX2cg3xycNI/uuxqDTpieHr+/lvOflqcj6+6Fq3Uvg65
8rl5vzsrWArX/3/5sfGG6pqPaCjEHb0FeP8zzxzUTw6J46mzCuG90ERCJ/75wTmT
QD3oCtLtu/nI4MsR8I4VVn26u8FO63xDSk8xPvS6o8wU7EoZXH3+74EFf5beGgt8
cMTS1Zil/MrtFOSC+MypihKCaYYjVr66F3h3I1RBef+bwuwOuQacaQCXkLHOWC3S
pH1iuKGt7lbpGPz103pkc4ssMYAc66nEYXf9I8MATP1aYOyP5o78yegWqgiUs+jd
frdgEsW3fsmeA655+5XZmXLHlmkpbb31KeVfCQXoTbHvExTqK91k73xn7/YRHLKq
vFKsz6cuWFnHmhb9gInH8iNzEM8DEJq+lEEhEi9XjeNmgnzd2vVl+3a2GPoy2h7u
VoGAwr7phI1PiD2aRoB7ZWiR4xxbwl8n+hHh63hSGNYHOeQ7JosPnqcwvHUZo4JZ
CXAI6T9snlZRg2G/BT627LYRGqu8piWl3FJXVaVd8lo6g4ZUrhyuV+48tJy1OvHT
gM1IATYnml6FPLXAqouxDrMKToAw45KOLrevGDDaQ91kxPrgEpK3fcnvH0FgJ16x
/N7uqBmo2XYZM6QxTrq1iShpGFoZ+DC3FOtDT3TKnsrlEUBLzgP3yqJje9Dn+BRs
td8=
-----END ENCRYPTED PRIVATE KEY-----

View File

@ -92,6 +92,13 @@ Feature: Going back and forward.
- url: http://localhost:*/data/backforward/2.txt - url: http://localhost:*/data/backforward/2.txt
- url: http://localhost:*/data/backforward/3.txt - url: http://localhost:*/data/backforward/3.txt
Scenario: Going back too much with count.
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I open data/backforward/3.txt
And I run :back with count 3
Then the error "At beginning of history." should be shown
Scenario: Going back with very big count. Scenario: Going back with very big count.
Given I open data/backforward/1.txt Given I open data/backforward/1.txt
When I run :back with count 99999999999 When I run :back with count 99999999999
@ -132,3 +139,11 @@ Feature: Going back and forward.
Given I open data/backforward/1.txt Given I open data/backforward/1.txt
When I run :forward When I run :forward
Then the error "At end of history." should be shown Then the error "At end of history." should be shown
Scenario: Going forward too much with count.
Given I open data/backforward/1.txt
When I open data/backforward/2.txt
And I open data/backforward/3.txt
And I run :back with count 2
And I run :forward with count 3
Then the error "At end of history." should be shown

View File

@ -3,7 +3,7 @@ Feature: Caret mode
Background: Background:
Given I open data/caret.html Given I open data/caret.html
And I run :enter-mode caret And I run :tab-only ;; :enter-mode caret
# document # document
@ -258,3 +258,63 @@ Feature: Caret mode
Then the message "3 chars yanked to clipboard" should be shown. Then the message "3 chars yanked to clipboard" should be shown.
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-selected
Then the message "Nothing to yank" should be shown.
# :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 -> allow-javascript 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 -> allow-javascript 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)
When I set content -> allow-javascript to true
And I run :tab-only
And I run :enter-mode caret
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :follow-selected --tab
Then data/hello.txt should be loaded
And the following tabs should be open:
- data/caret.html
- data/hello.txt (active)
Scenario: :follow-selected with --tab (without JS)
When I set content -> allow-javascript to false
And I run :tab-only
And I run :enter-mode caret
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :follow-selected --tab
Then data/hello.txt should be loaded
And the following tabs should be open:
- data/caret.html
- data/hello.txt (active)

View File

@ -25,6 +25,7 @@ import json
import os.path import os.path
import logging import logging
import collections import collections
import textwrap
import pytest import pytest
import yaml import yaml
@ -35,6 +36,11 @@ from PyQt5.QtGui import QClipboard
from helpers import utils from helpers import utils
class WaitForClipboardTimeout(Exception):
"""Raised when _wait_for_clipboard didn't get the expected message."""
def _clipboard_mode(qapp, what): def _clipboard_mode(qapp, what):
"""Get the QClipboard::Mode to use based on a string.""" """Get the QClipboard::Mode to use based on a string."""
if what == 'clipboard': if what == 'clipboard':
@ -68,6 +74,7 @@ def open_path_given(quteproc, path):
It always opens a new tab, unlike "When I open ..." It always opens a new tab, unlike "When I open ..."
""" """
quteproc.open_path(path, new_tab=True) quteproc.open_path(path, new_tab=True)
quteproc.wait_for_load_finished(path)
@bdd.given(bdd.parsers.parse("I run {command}")) @bdd.given(bdd.parsers.parse("I run {command}"))
@ -93,17 +100,32 @@ def fresh_instance(quteproc):
def open_path(quteproc, path): def open_path(quteproc, path):
"""Open a URL. """Open a URL.
If used like "When I open ... in a new tab", the URL is opened ina new If used like "When I open ... in a new tab", the URL is opened in a new
tab. tab. With "... in a new window", it's opened in a new window.
""" """
new_tab = False
new_window = False
wait_for_load_finished = True
new_tab_suffix = ' in a new tab' new_tab_suffix = ' in a new tab'
new_window_suffix = ' in a new window'
do_not_wait_suffix = ' without waiting'
if path.endswith(new_tab_suffix): if path.endswith(new_tab_suffix):
path = path[:-len(new_tab_suffix)] path = path[:-len(new_tab_suffix)]
new_tab = True new_tab = True
else: elif path.endswith(new_window_suffix):
new_tab = False path = path[:-len(new_window_suffix)]
new_window = True
quteproc.open_path(path, new_tab=new_tab) if path.endswith(do_not_wait_suffix):
path = path[:-len(do_not_wait_suffix)]
wait_for_load_finished = False
quteproc.open_path(path, new_tab=new_tab, new_window=new_window)
if wait_for_load_finished:
quteproc.wait_for_load_finished(path)
@bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}")) @bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}"))
@ -131,7 +153,7 @@ def run_command(quteproc, httpbin, command):
@bdd.when(bdd.parsers.parse("I reload")) @bdd.when(bdd.parsers.parse("I reload"))
def reload(qtbot, httpbin, quteproc, command): def reload(qtbot, httpbin, quteproc, command):
"""Reload and wait until a new request is received.""" """Reload and wait until a new request is received."""
with qtbot.waitSignal(httpbin.new_request, raising=True): with qtbot.waitSignal(httpbin.new_request):
quteproc.send_cmd(':reload') quteproc.send_cmd(':reload')
@ -142,8 +164,9 @@ def wait_until_loaded(quteproc, path):
@bdd.when(bdd.parsers.re(r'I wait for (?P<is_regex>regex )?"' @bdd.when(bdd.parsers.re(r'I wait for (?P<is_regex>regex )?"'
r'(?P<pattern>[^"]+)" in the log')) r'(?P<pattern>[^"]+)" in the log(?P<do_skip> or skip '
def wait_in_log(quteproc, is_regex, pattern): r'the test)?'))
def wait_in_log(quteproc, is_regex, pattern, do_skip):
"""Wait for a given pattern in the qutebrowser log. """Wait for a given pattern in the qutebrowser log.
If used like "When I wait for regex ... in the log" the argument is treated If used like "When I wait for regex ... in the log" the argument is treated
@ -151,7 +174,9 @@ def wait_in_log(quteproc, is_regex, pattern):
""" """
if is_regex: if is_regex:
pattern = re.compile(pattern) pattern = re.compile(pattern)
quteproc.wait_for(message=pattern)
line = quteproc.wait_for(message=pattern, do_skip=bool(do_skip))
line.expected = True
@bdd.when(bdd.parsers.re(r'I wait for the (?P<category>error|message|warning) ' @bdd.when(bdd.parsers.re(r'I wait for the (?P<category>error|message|warning) '
@ -191,11 +216,33 @@ def fill_clipboard(qtbot, qapp, httpbin, what, content):
clipboard.setText(content, mode) clipboard.setText(content, mode)
@bdd.when(bdd.parsers.re(r'I put the following lines into the '
r'(?P<what>primary selection|clipboard):\n'
r'(?P<content>.+)$', flags=re.DOTALL))
def fill_clipboard_multiline(qtbot, qapp, httpbin, what, content):
fill_clipboard(qtbot, qapp, httpbin, what, textwrap.dedent(content))
## Then ## Then
@bdd.then(bdd.parsers.parse("{path} should be loaded")) @bdd.then(bdd.parsers.parse("{path} should be loaded"))
def path_should_be_loaded(httpbin, path): def path_should_be_loaded(quteproc, path):
"""Make sure the given path was loaded according to the log.
This is usally the better check compared to "should be requested" as the
page could be loaded from local cache.
"""
url = quteproc.path_to_url(path)
pattern = re.compile(
r"load status for <qutebrowser\.browser\.webview\.WebView "
r"tab_id=\d+ url='{url}/?'>: LoadStatus\.success".format(
url=re.escape(url)))
quteproc.wait_for(message=pattern)
@bdd.then(bdd.parsers.parse("{path} should be requested"))
def path_should_be_requested(httpbin, path):
"""Make sure the given path was loaded from the webserver.""" """Make sure the given path was loaded from the webserver."""
httpbin.wait_for(verb='GET', path='/' + path) httpbin.wait_for(verb='GET', path='/' + path)
@ -243,7 +290,8 @@ def should_be_logged(quteproc, is_regex, pattern):
"""Expect the given pattern on regex in the log.""" """Expect the given pattern on regex in the log."""
if is_regex: if is_regex:
pattern = re.compile(pattern) pattern = re.compile(pattern)
quteproc.wait_for(message=pattern) line = quteproc.wait_for(message=pattern)
line.expected = True
@bdd.then(bdd.parsers.parse('"{pattern}" should not be logged')) @bdd.then(bdd.parsers.parse('"{pattern}" should not be logged'))
@ -256,8 +304,7 @@ def ensure_not_logged(quteproc, pattern):
'logged')) 'logged'))
def javascript_message_logged(quteproc, message): def javascript_message_logged(quteproc, message):
"""Make sure the given message was logged via javascript.""" """Make sure the given message was logged via javascript."""
quteproc.wait_for(category='js', function='javaScriptConsoleMessage', quteproc.wait_for_js(message)
message='[*] {}'.format(message))
@bdd.then(bdd.parsers.parse('the javascript message "{message}" should not be ' @bdd.then(bdd.parsers.parse('the javascript message "{message}" should not be '
@ -316,7 +363,24 @@ def check_contents(quteproc, filename):
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..',
'data', os.path.join(*filename.split('/'))) 'data', os.path.join(*filename.split('/')))
with open(path, 'r', encoding='utf-8') as f: with open(path, 'r', encoding='utf-8') as f:
assert content == f.read() file_content = f.read()
assert content == file_content
@bdd.then(bdd.parsers.parse('the page should contain the plaintext "{text}"'))
def check_contents_plain(quteproc, text):
"""Check the current page's content based on a substring."""
content = quteproc.get_content().strip()
assert text in content
@bdd.then(bdd.parsers.parse('the json on the page should be:\n{text}'))
def check_contents_json(quteproc, text):
"""Check the current page's content as json."""
content = quteproc.get_content().strip()
expected = json.loads(text)
actual = json.loads(content)
assert actual == expected
@bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}")) @bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}"))
@ -335,6 +399,7 @@ def check_open_tabs(quteproc, tabs):
for i, line in enumerate(tabs): for i, line in enumerate(tabs):
line = line.strip() line = line.strip()
assert line.startswith('- ')
line = line[2:] # remove "- " prefix line = line[2:] # remove "- " prefix
if line.endswith(active_suffix): if line.endswith(active_suffix):
path = line[:-len(active_suffix)] path = line[:-len(active_suffix)]
@ -359,16 +424,27 @@ def _wait_for_clipboard(qtbot, clipboard, mode, expected):
while True: while True:
if clipboard.text(mode=mode) == expected: if clipboard.text(mode=mode) == expected:
return return
with qtbot.waitSignal(clipboard.changed, timeout=timeout) as blocker:
# We need to poll the clipboard, as for some reason it can change with
# emitting changed (?).
with qtbot.waitSignal(clipboard.changed, timeout=100, raising=False):
pass pass
if not blocker.signal_triggered or timer.hasExpired(timeout):
if timer.hasExpired(timeout):
mode_names = { mode_names = {
QClipboard.Clipboard: 'clipboard', QClipboard.Clipboard: 'clipboard',
QClipboard.Selection: 'primary selection', QClipboard.Selection: 'primary selection',
} }
raise WaitForTimeout( raise WaitForClipboardTimeout(
"Timed out after {}ms waiting for {} in {}.".format( "Timed out after {timeout}ms waiting for {what}:\n"
timeout, expected, mode_names[mode])) " expected: {expected!r}\n"
" clipboard: {clipboard!r}\n"
" primary: {primary!r}.".format(
timeout=timeout, what=mode_names[mode],
expected=expected,
clipboard=clipboard.text(mode=QClipboard.Clipboard),
primary=clipboard.text(mode=QClipboard.Selection))
)
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should ' @bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
@ -382,6 +458,13 @@ def clipboard_contains(qtbot, qapp, httpbin, what, content):
@bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}')) @bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}'))
def clipboard_contains_multiline(qtbot, qapp, content): def clipboard_contains_multiline(qtbot, qapp, content):
expected = '\n'.join(line.strip() for line in content.splitlines()) expected = textwrap.dedent(content)
_wait_for_clipboard(qtbot, qapp.clipboard(), QClipboard.Clipboard, _wait_for_clipboard(qtbot, qapp.clipboard(), QClipboard.Clipboard,
expected) expected)
@bdd.then("qutebrowser should quit")
def should_quit(qtbot, quteproc):
quteproc.exit_expected = True
with qtbot.waitSignal(quteproc.proc.finished, timeout=5000):
pass

View File

@ -117,8 +117,25 @@ Feature: Various utility commands.
And I wait for "Focus object changed: *" in the log And I wait for "Focus object changed: *" in the log
Then no crash should happen Then no crash should happen
# Different code path as an inspector got created now
Scenario: Inspector without developer extras (after smoke)
When I set general -> developer-extras to false
And I run :inspector
Then the error "Please enable developer-extras before using the webinspector!" should be shown
# Different code path as an inspector got created now
@not_xvfb @posix
Scenario: Inspector smoke test 2
When I set general -> developer-extras to true
And I run :inspector
And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log
And I run :inspector
And I wait for "Focus object changed: *" in the log
Then no crash should happen
# :stop/:reload # :stop/:reload
# WORKAROUND for https://bitbucket.org/cherrypy/cherrypy/pull-requests/117/
@not_osx @not_osx
Scenario: :stop Scenario: :stop
Given I have a fresh instance Given I have a fresh instance
@ -135,13 +152,19 @@ Feature: Various utility commands.
custom/redirect-later?delay=-1 custom/redirect-later?delay=-1
# no request on / because we stopped the redirect # no request on / because we stopped the redirect
Scenario: :reload Scenario: :stop with wrong count
When I open data/hello.txt When I open data/hello.txt
And I run :tab-only
And I run :stop with count 2
Then no crash should happen
Scenario: :reload
When I open data/reload.txt
And I run :reload And I run :reload
And I wait until data/hello.txt is loaded And I wait until data/reload.txt is loaded
Then the requests should be: Then the requests should be:
data/hello.txt data/reload.txt
data/hello.txt data/reload.txt
Scenario: :reload with force Scenario: :reload with force
When I open headers When I open headers
@ -149,6 +172,12 @@ Feature: Various utility commands.
And I wait until headers is loaded And I wait until headers is loaded
Then the header Cache-Control should be set to no-cache Then the header Cache-Control should be set to no-cache
Scenario: :reload with wrong count
When I open data/hello.txt
And I run :tab-only
And I run :reload with count 2
Then no crash should happen
# :view-source # :view-source
Scenario: :view-source Scenario: :view-source
@ -260,3 +289,42 @@ Feature: Various utility commands.
And I set storage -> prompt-download-directory to false And I set storage -> prompt-download-directory to false
And I open data/misc/test.pdf And I open data/misc/test.pdf
Then "Download finished" should be logged Then "Download finished" should be logged
# :print
# Disabled because it causes weird segfaults and QPainter warnings in Qt...
@xfail_norun
Scenario: print preview
When I open data/hello.txt
And I run :print --preview
And I wait for "Focus object changed: *" in the log
And I run :debug-pyeval QApplication.instance().activeModalWidget().close()
Then no crash should happen
# On Windows/OS X, we get a "QPrintDialog: Cannot be used on non-native
# printers" qWarning.
#
# Disabled because it causes weird segfaults and QPainter warnings in Qt...
@xfail_norun
Scenario: print
When I open data/hello.txt
And I run :print
And I wait for "Focus object changed: *" in the log or skip the test
And I run :debug-pyeval QApplication.instance().activeModalWidget().close()
Then no crash should happen
# :pyeval
Scenario: Running :pyeval
When I run :debug-pyeval 1+1
And I wait until qute:pyeval is loaded
Then the page should contain the plaintext "2"
Scenario: Causing exception in :pyeval
When I run :debug-pyeval 1/0
And I wait until qute:pyeval is loaded
Then the page should contain the plaintext "ZeroDivisionError"
Scenario: Running :pyeval with --quiet
When I run :debug-pyeval --quiet 1+1
Then "pyeval output: 2" should be logged

View File

@ -0,0 +1,195 @@
Feature: Prompts
Various prompts (javascript, SSL errors, authentification, etc.)
Background:
Given I set general -> log-javascript-console to debug
# Javascript
Scenario: Javascript alert
When I open data/prompt/jsalert.html
And I click the button
And I wait for a prompt
And I run :prompt-accept
Then the javascript message "Alert done" should be logged
Scenario: Using content -> ignore-javascript-alert
When I set content -> ignore-javascript-alert to true
And I open data/prompt/jsalert.html
And I click the button
Then the javascript message "Alert done" should be logged
Scenario: Javascript confirm - yes
When I open data/prompt/jsconfirm.html
And I click the button
And I wait for a prompt
And I run :prompt-yes
Then the javascript message "confirm reply: true" should be logged
Scenario: Javascript confirm - no
When I open data/prompt/jsconfirm.html
And I click the button
And I wait for a prompt
And I run :prompt-no
Then the javascript message "confirm reply: false" should be logged
Scenario: Javascript confirm - aborted
When I open data/prompt/jsconfirm.html
And I click the button
And I wait for a prompt
And I run :leave-mode
Then the javascript message "confirm reply: false" should be logged
@pyqt531_or_newer
Scenario: Javascript prompt
When I open data/prompt/jsprompt.html
And I click the button
And I wait for a prompt
And I press the keys "prompt test"
And I run :prompt-accept
Then the javascript message "Prompt reply: prompt test" should be logged
@pyqt531_or_newer
Scenario: Rejected javascript prompt
When I open data/prompt/jsprompt.html
And I click the button
And I wait for a prompt
And I press the keys "prompt test"
And I run :leave-mode
Then the javascript message "Prompt reply: null" should be logged
@pyqt531_or_newer
Scenario: Using content -> ignore-javascript-prompt
When I set content -> ignore-javascript-prompt to true
And I open data/prompt/jsprompt.html
And I click the button
Then the javascript message "Prompt reply: null" should be logged
# SSL
Scenario: SSL error with ssl-strict = false
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to false
And I load a SSL page
And I wait until the SSL page finished loading
Then the error "SSL error: *" should be shown
And the page should contain the plaintext "Hello World via SSL!"
Scenario: SSL error with ssl-strict = true
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to true
And I load a SSL page
Then "Error while loading *: SSL handshake failed" should be logged
And the page should contain the plaintext "Unable to load page"
Scenario: SSL error with ssl-strict = ask -> yes
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
And I load a SSL page
And I wait for a prompt
And I run :prompt-yes
And I wait until the SSL page finished loading
Then the page should contain the plaintext "Hello World via SSL!"
Scenario: SSL error with ssl-strict = ask -> no
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
And I load a SSL page
And I wait for a prompt
And I run :prompt-no
Then "Error while loading *: SSL handshake failed" should be logged
And the page should contain the plaintext "Unable to load page"
# Geolocation
Scenario: Always rejecting geolocation
When I set content -> geolocation to false
And I open data/prompt/geolocation.html in a new tab
And I click the button
Then the javascript message "geolocation permission denied" should be logged
Scenario: Always accepting geolocation
When I set content -> geolocation to true
And I open data/prompt/geolocation.html in a new tab
And I click the button
Then the javascript message "geolocation permission denied" should not be logged
Scenario: geolocation with ask -> true
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
And I click the button
And I wait for a prompt
And I run :prompt-yes
Then the javascript message "geolocation permission denied" should not be logged
Scenario: geolocation with ask -> false
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
And I click the button
And I wait for a prompt
And I run :prompt-no
Then the javascript message "geolocation permission denied" should be logged
Scenario: geolocation with ask -> abort
When I set content -> geolocation to ask
And I open data/prompt/geolocation.html in a new tab
And I click the button
And I wait for a prompt
And I run :leave-mode
Then the javascript message "geolocation permission denied" should be logged
# Notifications
Scenario: Always rejecting notifications
When I set content -> notifications to false
And I open data/prompt/notifications.html in a new tab
And I click the button
Then the javascript message "notification permission denied" should be logged
Scenario: Always accepting notifications
When I set content -> notifications to true
And I open data/prompt/notifications.html in a new tab
And I click the button
Then the javascript message "notification permission granted" should be logged
Scenario: notifications with ask -> false
When I set content -> notifications to ask
And I open data/prompt/notifications.html in a new tab
And I click the button
And I wait for a prompt
And I run :prompt-no
Then the javascript message "notification permission denied" should be logged
Scenario: notifications with ask -> true
When I set content -> notifications to ask
And I open data/prompt/notifications.html in a new tab
And I click the button
And I wait for a prompt
And I run :prompt-yes
Then the javascript message "notification permission granted" should be logged
# This actually gives us a denied rather than an aborted
@xfail_norun
Scenario: notifications with ask -> abort
When I set content -> notifications to ask
And I open data/prompt/notifications.html in a new tab
And I click the button
And I wait for a prompt
And I run :leave-mode
Then the javascript message "notification permission aborted" should be logged
# Page authentication
Scenario: Successful webpage authentification
When I open basic-auth/user/password without waiting
And I wait for a prompt
And I press the keys "user"
And I run :prompt-accept
And I press the keys "password"
And I run :prompt-accept
And I wait until basic-auth/user/password is loaded
Then the json on the page should be:
{
"authenticated": true,
"user": "user"
}

View File

@ -0,0 +1,187 @@
Feature: Searching on a page
Searching text on the page (like /foo) with different options.
Background:
Given I open data/search.html
And I run :tab-only
## searching
Scenario: Searching text
When I run :search foo
And I run :yank-selected
Then the clipboard should contain "foo"
Scenario: Searching twice
When I run :search foo
And I run :search bar
And I run :yank-selected
Then the clipboard should contain "Bar"
Scenario: Searching with --reverse
When I set general -> ignore-case to true
And I run :search -r foo
And I run :yank-selected
Then the clipboard should contain "Foo"
Scenario: Searching without matches
When I run :search doesnotmatch
Then the warning "Text 'doesnotmatch' not found on page!" should be shown
@xfail_norun
Scenario: Searching with / and spaces at the end (issue 874)
When I run :set-cmd-text -s /space
And I run :command-accept
And I run :yank-selected
Then the clipboard should contain "space "
Scenario: Searching with / and slash in search term (issue 507)
When I run :set-cmd-text -s //slash
And I run :command-accept
And I run :yank-selected
Then the clipboard should contain "/slash"
# This doesn't work because this is QtWebKit behaviour.
@xfail_norun
Scenario: Searching text with umlauts
When I run :search blub
Then the warning "Text 'blub' not found on page!" should be shown
## ignore-case
Scenario: Searching text with ignore-case = true
When I set general -> ignore-case to true
And I run :search bar
And I run :yank-selected
Then the clipboard should contain "Bar"
Scenario: Searching text with ignore-case = false
When I set general -> ignore-case to false
And I run :search bar
And I run :yank-selected
Then the clipboard should contain "bar"
Scenario: Searching text with ignore-case = smart (lower-case)
When I set general -> ignore-case to smart
And I run :search bar
And I run :yank-selected
Then the clipboard should contain "Bar"
Scenario: Searching text with ignore-case = smart (upper-case)
When I set general -> ignore-case to smart
And I run :search Foo
And I run :yank-selected
Then the clipboard should contain "Foo" # even though foo was first
## :search-next
Scenario: Jumping to next match
When I set general -> ignore-case to true
And I run :search foo
And I run :search-next
And I run :yank-selected
Then the clipboard should contain "Foo"
Scenario: Jumping to next match with count
When I set general -> ignore-case to true
And I run :search baz
And I run :search-next with count 2
And I run :yank-selected
Then the clipboard should contain "BAZ"
Scenario: Jumping to next match with --reverse
When I set general -> ignore-case to true
And I run :search --reverse foo
And I run :search-next
And I run :yank-selected
Then the clipboard should contain "foo"
Scenario: Jumping to next match without search
# Make sure there was no search in the same window before
When I open data/search.html in a new window
And I run :search-next
Then no crash should happen
Scenario: Repeating search in a second tab (issue #940)
When I open data/search.html in a new tab
And I run :search foo
And I run :tab-prev
And I run :search-next
And I run :yank-selected
Then the clipboard should contain "foo"
## :search-prev
Scenario: Jumping to previous match
When I set general -> ignore-case to true
And I run :search foo
And I run :search-next
And I run :search-prev
And I run :yank-selected
Then the clipboard should contain "foo"
Scenario: Jumping to previous match with count
When I set general -> ignore-case to true
And I run :search baz
And I run :search-next
And I run :search-next
And I run :search-prev with count 2
And I run :yank-selected
Then the clipboard should contain "baz"
Scenario: Jumping to previous match with --reverse
When I set general -> ignore-case to true
And I run :search --reverse foo
And I run :search-next
And I run :search-prev
And I run :yank-selected
Then the clipboard should contain "Foo"
Scenario: Jumping to previous match without search
# Make sure there was no search in the same window before
When I open data/search.html in a new window
And I run :search-prev
Then no crash should happen
## wrapping
Scenario: Wrapping around page
When I set general -> wrap-search to true
And I run :search foo
And I run :search-next
And I run :search-next
And I run :yank-selected
Then the clipboard should contain "foo"
Scenario: Wrapping around page with wrap-search = false
When I set general -> wrap-search to false
And I run :search foo
And I run :search-next
And I run :search-next
Then the warning "Search hit BOTTOM without match for: foo" should be shown
Scenario: Wrapping around page with --reverse
When I set general -> wrap-search to true
And I run :search --reverse foo
And I run :search-next
And I run :search-next
And I run :yank-selected
Then the clipboard should contain "Foo"
Scenario: Wrapping around page with wrap-search = false and --reverse
When I set general -> wrap-search to false
And I run :search --reverse foo
And I run :search-next
And I run :search-next
Then the warning "Search hit TOP without match for: foo" should be shown
Scenario: Wrapping around page
When I set general -> wrap-search to true
And I run :search foo
And I run :search-next
And I run :search-next
And I run :yank-selected
Then the clipboard should contain "foo"
# TODO: wrapping message with scrolling
# TODO: wrapping message without scrolling

View File

@ -533,3 +533,105 @@ Feature: Tab management
- tabs: - tabs:
- history: - history:
- url: http://localhost:*/data/numbers/2.txt - url: http://localhost:*/data/numbers/2.txt
# :undo
Scenario: Undo without any closed tabs
Given I have a fresh instance
When I run :undo
Then the error "Nothing to undo!" should be shown
Scenario: Undo closing a tab
When I open data/numbers/1.txt
And I run :tab-only
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt
And I run :tab-close
And I run :undo
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- url: http://localhost:*/data/numbers/1.txt
- active: true
history:
- url: http://localhost:*/data/numbers/2.txt
- url: http://localhost:*/data/numbers/3.txt
Scenario: Undo with auto-created last tab
When I open data/hello.txt
And I run :tab-only
And I set tabs -> last-close to blank
And I run :tab-close
And I run :undo
Then the following tabs should be open:
- data/hello.txt (active)
Scenario: Undo with auto-created last tab, with history
When I open data/hello.txt
And I open data/hello2.txt
And I run :tab-only
And I set tabs -> last-close to blank
And I run :tab-close
And I run :undo
Then the following tabs should be open:
- data/hello2.txt (active)
Scenario: Undo with auto-created last tab (startpage)
When I open data/hello.txt
And I run :tab-only
And I set tabs -> last-close to startpage
And I set general -> startpage to http://localhost:(port)/data/numbers/4.txt,http://localhost:(port)/data/numbers/5.txt
And I run :tab-close
And I run :undo
Then the following tabs should be open:
- data/hello.txt (active)
Scenario: Undo with auto-created last tab (default-page)
When I open data/hello.txt
And I run :tab-only
And I set tabs -> last-close to default-page
And I set general -> default-page to http://localhost:(port)/data/numbers/6.txt
And I run :tab-close
And I run :undo
Then the following tabs should be open:
- data/hello.txt (active)
# last-close
Scenario: last-close = blank
When I open data/hello.txt
And I set tabs -> last-close to blank
And I run :tab-only
And I run :tab-close
And I wait until about:blank is loaded
Then the following tabs should be open:
- about:blank (active)
Scenario: last-close = startpage
When I set general -> startpage to http://localhost:(port)/data/numbers/7.txt,http://localhost:(port)/data/numbers/8.txt
And I set tabs -> last-close to startpage
And I open data/hello.txt
And I run :tab-only
And I run :tab-close
And I wait until data/numbers/7.txt is loaded
Then the following tabs should be open:
- data/numbers/7.txt (active)
Scenario: last-close = default-page
When I set general -> default-page to http://localhost:(port)/data/numbers/9.txt
And I set tabs -> last-close to default-page
And I open data/hello.txt
And I run :tab-only
And I run :tab-close
And I wait until data/numbers/9.txt is loaded
Then the following tabs should be open:
- data/numbers/9.txt (active)
Scenario: last-close = close
When I open data/hello.txt
And I set tabs -> last-close to close
And I run :tab-only
And I run :tab-close
Then qutebrowser should quit

View File

@ -0,0 +1,50 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 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/>.
import pytest_bdd as bdd
bdd.scenarios('prompts.feature')
@bdd.when("I load a SSL page")
def load_ssl_page(quteproc, ssl_server):
quteproc.open_path('/', port=ssl_server.port, https=True)
# We don't call wait_for_load_finished here as we can get an SSL question.
@bdd.when("I wait until the SSL page finished loading")
def wait_ssl_page_finished_loading(quteproc, ssl_server):
quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True,
load_status='warn')
@bdd.when("I click the button")
def click_button(quteproc):
quteproc.send_cmd(':hint')
quteproc.send_cmd(':follow-hint a')
@bdd.when("I wait for a prompt")
def wait_for_prompt(quteproc):
quteproc.wait_for(message='Entering mode KeyMode.* (reason: question '
'asked)')
@bdd.then("no prompt should be shown")
def no_prompt_shown(quteproc):
quteproc.ensure_not_logged(message='Entering mode KeyMode.* (reason: '
'question asked)')

View File

@ -0,0 +1,26 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 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/>.
import pytest_bdd as bdd
# pylint: disable=unused-import
from test_yankpaste import skip_with_broken_clipboard
bdd.scenarios('search.feature')

View File

@ -26,20 +26,49 @@ from helpers import utils
bdd.scenarios('urlmarks.feature') bdd.scenarios('urlmarks.feature')
@bdd.then(bdd.parsers.parse('the bookmark file should contain "{expected}"')) def _check_marks(quteproc, quickmarks, expected, contains):
def bookmark_file_contains(quteproc, expected): """Make sure the given line does (not) exist in the bookmarks.
bookmark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks',
Args:
quickmarks: True to check the quickmarks file instead of bookmarks.
expected: The line to search for.
contains: True if the line should be there, False otherwise.
"""
if quickmarks:
mark_file = os.path.join(quteproc.basedir, 'config', 'quickmarks')
else:
mark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks',
'urls') 'urls')
quteproc.clear_data() # So we don't match old messages quteproc.clear_data() # So we don't match old messages
quteproc.send_cmd(':save') quteproc.send_cmd(':save')
quteproc.wait_for(message='Saved to {}'.format(bookmark_file)) quteproc.wait_for(message='Saved to {}'.format(mark_file))
with open(bookmark_file, 'r', encoding='utf-8') as f: with open(mark_file, 'r', encoding='utf-8') as f:
lines = f.readlines() lines = f.readlines()
matched_line = any( matched_line = any(
utils.pattern_match(pattern=expected, value=line.rstrip('\n')) utils.pattern_match(pattern=expected, value=line.rstrip('\n'))
for line in lines) for line in lines)
assert matched_line, lines assert matched_line == contains, lines
@bdd.then(bdd.parsers.parse('the bookmark file should contain "{line}"'))
def bookmark_file_contains(quteproc, line):
_check_marks(quteproc, quickmarks=False, expected=line, contains=True)
@bdd.then(bdd.parsers.parse('the bookmark file should not contain "{line}"'))
def bookmark_file_does_not_contain(quteproc, line):
_check_marks(quteproc, quickmarks=False, expected=line, contains=False)
@bdd.then(bdd.parsers.parse('the quickmark file should contain "{line}"'))
def quickmark_file_contains(quteproc, line):
_check_marks(quteproc, quickmarks=True, expected=line, contains=True)
@bdd.then(bdd.parsers.parse('the quickmark file should not contain "{line}"'))
def quickmark_file_does_not_contain(quteproc, line):
_check_marks(quteproc, quickmarks=True, expected=line, contains=False)

View File

@ -33,7 +33,7 @@ def skip_with_broken_clipboard(qtbot, qapp):
""" """
clipboard = qapp.clipboard() clipboard = qapp.clipboard()
with qtbot.waitSignal(clipboard.changed): with qtbot.waitSignal(clipboard.changed, raising=False):
clipboard.setText("Does this work?") clipboard.setText("Does this work?")
if clipboard.text() != "Does this work?": if clipboard.text() != "Does this work?":

View File

@ -1,7 +1,174 @@
Feature: quickmarks and bookmarks Feature: quickmarks and bookmarks
## bookmarks
Scenario: Saving a bookmark Scenario: Saving a bookmark
When I open data/title.html When I open data/title.html
And I run :bookmark-add And I run :bookmark-add
Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown
And the bookmark file should contain "http://localhost:*/data/title.html Test title" And the bookmark file should contain "http://localhost:*/data/title.html Test title"
Scenario: Saving a duplicate bookmark
Given I have a fresh instance
When I open data/title.html
And I run :bookmark-add
And I run :bookmark-add
Then the error "Bookmark already exists!" should be shown
Scenario: Loading a bookmark
When I run :tab-only
And I run :bookmark-load http://localhost:(port)/data/numbers/1.txt
Then data/numbers/1.txt should be loaded
And the following tabs should be open:
- data/numbers/1.txt (active)
Scenario: Loading a bookmark in a new tab
Given I open about:blank
When I run :tab-only
And I run :bookmark-load -t http://localhost:(port)/data/numbers/2.txt
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
- about:blank
- data/numbers/2.txt (active)
Scenario: Loading a bookmark in a background tab
Given I open about:blank
When I run :tab-only
And I run :bookmark-load -b http://localhost:(port)/data/numbers/3.txt
Then data/numbers/3.txt should be loaded
And the following tabs should be open:
- about:blank (active)
- data/numbers/3.txt
Scenario: Loading a bookmark in a new window
Given I open about:blank
When I run :tab-only
And I run :bookmark-load -w http://localhost:(port)/data/numbers/4.txt
And I wait until data/numbers/4.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: about:blank
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/numbers/4.txt
Scenario: Loading a bookmark with -t and -b
When I run :bookmark-load -t -b about:blank
Then the error "Only one of -t/-b/-w can be given!" should be shown
Scenario: Deleting a bookmark which does not exist
When I run :bookmark-del doesnotexist
Then the error "Bookmark 'doesnotexist' not found!" should be shown
Scenario: Deleting a bookmark
When I open data/numbers/5.txt
And I run :bookmark-add
And I run :bookmark-del http://localhost:(port)/data/numbers/5.txt
Then the bookmark file should not contain "http://localhost:*/data/numbers/5.txt "
## quickmarks
Scenario: Saving a quickmark (:quickmark-add)
When I run :quickmark-add http://localhost:(port)/data/numbers/6.txt six
Then the quickmark file should contain "six http://localhost:*/data/numbers/6.txt"
Scenario: Saving a quickmark (:quickmark-save)
When I open http://localhost:(port)/data/numbers/7.txt
And I run :quickmark-save
And I wait for "Entering mode KeyMode.prompt (reason: question asked)" in the log
And I press the keys "seven"
And I press the keys "<Enter>"
Then the quickmark file should contain "seven http://localhost:*/data/numbers/7.txt"
Scenario: Saving a duplicate quickmark (without override)
When I run :quickmark-add http://localhost:(port)/data/numbers/8.txt eight
And I run :quickmark-add http://localhost:(port)/data/numbers/8_2.txt eight
And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log
And I run :prompt-no
Then the quickmark file should contain "eight http://localhost:*/data/numbers/8.txt"
Scenario: Saving a duplicate quickmark (with override)
When I run :quickmark-add http://localhost:(port)/data/numbers/9.txt nine
And I run :quickmark-add http://localhost:(port)/data/numbers/9_2.txt nine
And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log
And I run :prompt-yes
Then the quickmark file should contain "nine http://localhost:*/data/numbers/9_2.txt"
Scenario: Adding a quickmark with an empty name
When I run :quickmark-add about:blank ""
Then the error "Can't set mark with empty name!" should be shown
Scenario: Adding a quickmark with an empty URL
When I run :quickmark-add "" foo
Then the error "Can't set mark with empty URL!" should be shown
Scenario: Loading a quickmark
Given I have a fresh instance
When I run :quickmark-add http://localhost:(port)/data/numbers/10.txt ten
And I run :quickmark-load ten
Then data/numbers/10.txt should be loaded
And the following tabs should be open:
- data/numbers/10.txt (active)
Scenario: Loading a quickmark in a new tab
Given I open about:blank
When I run :tab-only
And I run :quickmark-add http://localhost:(port)/data/numbers/11.txt eleven
And I run :quickmark-load -t eleven
Then data/numbers/11.txt should be loaded
And the following tabs should be open:
- about:blank
- data/numbers/11.txt (active)
Scenario: Loading a quickmark in a background tab
Given I open about:blank
When I run :tab-only
And I run :quickmark-add http://localhost:(port)/data/numbers/12.txt twelve
And I run :quickmark-load -b twelve
Then data/numbers/12.txt should be loaded
And the following tabs should be open:
- about:blank (active)
- data/numbers/12.txt
Scenario: Loading a quickmark in a new window
Given I open about:blank
When I run :tab-only
And I run :quickmark-add http://localhost:(port)/data/numbers/13.txt thirteen
And I run :quickmark-load -w thirteen
And I wait until data/numbers/13.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: about:blank
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/numbers/13.txt
Scenario: Loading a quickmark which does not exist
When I run :quickmark-load -b doesnotexist
Then the error "Quickmark 'doesnotexist' does not exist!" should be shown
Scenario: Loading a quickmark with -t and -b
When I run :quickmark-add http://localhost:(port)/data/numbers/14.txt fourteen
When I run :quickmark-load -t -b fourteen
Then the error "Only one of -t/-b/-w can be given!" should be shown
Scenario: Deleting a quickmark which does not exist
When I run :quickmark-del doesnotexist
Then the error "Quickmark 'doesnotexist' not found!" should be shown
Scenario: Deleting a quickmark
When I run :quickmark-add http://localhost:(port)/data/numbers/15.txt fifteen
And I run :quickmark-del fifteen
Then the quickmark file should not contain "fourteen http://localhost:*/data/numbers/15.txt "

View File

@ -98,3 +98,75 @@ Feature: Yanking and pasting.
history: history:
- active: true - active: true
url: http://localhost:*/data/hello.txt url: http://localhost:*/data/hello.txt
Scenario: Pasting an invalid URL
When I set general -> auto-search to false
And I put "foo bar" into the clipboard
And I run :paste
Then the error "Invalid URL" should be shown
Scenario: Pasting multiple urls in a new tab
Given I have a fresh instance
When I put the following lines into the clipboard:
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -t
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
Then the following tabs should be open:
- about:blank
- data/hello.txt (active)
- data/hello2.txt
- data/hello3.txt
Scenario: Pasting multiple urls in a background tab
Given I open about:blank
When I run :tab-only
And I put the following lines into the clipboard:
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -b
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
Then the following tabs should be open:
- about:blank (active)
- data/hello.txt
- data/hello2.txt
- data/hello3.txt
Scenario: Pasting multiple urls in new windows
Given I have a fresh instance
When I put the following lines into the clipboard:
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -w
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
Then the session should look like:
windows:
- tabs:
- active: true
history:
- active: true
url: about:blank
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/hello.txt
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/hello2.txt
- tabs:
- active: true
history:
- active: true
url: http://localhost:*/data/hello3.txt

View File

@ -55,3 +55,7 @@ Feature: Zooming in and out
Scenario: Setting zoom with very big count Scenario: Setting zoom with very big count
When I run :zoom with count 99999999999 When I run :zoom with count 99999999999
Then the message "Zoom level: 99999999999%" should be shown Then the message "Zoom level: 99999999999%" should be shown
Scenario: Setting zoom with argument and count
When I run :zoom 50 with count 60
Then the error "Both count and argument given!" should be shown

View File

@ -19,6 +19,7 @@
"""Fixtures to run qutebrowser in a QProcess and communicate.""" """Fixtures to run qutebrowser in a QProcess and communicate."""
import os
import re import re
import sys import sys
import time import time
@ -35,6 +36,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl
import testprocess import testprocess
from qutebrowser.misc import ipc from qutebrowser.misc import ipc
from qutebrowser.utils import log, utils from qutebrowser.utils import log, utils
from helpers import utils as testutils
def is_ignored_qt_message(message): def is_ignored_qt_message(message):
@ -169,12 +171,7 @@ class QuteProc(testprocess.Process):
else: else:
raise raise
# WORKAROUND for https://bitbucket.org/logilab/pylint/issues/717/ self._log(line)
# we should switch to generated-members after that
# pylint: disable=no-member
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
pytest.config.getoption('--verbose')):
self._log(line)
start_okay_message_load = ( start_okay_message_load = (
"load status for <qutebrowser.browser.webview.WebView tab_id=0 " "load status for <qutebrowser.browser.webview.WebView tab_id=0 "
@ -197,7 +194,7 @@ class QuteProc(testprocess.Process):
log_line.function == 'init' and log_line.function == 'init' and
log_line.message.startswith('Base directory:')): log_line.message.startswith('Base directory:')):
self.basedir = log_line.message.split(':', maxsplit=1)[1].strip() self.basedir = log_line.message.split(':', maxsplit=1)[1].strip()
elif log_line.loglevel > logging.INFO: elif self._is_error_logline(log_line):
self.got_error.emit() self.got_error.emit()
return log_line return log_line
@ -214,7 +211,7 @@ class QuteProc(testprocess.Process):
'about:blank'] 'about:blank']
return executable, args return executable, args
def path_to_url(self, path): def path_to_url(self, path, *, port=None, https=False):
"""Get a URL based on a filename for the localhost webserver. """Get a URL based on a filename for the localhost webserver.
URLs like about:... and qute:... are handled specially and returned URLs like about:... and qute:... are handled specially and returned
@ -223,18 +220,53 @@ class QuteProc(testprocess.Process):
if path.startswith('about:') or path.startswith('qute:'): if path.startswith('about:') or path.startswith('qute:'):
return path return path
else: else:
return 'http://localhost:{}/{}'.format( return '{}://localhost:{}/{}'.format(
self._httpbin.port, 'https' if https else 'http',
self._httpbin.port if port is None else port,
path if path != '/' else '') path if path != '/' else '')
def wait_for_js(self, message):
"""Wait for the given javascript console message."""
self.wait_for(category='js', function='javaScriptConsoleMessage',
message='[*] {}'.format(message))
def _is_error_logline(self, msg):
"""Check if the given LogLine is some kind of error message."""
is_js_error = (msg.category == 'js' and
msg.function == 'javaScriptConsoleMessage' and
testutils.pattern_match(pattern='[*] [FAIL] *',
value=msg.message))
return msg.loglevel > logging.INFO or is_js_error
def _maybe_skip(self):
"""Skip the test if [SKIP] lines were logged."""
skip_texts = []
for msg in self._data:
if (msg.category == 'js' and
msg.function == 'javaScriptConsoleMessage' and
testutils.pattern_match(pattern='[*] [SKIP] *',
value=msg.message)):
skip_texts.append(msg.message.partition(' [SKIP] ')[2])
if skip_texts:
pytest.skip(', '.join(skip_texts))
def after_test(self): def after_test(self):
bad_msgs = [msg for msg in self._data bad_msgs = [msg for msg in self._data
if msg.loglevel > logging.INFO and not msg.expected] if self._is_error_logline(msg) and not msg.expected]
super().after_test()
if bad_msgs: try:
text = 'Logged unexpected errors:\n\n' + '\n'.join( if bad_msgs:
str(e) for e in bad_msgs) text = 'Logged unexpected errors:\n\n' + '\n'.join(
pytest.fail(text, pytrace=False) str(e) for e in bad_msgs)
# We'd like to use pytrace=False here but don't as a WORKAROUND
# for https://github.com/pytest-dev/pytest/issues/1316
pytest.fail(text)
else:
self._maybe_skip()
finally:
super().after_test()
def send_cmd(self, command, count=None): def send_cmd(self, command, count=None):
"""Send a command to the running qutebrowser instance.""" """Send a command to the running qutebrowser instance."""
@ -269,14 +301,19 @@ class QuteProc(testprocess.Process):
yield yield
self.set_setting(sect, opt, old_value) self.set_setting(sect, opt, old_value)
def open_path(self, path, new_tab=False): def open_path(self, path, *, new_tab=False, new_window=False, port=None,
https=False):
"""Open the given path on the local webserver in qutebrowser.""" """Open the given path on the local webserver in qutebrowser."""
url = self.path_to_url(path) if new_tab and new_window:
raise ValueError("new_tab and new_window given!")
url = self.path_to_url(path, port=port, https=https)
if new_tab: if new_tab:
self.send_cmd(':open -t ' + url) self.send_cmd(':open -t ' + url)
elif new_window:
self.send_cmd(':open -w ' + url)
else: else:
self.send_cmd(':open ' + url) self.send_cmd(':open ' + url)
self.wait_for_load_finished(path)
def mark_expected(self, category=None, loglevel=None, message=None): def mark_expected(self, category=None, loglevel=None, message=None):
"""Mark a given logging message as expected.""" """Mark a given logging message as expected."""
@ -284,16 +321,24 @@ class QuteProc(testprocess.Process):
message=message) message=message)
line.expected = True line.expected = True
def wait_for_load_finished(self, path, timeout=15000): def wait_for_load_finished(self, path, *, port=None, https=False,
timeout=None, load_status='success'):
"""Wait until any tab has finished loading.""" """Wait until any tab has finished loading."""
url = self.path_to_url(path) if timeout is None:
if 'CI' in os.environ:
timeout = 15000
else:
timeout = 5000
url = self.path_to_url(path, port=port, https=https)
# We really need the same representation that the webview uses in its # We really need the same representation that the webview uses in its
# __repr__ # __repr__
url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100) url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100)
pattern = re.compile( pattern = re.compile(
r"(load status for <qutebrowser.browser.webview.WebView " r"(load status for <qutebrowser\.browser\.webview\.WebView "
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: " r"tab_id=\d+ url='{url}'>: LoadStatus\.{load_status}|fetch: "
r"PyQt5.QtCore.QUrl\('{url}'\) -> .*)".format(url=re.escape(url))) r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format(
load_status=re.escape(load_status), url=re.escape(url)))
self.wait_for(message=pattern, timeout=timeout) self.wait_for(message=pattern, timeout=timeout)
def get_session(self): def get_session(self):

View File

@ -87,7 +87,10 @@ def test_mhtml(test_name, download_dir, quteproc, httpbin):
'data', 'downloads', 'mhtml', test_name) 'data', 'downloads', 'mhtml', test_name)
test_path = 'data/downloads/mhtml/{}'.format(test_name) test_path = 'data/downloads/mhtml/{}'.format(test_name)
quteproc.open_path('{}/{}.html'.format(test_path, test_name)) url_path = '{}/{}.html'.format(test_path, test_name)
quteproc.open_path(url_path)
quteproc.wait_for_load_finished(url_path)
download_dest = os.path.join(download_dir.location, download_dest = os.path.join(download_dir.location,
'{}-downloaded.mht'.format(test_name)) '{}-downloaded.mht'.format(test_name))

View File

@ -29,22 +29,54 @@ import testprocess
from qutebrowser.utils import log from qutebrowser.utils import log
def test_quteproc_error_message(qtbot, quteproc): @pytest.mark.parametrize('cmd', [
':message-error test',
':jseval console.log("[FAIL] test");'
])
def test_quteproc_error_message(qtbot, quteproc, cmd):
"""Make sure the test fails with an unexpected error message.""" """Make sure the test fails with an unexpected error message."""
with qtbot.waitSignal(quteproc.got_error, raising=True): with qtbot.waitSignal(quteproc.got_error):
quteproc.send_cmd(':message-error test') quteproc.send_cmd(cmd)
# Usually we wouldn't call this from inside a test, but here we force the # Usually we wouldn't call this from inside a test, but here we force the
# error to occur during the test rather than at teardown time. # error to occur during the test rather than at teardown time.
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
quteproc.after_test() quteproc.after_test()
def test_quteproc_skip_via_js(qtbot, quteproc):
with pytest.raises(pytest.skip.Exception) as excinfo:
quteproc.send_cmd(':jseval console.log("[SKIP] test");')
quteproc.wait_for_js('[SKIP] test')
# Usually we wouldn't call this from inside a test, but here we force
# the error to occur during the test rather than at teardown time.
quteproc.after_test()
assert str(excinfo.value) == 'test'
def test_quteproc_skip_and_wait_for(qtbot, quteproc):
"""This test will skip *again* during teardown, but we don't care."""
with pytest.raises(pytest.skip.Exception):
quteproc.send_cmd(':jseval console.log("[SKIP] foo");')
quteproc.wait_for_js("[SKIP] foo")
quteproc.wait_for(message='This will not match')
def test_qt_log_ignore(qtbot, quteproc): def test_qt_log_ignore(qtbot, quteproc):
"""Make sure the test passes when logging a qt_log_ignore message.""" """Make sure the test passes when logging a qt_log_ignore message."""
with qtbot.waitSignal(quteproc.got_error, raising=True): with qtbot.waitSignal(quteproc.got_error):
quteproc.send_cmd(':message-error "SpellCheck: test"') quteproc.send_cmd(':message-error "SpellCheck: test"')
def test_quteprocess_quitting(qtbot, quteproc_process):
"""When qutebrowser quits, after_test should fail."""
with qtbot.waitSignal(quteproc_process.proc.finished, timeout=5000):
quteproc_process.send_cmd(':quit')
with pytest.raises(testprocess.ProcessExited):
quteproc_process.after_test()
@pytest.mark.parametrize('data, attrs', [ @pytest.mark.parametrize('data, attrs', [
( (
# Normal message # Normal message

View File

@ -74,6 +74,29 @@ class PythonProcess(testprocess.Process):
return (sys.executable, ['-c', ';'.join(code)]) return (sys.executable, ['-c', ';'.join(code)])
class QuitPythonProcess(testprocess.Process):
"""A testprocess which quits immediately."""
def __init__(self):
super().__init__()
self.proc.setReadChannel(QProcess.StandardOutput)
def _parse_line(self, line):
print("LINE: {}".format(line))
if line.strip() == 'ready':
self.ready.emit()
return testprocess.Line(line)
def _executable_args(self):
code = [
'import sys',
'print("ready")',
'sys.exit(0)',
]
return (sys.executable, ['-c', ';'.join(code)])
@pytest.yield_fixture @pytest.yield_fixture
def pyproc(): def pyproc():
proc = PythonProcess() proc = PythonProcess()
@ -81,6 +104,35 @@ def pyproc():
proc.terminate() proc.terminate()
@pytest.yield_fixture
def quit_pyproc():
proc = QuitPythonProcess()
yield proc
proc.terminate()
def test_quitting_process(qtbot, quit_pyproc):
with qtbot.waitSignal(quit_pyproc.proc.finished):
quit_pyproc.start()
with pytest.raises(testprocess.ProcessExited):
quit_pyproc.after_test()
def test_quitting_process_expected(qtbot, quit_pyproc):
quit_pyproc.exit_expected = True
with qtbot.waitSignal(quit_pyproc.proc.finished):
quit_pyproc.start()
quit_pyproc.after_test()
def test_wait_signal_raising(qtbot):
"""testprocess._wait_signal should raise by default."""
proc = testprocess.Process()
with pytest.raises(qtbot.SignalTimeoutError):
with proc._wait_signal(proc.proc.started, timeout=0):
pass
class TestWaitFor: class TestWaitFor:
def test_successful(self, pyproc): def test_successful(self, pyproc):
@ -144,6 +196,13 @@ class TestWaitFor:
with pytest.raises(TypeError): with pytest.raises(TypeError):
pyproc.wait_for() pyproc.wait_for()
def test_do_skip(self, pyproc):
"""Test wait_for when getting no text at all, with do_skip."""
pyproc.code = "pass"
pyproc.start()
with pytest.raises(pytest.skip.Exception):
pyproc.wait_for(data="foobar", timeout=100, do_skip=True)
class TestEnsureNotLogged: class TestEnsureNotLogged:

View File

@ -33,7 +33,7 @@ import pytest
('/data/hello.txt', 'Hello World!', True), ('/data/hello.txt', 'Hello World!', True),
]) ])
def test_httpbin(httpbin, qtbot, path, content, expected): def test_httpbin(httpbin, qtbot, path, content, expected):
with qtbot.waitSignal(httpbin.new_request, raising=True, timeout=100): with qtbot.waitSignal(httpbin.new_request, timeout=100):
url = 'http://localhost:{}{}'.format(httpbin.port, path) url = 'http://localhost:{}{}'.format(httpbin.port, path)
try: try:
response = urllib.request.urlopen(url) response = urllib.request.urlopen(url)

View File

@ -72,11 +72,21 @@ class Line:
return '{}({!r})'.format(self.__class__.__name__, self.data) return '{}({!r})'.format(self.__class__.__name__, self.data)
def _render_log(data, threshold=50):
"""Shorten the given log without -v and convert to a string."""
# pylint: disable=no-member
if len(data) > threshold and not pytest.config.getoption('--verbose'):
msg = '[{} lines suppressed, use -v to show]'.format(
len(data) - threshold)
data = [msg] + data[-threshold:]
return '\n'.join(data)
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
"""Add qutebrowser/httpbin sections to captured output if a test failed.""" """Add qutebrowser/httpbin sections to captured output if a test failed."""
outcome = yield outcome = yield
if call.when != 'call': if call.when not in ['call', 'teardown']:
return return
report = outcome.get_result() report = outcome.get_result()
@ -91,12 +101,16 @@ def pytest_runtest_makereport(item, call):
# actually a tuple. This is handled similarily in pytest-qt too. # actually a tuple. This is handled similarily in pytest-qt too.
return return
# pylint: disable=no-member
if pytest.config.getoption('--capture') == 'no':
# Already printed live
return
if quteproc_log is not None: if quteproc_log is not None:
report.longrepr.addsection("qutebrowser output", report.longrepr.addsection("qutebrowser output",
'\n'.join(quteproc_log)) _render_log(quteproc_log))
if httpbin_log is not None: if httpbin_log is not None:
report.longrepr.addsection("httpbin output", report.longrepr.addsection("httpbin output", _render_log(httpbin_log))
'\n'.join(httpbin_log))
class Process(QObject): class Process(QObject):
@ -109,6 +123,7 @@ class Process(QObject):
_invalid: A list of lines which could not be parsed. _invalid: A list of lines which could not be parsed.
_data: A list of parsed lines. _data: A list of parsed lines.
proc: The QProcess for the underlying process. proc: The QProcess for the underlying process.
exit_expected: Whether the process is expected to quit.
Signals: Signals:
ready: Emitted when the server finished starting up. ready: Emitted when the server finished starting up.
@ -126,9 +141,13 @@ class Process(QObject):
self._data = [] self._data = []
self.proc = QProcess() self.proc = QProcess()
self.proc.setReadChannel(QProcess.StandardError) self.proc.setReadChannel(QProcess.StandardError)
self.exit_expected = False
def _log(self, line): def _log(self, line):
"""Add the given line to the captured log output.""" """Add the given line to the captured log output."""
# pylint: disable=no-member
if pytest.config.getoption('--capture') == 'no':
print(line)
self.captured_log.append(line) self.captured_log.append(line)
def _parse_line(self, line): def _parse_line(self, line):
@ -225,8 +244,9 @@ class Process(QObject):
raise InvalidLine(self._invalid) raise InvalidLine(self._invalid)
self.clear_data() self.clear_data()
if not self.is_running(): if not self.is_running() and not self.exit_expected:
raise ProcessExited raise ProcessExited
self.exit_expected = False
def clear_data(self): def clear_data(self):
"""Clear the collected data.""" """Clear the collected data."""
@ -287,7 +307,41 @@ class Process(QObject):
return line return line
return None return None
def wait_for(self, timeout=None, *, override_waited_for=False, **kwargs): def _wait_for_match(self, spy, kwargs):
"""Try matching the kwargs with the given QSignalSpy."""
for args in spy:
assert len(args) == 1
line = args[0]
matches = []
for key, expected in kwargs.items():
value = getattr(line, key)
matches.append(self._match_data(value, expected))
if all(matches):
# If we waited for this line, chances are we don't mean the
# same thing the next time we use wait_for and it matches
# this line again.
line.waited_for = True
return line
return None
def _maybe_skip(self):
"""Can be overridden by subclasses to skip on certain log lines.
We can't run pytest.skip directly while parsing the log, as that would
lead to a pytest.skip.Exception error in a virtual Qt method, which
means pytest-qt fails the test.
Instead, we check for skip messages periodically in
QuteProc._maybe_skip, and call _maybe_skip after every parsed message
in wait_for (where it's most likely that new messages arrive).
"""
pass
def wait_for(self, timeout=None, *, override_waited_for=False,
do_skip=False, **kwargs):
"""Wait until a given value is found in the data. """Wait until a given value is found in the data.
Keyword arguments to this function get interpreted as attributes of the Keyword arguments to this function get interpreted as attributes of the
@ -298,13 +352,17 @@ class Process(QObject):
timeout: How long to wait for the message. timeout: How long to wait for the message.
override_waited_for: If set, gets triggered by previous messages override_waited_for: If set, gets triggered by previous messages
again. again.
do_skip: If set, call pytest.skip on a timeout.
Return: Return:
The matched line. The matched line.
""" """
__tracebackhide__ = True __tracebackhide__ = True
if timeout is None: if timeout is None:
if 'CI' in os.environ: if do_skip:
timeout = 2000
elif 'CI' in os.environ:
timeout = 15000 timeout = 15000
else: else:
timeout = 5000 timeout = 5000
@ -324,27 +382,20 @@ class Process(QObject):
elapsed_timer.start() elapsed_timer.start()
while True: while True:
# Skip if there are pending messages causing a skip
self._maybe_skip()
got_signal = spy.wait(timeout) got_signal = spy.wait(timeout)
if not got_signal or elapsed_timer.hasExpired(timeout): if not got_signal or elapsed_timer.hasExpired(timeout):
raise WaitForTimeout("Timed out after {}ms waiting for " msg = "Timed out after {}ms waiting for {!r}.".format(
"{!r}.".format(timeout, kwargs)) timeout, kwargs)
if do_skip:
pytest.skip(msg)
else:
raise WaitForTimeout(msg)
for args in spy: match = self._wait_for_match(spy, kwargs)
assert len(args) == 1 if match is not None:
line = args[0] return match
matches = []
for key, expected in kwargs.items():
value = getattr(line, key)
matches.append(self._match_data(value, expected))
if all(matches):
# If we waited for this line, chances are we don't mean the
# same thing the next time we use wait_for and it matches
# this line again.
line.waited_for = True
return line
def ensure_not_logged(self, delay=500, **kwargs): def ensure_not_logged(self, delay=500, **kwargs):
"""Make sure the data matching the given arguments is not logged. """Make sure the data matching the given arguments is not logged.

View File

@ -19,13 +19,15 @@
"""Fixtures for the httpbin webserver.""" """Fixtures for the httpbin webserver."""
import re
import sys import sys
import json import json
import socket import socket
import os.path import os.path
import http.client
import pytest import pytest
from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import pyqtSignal, QUrl
import testprocess import testprocess
@ -54,13 +56,23 @@ class Request(testprocess.Line):
self.path = '/' if path == '/' else path.rstrip('/') self.path = '/' if path == '/' else path.rstrip('/')
self.status = parsed['status'] self.status = parsed['status']
self._check_status()
missing_paths = ['/favicon.ico', '/does-not-exist'] def _check_status(self):
"""Check if the http status is what we expected."""
# WORKAROUND for https://github.com/PyCQA/pylint/issues/399 (?)
# pylint: disable=no-member, useless-suppression
path_to_statuses = {
'/favicon.ico': [http.client.NOT_FOUND],
'/does-not-exist': [http.client.NOT_FOUND],
'/custom/redirect-later': [http.client.FOUND],
'/basic-auth/user/password':
[http.client.UNAUTHORIZED, http.client.OK],
}
if self.path in missing_paths: sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
assert self.status == 404 expected_statuses = path_to_statuses.get(sanitized, [http.client.OK])
else: assert self.status in expected_statuses
assert self.status < 400
def __eq__(self, other): def __eq__(self, other):
return NotImplemented return NotImplemented
@ -94,7 +106,7 @@ class ExpectedRequest:
.format(self.verb, self.path)) .format(self.verb, self.path))
class HTTPBin(testprocess.Process): class WebserverProcess(testprocess.Process):
"""Abstraction over a running HTTPbin server process. """Abstraction over a running HTTPbin server process.
@ -110,8 +122,9 @@ class HTTPBin(testprocess.Process):
KEYS = ['verb', 'path'] KEYS = ['verb', 'path']
def __init__(self, parent=None): def __init__(self, script, parent=None):
super().__init__(parent) super().__init__(parent)
self._script = script
self.port = self._get_port() self.port = self._get_port()
self.new_data.connect(self.new_request) self.new_data.connect(self.new_request)
@ -130,8 +143,9 @@ class HTTPBin(testprocess.Process):
def _parse_line(self, line): def _parse_line(self, line):
self._log(line) self._log(line)
if line == (' * Running on http://127.0.0.1:{}/ (Press CTRL+C to ' started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/ '
'quit)'.format(self.port)): r'\(Press CTRL\+C to quit\)'.format(self.port))
if started_re.fullmatch(line):
self.ready.emit() self.ready.emit()
return None return None
return Request(line) return Request(line)
@ -139,12 +153,12 @@ class HTTPBin(testprocess.Process):
def _executable_args(self): def _executable_args(self):
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen'):
executable = os.path.join(os.path.dirname(sys.executable), executable = os.path.join(os.path.dirname(sys.executable),
'webserver_sub') self._script)
args = [str(self.port)] args = [str(self.port)]
else: else:
executable = sys.executable executable = sys.executable
py_file = os.path.join(os.path.dirname(__file__), py_file = os.path.join(os.path.dirname(__file__),
'webserver_sub.py') self._script + '.py')
args = [py_file, str(self.port)] args = [py_file, str(self.port)]
return executable, args return executable, args
@ -157,7 +171,7 @@ class HTTPBin(testprocess.Process):
@pytest.yield_fixture(scope='session', autouse=True) @pytest.yield_fixture(scope='session', autouse=True)
def httpbin(qapp): def httpbin(qapp):
"""Fixture for a httpbin object which ensures clean setup/teardown.""" """Fixture for a httpbin object which ensures clean setup/teardown."""
httpbin = HTTPBin() httpbin = WebserverProcess('webserver_sub')
httpbin.start() httpbin.start()
yield httpbin yield httpbin
httpbin.cleanup() httpbin.cleanup()
@ -169,3 +183,18 @@ def httpbin_after_test(httpbin, request):
request.node._httpbin_log = httpbin.captured_log request.node._httpbin_log = httpbin.captured_log
yield yield
httpbin.after_test() httpbin.after_test()
@pytest.yield_fixture
def ssl_server(request, qapp):
"""Fixture for a webserver with a self-signed SSL certificate.
This needs to be explicitly used in a test, and overwrites the httpbin log
used in that test.
"""
server = WebserverProcess('webserver_sub_ssl')
request.node._httpbin_log = server.captured_log
server.start()
yield server
server.after_test()
server.cleanup()

View File

@ -0,0 +1,66 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 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/>.
"""Minimal flask webserver serving a Hello World via SSL.
This script gets called as a QProcess from integration/conftest.py.
"""
import ssl
import sys
import logging
import os.path
import flask
import webserver_sub
app = flask.Flask(__name__)
@app.route('/')
def hello_world():
return "Hello World via SSL!"
@app.after_request
def log_request(response):
return webserver_sub.log_request(response)
@app.before_first_request
def turn_off_logging():
# Turn off werkzeug logging after the startup message has been printed.
logging.getLogger('werkzeug').setLevel(logging.ERROR)
def main():
ssl_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'data', 'ssl')
# WORKAROUND for https://github.com/PyCQA/pylint/issues/399
# pylint: disable=no-member, useless-suppression
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(os.path.join(ssl_dir, 'cert.pem'),
os.path.join(ssl_dir, 'key.pem'))
app.run(port=int(sys.argv[1]), debug=False, ssl_context=context)
if __name__ == '__main__':
main()

View File

@ -53,7 +53,7 @@ class TestFixedDataNetworkReply:
def test_data(self, qtbot, req, data): def test_data(self, qtbot, req, data):
reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo') reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo')
with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead, with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead,
reply.finished], raising=True): reply.finished]):
pass pass
assert reply.bytesAvailable() == len(data) assert reply.bytesAvailable() == len(data)
@ -78,7 +78,7 @@ def test_error_network_reply(qtbot, req):
reply = networkreply.ErrorNetworkReply( reply = networkreply.ErrorNetworkReply(
req, "This is an error", QNetworkReply.UnknownNetworkError) req, "This is an error", QNetworkReply.UnknownNetworkError)
with qtbot.waitSignals([reply.error, reply.finished], raising=True): with qtbot.waitSignals([reply.error, reply.finished]):
pass pass
reply.abort() # shouldn't do anything reply.abort() # shouldn't do anything

View File

@ -22,7 +22,6 @@
from unittest import mock from unittest import mock
from PyQt5.QtNetwork import QNetworkCookie from PyQt5.QtNetwork import QNetworkCookie
from PyQt5.QtTest import QSignalSpy
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
import pytest import pytest
@ -79,7 +78,7 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch):
ram_jar = cookies.RAMCookieJar() ram_jar = cookies.RAMCookieJar()
cookie = QNetworkCookie(b'foo', b'bar') cookie = QNetworkCookie(b'foo', b'bar')
url = QUrl('http://example.com/') url = QUrl('http://example.com/')
with qtbot.waitSignal(ram_jar.changed, raising=True): with qtbot.waitSignal(ram_jar.changed):
assert ram_jar.setCookiesFromUrl([cookie], url) assert ram_jar.setCookiesFromUrl([cookie], url)
# assert the cookies are added correctly # assert the cookies are added correctly
@ -90,15 +89,15 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch):
assert saved_cookie.name(), saved_cookie.value() == expected assert saved_cookie.name(), saved_cookie.value() == expected
def test_set_cookies_never_accept(config_stub): def test_set_cookies_never_accept(qtbot, config_stub):
"""Test setCookiesFromUrl when cookies are not accepted.""" """Test setCookiesFromUrl when cookies are not accepted."""
config_stub.data = CONFIG_NEVER_COOKIES config_stub.data = CONFIG_NEVER_COOKIES
ram_jar = cookies.RAMCookieJar() ram_jar = cookies.RAMCookieJar()
changed_signal_spy = QSignalSpy(ram_jar.changed)
url = QUrl('http://example.com/') url = QUrl('http://example.com/')
assert not ram_jar.setCookiesFromUrl(url, 'test')
assert not changed_signal_spy with qtbot.assertNotEmitted(ram_jar.changed):
assert not ram_jar.setCookiesFromUrl(url, 'test')
assert not ram_jar.cookiesForUrl(url) assert not ram_jar.cookiesForUrl(url)
@ -151,21 +150,10 @@ def test_cookies_changed_emit(config_stub, fake_save_manager,
'LineParser', LineparserSaveStub) 'LineParser', LineparserSaveStub)
jar = cookies.CookieJar() jar = cookies.CookieJar()
with qtbot.waitSignal(jar.changed, raising=True): with qtbot.waitSignal(jar.changed):
config_stub.set('content', 'cookies-store', False) config_stub.set('content', 'cookies-store', False)
def test_cookies_changed_not_emitted(config_stub, fake_save_manager,
monkeypatch, qapp):
"""Test that changed is not emitted when nothing changes."""
config_stub.data = CONFIG_COOKIES_ENABLED
monkeypatch.setattr(lineparser,
'LineParser', LineparserSaveStub)
jar = cookies.CookieJar()
changed_spy = QSignalSpy(jar.changed)
assert not changed_spy
@pytest.mark.parametrize('store_cookies,empty', [ @pytest.mark.parametrize('store_cookies,empty', [
(True, False), (True, False),
(False, True) (False, True)

View File

@ -628,7 +628,7 @@ class TestJavascriptEscape:
with open(path, encoding='utf-8') as f: with open(path, encoding='utf-8') as f:
html_source = f.read().replace('%INPUT%', escaped) html_source = f.read().replace('%INPUT%', escaped)
with qtbot.waitSignal(webframe.loadFinished, raising=True) as blocker: with qtbot.waitSignal(webframe.loadFinished) as blocker:
webframe.setHtml(html_source) webframe.setHtml(html_source)
assert blocker.args == [True] assert blocker.args == [True]

View File

@ -79,8 +79,7 @@ def test_command(qtbot, py_proc, runner):
with open(os.environ['QUTE_FIFO'], 'w') as f: with open(os.environ['QUTE_FIFO'], 'w') as f:
f.write('foo\n') f.write('foo\n')
""") """)
with qtbot.waitSignal(runner.got_cmd, raising=True, with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
timeout=10000) as blocker:
runner.run(cmd, *args) runner.run(cmd, *args)
assert blocker.args == ['foo'] assert blocker.args == ['foo']
@ -100,8 +99,7 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
f.write('\n') f.write('\n')
""") """)
with qtbot.waitSignal(runner.got_cmd, raising=True, with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
timeout=10000) as blocker:
runner.run(cmd, *args, env=env) runner.run(cmd, *args, env=env)
data = blocker.args[0] data = blocker.args[0]
@ -136,9 +134,8 @@ def test_temporary_files(qtbot, tmpdir, py_proc, runner):
f.write('\n') f.write('\n')
""") """)
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000): with qtbot.waitSignal(runner.finished, timeout=10000):
with qtbot.waitSignal(runner.got_cmd, raising=True, with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
timeout=10000) as blocker:
runner.run(cmd, *args, env=env) runner.run(cmd, *args, env=env)
data = blocker.args[0] data = blocker.args[0]
@ -160,7 +157,7 @@ def test_command_with_error(qtbot, tmpdir, py_proc, runner):
sys.exit(1) sys.exit(1)
""") """)
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000): with qtbot.waitSignal(runner.finished, timeout=10000):
runner.run(cmd, *args, env=env) runner.run(cmd, *args, env=env)
assert not text_file.exists() assert not text_file.exists()
@ -191,14 +188,13 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner):
""") """)
args.append(str(pidfile)) args.append(str(pidfile))
with qtbot.waitSignal(watcher.directoryChanged, raising=True, with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
timeout=10000):
runner.run(cmd, *args, env=env) runner.run(cmd, *args, env=env)
# Make sure the PID was written to the file, not just the file created # Make sure the PID was written to the file, not just the file created
time.sleep(0.5) time.sleep(0.5)
with qtbot.waitSignal(runner.finished, raising=True): with qtbot.waitSignal(runner.finished):
os.kill(int(pidfile.read()), signal.SIGTERM) os.kill(int(pidfile.read()), signal.SIGTERM)
assert not text_file.exists() assert not text_file.exists()
@ -216,7 +212,7 @@ def test_temporary_files_failed_cleanup(caplog, qtbot, tmpdir, py_proc,
""") """)
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000): with qtbot.waitSignal(runner.finished, timeout=10000):
runner.run(cmd, *args, env={'QUTE_HTML': str(test_file)}) runner.run(cmd, *args, env={'QUTE_HTML': str(test_file)})
assert len(caplog.records) == 1 assert len(caplog.records) == 1

View File

@ -305,6 +305,8 @@ class TestString:
({'minlen': 2}, 'fo'), ({'minlen': 2}, 'fo'),
({'minlen': 2, 'maxlen': 3}, 'fo'), ({'minlen': 2, 'maxlen': 3}, 'fo'),
({'minlen': 2, 'maxlen': 3}, 'foo'), ({'minlen': 2, 'maxlen': 3}, 'foo'),
# valid_values
({'valid_values': configtypes.ValidValues('fooo')}, 'fooo'),
]) ])
def test_validate_valid(self, klass, kwargs, val): def test_validate_valid(self, klass, kwargs, val):
klass(**kwargs).validate(val) klass(**kwargs).validate(val)
@ -319,6 +321,8 @@ class TestString:
({'maxlen': 2}, 'fob'), ({'maxlen': 2}, 'fob'),
({'minlen': 2, 'maxlen': 3}, 'f'), ({'minlen': 2, 'maxlen': 3}, 'f'),
({'minlen': 2, 'maxlen': 3}, 'fooo'), ({'minlen': 2, 'maxlen': 3}, 'fooo'),
# valid_values
({'valid_values': configtypes.ValidValues('blah')}, 'fooo'),
]) ])
def test_validate_invalid(self, klass, kwargs, val): def test_validate_invalid(self, klass, kwargs, val):
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
@ -335,6 +339,15 @@ class TestString:
def test_complete(self, klass, value): def test_complete(self, klass, value):
assert klass(completions=value).complete() == value assert klass(completions=value).complete() == value
@pytest.mark.parametrize('valid_values, expected', [
(configtypes.ValidValues('one', 'two'),
[('one', ''), ('two', '')]),
(configtypes.ValidValues(('1', 'one'), ('2', 'two')),
[('1', 'one'), ('2', 'two')]),
])
def test_complete_valid_values(self, klass, valid_values, expected):
assert klass(valid_values=valid_values).complete() == expected
class TestList: class TestList:

View File

@ -94,8 +94,7 @@ class JSTester:
**kwargs: Passed to jinja's template.render(). **kwargs: Passed to jinja's template.render().
""" """
template = self._jinja_env.get_template(path) template = self._jinja_env.get_template(path)
with self._qtbot.waitSignal(self.webview.loadFinished, with self._qtbot.waitSignal(self.webview.loadFinished) as blocker:
raising=True) as blocker:
self.webview.setHtml(template.render(**kwargs)) self.webview.setHtml(template.render(**kwargs))
assert blocker.args == [True] assert blocker.args == [True]

View File

@ -282,7 +282,7 @@ class TestKeyChain:
assert not keyparser.execute.called assert not keyparser.execute.called
assert keyparser._ambiguous_timer.isActive() assert keyparser._ambiguous_timer.isActive()
# We wait for the timeout to occur. # We wait for the timeout to occur.
with qtbot.waitSignal(keyparser.keystring_updated, raising=True): with qtbot.waitSignal(keyparser.keystring_updated):
pass pass
assert keyparser.execute.called assert keyparser.execute.called

View File

@ -23,29 +23,11 @@
from collections import namedtuple from collections import namedtuple
import pytest import pytest
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtCore import QSize, Qt
from qutebrowser.browser import webview from qutebrowser.browser import webview
from qutebrowser.mainwindow.statusbar.progress import Progress from qutebrowser.mainwindow.statusbar.progress import Progress
class FakeStatusBar(QWidget):
"""Fake statusbar to test progressbar sizing."""
def __init__(self, parent=None):
super().__init__(parent)
self.hbox = QHBoxLayout(self)
self.hbox.addStretch()
self.hbox.setContentsMargins(0, 0, 0, 0)
self.setAttribute(Qt.WA_StyledBackground, True)
self.setStyleSheet('background-color: red;')
def minimumSizeHint(self):
return QSize(1, self.fontMetrics().height())
@pytest.fixture @pytest.fixture
def progress_widget(qtbot, monkeypatch, config_stub): def progress_widget(qtbot, monkeypatch, config_stub):
"""Create a Progress widget and checks its initial state.""" """Create a Progress widget and checks its initial state."""
@ -62,25 +44,6 @@ def progress_widget(qtbot, monkeypatch, config_stub):
return widget return widget
@pytest.fixture
def fake_statusbar(qtbot):
"""Fixture providing a statusbar in a container window."""
container = QWidget()
qtbot.add_widget(container)
vbox = QVBoxLayout(container)
vbox.addStretch()
statusbar = FakeStatusBar(container)
# to make sure container isn't GCed
# pylint: disable=attribute-defined-outside-init
statusbar.container = container
vbox.addWidget(statusbar)
container.show()
qtbot.waitForWindowShown(container)
return statusbar
def test_load_started(progress_widget): def test_load_started(progress_widget):
"""Ensure the Progress widget reacts properly when the page starts loading. """Ensure the Progress widget reacts properly when the page starts loading.

View File

@ -0,0 +1,57 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 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/>.
"""Test Prompt widget."""
import sip
import pytest
from qutebrowser.mainwindow.statusbar.prompt import Prompt
from qutebrowser.utils import objreg
@pytest.yield_fixture
def prompt(qtbot, win_registry):
prompt = Prompt(0)
qtbot.addWidget(prompt)
yield prompt
# If we don't clean up here, this test will remove 'prompter' from the
# objreg at some point in the future, which will cause some other test to
# fail.
sip.delete(prompt)
def test_prompt(prompt):
prompt.show()
objreg.get('prompt', scope='window', window=0)
objreg.get('prompter', scope='window', window=0)
@pytest.mark.xfail(reason="This test is broken and I don't get why")
def test_resizing(fake_statusbar, prompt):
fake_statusbar.hbox.addWidget(prompt)
prompt.txt.setText("Blah?")
old_width = prompt.lineedit.width()
prompt.lineedit.setText("Hello World" * 100)
assert prompt.lineedit.width() > old_width

View File

@ -20,7 +20,6 @@
"""Tests for qutebrowser.misc.autoupdate""" """Tests for qutebrowser.misc.autoupdate"""
import pytest import pytest
from PyQt5.QtTest import QSignalSpy
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.misc import autoupdate, httpclient from qutebrowser.misc import autoupdate, httpclient
@ -63,13 +62,9 @@ def test_get_version_success(qtbot):
http_stub = HTTPGetStub(success=True) http_stub = HTTPGetStub(success=True)
client = autoupdate.PyPIVersionClient(client=http_stub) client = autoupdate.PyPIVersionClient(client=http_stub)
# Use a spy to inspect the signal with qtbot.assertNotEmitted(client.error):
error_spy = QSignalSpy(client.error) with qtbot.waitSignal(client.success):
client.get_version('test')
with qtbot.waitSignal(client.success, raising=True):
client.get_version('test')
assert len(error_spy) == 0
assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json') assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json')
@ -79,13 +74,9 @@ def test_get_version_error(qtbot):
http_stub = HTTPGetStub(success=False) http_stub = HTTPGetStub(success=False)
client = autoupdate.PyPIVersionClient(client=http_stub) client = autoupdate.PyPIVersionClient(client=http_stub)
# Use a spy to inspect the signal with qtbot.assertNotEmitted(client.success):
success_spy = QSignalSpy(client.success) with qtbot.waitSignal(client.error):
client.get_version('test')
with qtbot.waitSignal(client.error, raising=True):
client.get_version('test')
assert len(success_spy) == 0
@pytest.mark.parametrize('json', INVALID_JSON) @pytest.mark.parametrize('json', INVALID_JSON)
@ -95,10 +86,6 @@ def test_invalid_json(qtbot, json):
client = autoupdate.PyPIVersionClient(client=http_stub) client = autoupdate.PyPIVersionClient(client=http_stub)
client.get_version('test') client.get_version('test')
# Use a spy to inspect the signal with qtbot.assertNotEmitted(client.success):
success_spy = QSignalSpy(client.success) with qtbot.waitSignal(client.error):
client.get_version('test')
with qtbot.waitSignal(client.error, raising=True):
client.get_version('test')
assert len(success_spy) == 0

View File

@ -40,7 +40,8 @@ def proc(qtbot):
p = guiprocess.GUIProcess(0, 'testprocess') p = guiprocess.GUIProcess(0, 'testprocess')
yield p yield p
if p._proc.state() == QProcess.Running: if p._proc.state() == QProcess.Running:
with qtbot.waitSignal(p.finished, timeout=10000) as blocker: with qtbot.waitSignal(p.finished, timeout=10000,
raising=False) as blocker:
p._proc.terminate() p._proc.terminate()
if not blocker.signal_triggered: if not blocker.signal_triggered:
p._proc.kill() p._proc.kill()
@ -56,8 +57,7 @@ def fake_proc(monkeypatch, stubs):
def test_start(proc, qtbot, guiprocess_message_mock, py_proc): def test_start(proc, qtbot, guiprocess_message_mock, py_proc):
"""Test simply starting a process.""" """Test simply starting a process."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
timeout=10000):
argv = py_proc("import sys; print('test'); sys.exit(0)") argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv) proc.start(*argv)
@ -69,8 +69,7 @@ def test_start_verbose(proc, qtbot, guiprocess_message_mock, py_proc):
"""Test starting a process verbosely.""" """Test starting a process verbosely."""
proc.verbose = True proc.verbose = True
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
timeout=10000):
argv = py_proc("import sys; print('test'); sys.exit(0)") argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv) proc.start(*argv)
@ -97,8 +96,7 @@ def test_start_env(monkeypatch, qtbot, py_proc):
sys.exit(0) sys.exit(0)
""") """)
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
timeout=10000):
proc.start(*argv) proc.start(*argv)
data = bytes(proc._proc.readAll()).decode('utf-8') data = bytes(proc._proc.readAll()).decode('utf-8')
@ -110,8 +108,7 @@ def test_start_env(monkeypatch, qtbot, py_proc):
@pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device', extend=True) @pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device', extend=True)
def test_start_mode(proc, qtbot, py_proc): def test_start_mode(proc, qtbot, py_proc):
"""Test simply starting a process with mode parameter.""" """Test simply starting a process with mode parameter."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
timeout=10000):
argv = py_proc("import sys; print('test'); sys.exit(0)") argv = py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv, mode=QIODevice.NotOpen) proc.start(*argv, mode=QIODevice.NotOpen)
@ -139,7 +136,7 @@ def test_start_detached_error(fake_proc, guiprocess_message_mock):
def test_double_start(qtbot, proc, py_proc): def test_double_start(qtbot, proc, py_proc):
"""Test starting a GUIProcess twice.""" """Test starting a GUIProcess twice."""
with qtbot.waitSignal(proc.started, raising=True, timeout=10000): with qtbot.waitSignal(proc.started, timeout=10000):
argv = py_proc("import time; time.sleep(10)") argv = py_proc("import time; time.sleep(10)")
proc.start(*argv) proc.start(*argv)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -148,12 +145,10 @@ def test_double_start(qtbot, proc, py_proc):
def test_double_start_finished(qtbot, proc, py_proc): def test_double_start_finished(qtbot, proc, py_proc):
"""Test starting a GUIProcess twice (with the first call finished).""" """Test starting a GUIProcess twice (with the first call finished)."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
timeout=10000):
argv = py_proc("import sys; sys.exit(0)") argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv) proc.start(*argv)
with qtbot.waitSignals([proc.started, proc.finished], raising=True, with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
timeout=10000):
argv = py_proc("import sys; sys.exit(0)") argv = py_proc("import sys; sys.exit(0)")
proc.start(*argv) proc.start(*argv)
@ -180,7 +175,7 @@ def test_start_logging(fake_proc, caplog):
def test_error(qtbot, proc, caplog, guiprocess_message_mock): def test_error(qtbot, proc, caplog, guiprocess_message_mock):
"""Test the process emitting an error.""" """Test the process emitting an error."""
with caplog.at_level(logging.ERROR, 'message'): with caplog.at_level(logging.ERROR, 'message'):
with qtbot.waitSignal(proc.error, raising=True, timeout=5000): with qtbot.waitSignal(proc.error, timeout=5000):
proc.start('this_does_not_exist_either', []) proc.start('this_does_not_exist_either', [])
msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error, msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error,
@ -191,7 +186,7 @@ def test_error(qtbot, proc, caplog, guiprocess_message_mock):
def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc): def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc):
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000): with qtbot.waitSignal(proc.finished, timeout=10000):
proc.start(*py_proc('import sys; sys.exit(1)')) proc.start(*py_proc('import sys; sys.exit(1)'))
msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error) msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error)
@ -202,7 +197,7 @@ def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc):
def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream): def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream):
"""When a process fails, its output should be logged.""" """When a process fails, its output should be logged."""
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000): with qtbot.waitSignal(proc.finished, timeout=10000):
proc.start(*py_proc(""" proc.start(*py_proc("""
import sys import sys
print("test", file=sys.{}) print("test", file=sys.{})
@ -219,7 +214,7 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream):
The test doesn't actually check the log as it'd fail because of the error The test doesn't actually check the log as it'd fail because of the error
logging. logging.
""" """
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000): with qtbot.waitSignal(proc.finished, timeout=10000):
proc.start(*py_proc(""" proc.start(*py_proc("""
import sys import sys
print("test", file=sys.{}) print("test", file=sys.{})

View File

@ -343,13 +343,11 @@ class TestListen:
ipc_server.listen() ipc_server.listen()
old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000, with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000):
raising=True):
pass pass
# Make sure the timer is not singleShot # Make sure the timer is not singleShot
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000, with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000):
raising=True):
pass pass
new_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns new_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
@ -412,9 +410,9 @@ class TestHandleConnection:
def test_double_connection(self, qlocalsocket, ipc_server, caplog): def test_double_connection(self, qlocalsocket, ipc_server, caplog):
ipc_server._socket = qlocalsocket ipc_server._socket = qlocalsocket
ipc_server.handle_connection() ipc_server.handle_connection()
message = ("Got new connection but ignoring it because we're still " msg = ("Got new connection but ignoring it because we're still "
"handling another one.") "handling another one")
assert message in [rec.message for rec in caplog.records] assert any(rec.message.startswith(msg) for rec in caplog.records)
def test_disconnected_immediately(self, ipc_server, caplog): def test_disconnected_immediately(self, ipc_server, caplog):
socket = FakeSocket(state=QLocalSocket.UnconnectedState) socket = FakeSocket(state=QLocalSocket.UnconnectedState)
@ -444,7 +442,7 @@ class TestHandleConnection:
ipc_server._server = FakeServer(socket) ipc_server._server = FakeServer(socket)
with qtbot.waitSignal(ipc_server.got_args, raising=True) as blocker: with qtbot.waitSignal(ipc_server.got_args) as blocker:
ipc_server.handle_connection() ipc_server.handle_connection()
assert blocker.args == [['foo'], 'tab', ''] assert blocker.args == [['foo'], 'tab', '']
@ -458,7 +456,7 @@ def connected_socket(qtbot, qlocalsocket, ipc_server):
pytest.skip("Skipping connected_socket test - " pytest.skip("Skipping connected_socket test - "
"https://github.com/The-Compiler/qutebrowser/issues/1045") "https://github.com/The-Compiler/qutebrowser/issues/1045")
ipc_server.listen() ipc_server.listen()
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True): with qtbot.waitSignal(ipc_server._server.newConnection):
qlocalsocket.connectToServer('qute-test') qlocalsocket.connectToServer('qute-test')
yield qlocalsocket yield qlocalsocket
qlocalsocket.disconnectFromServer() qlocalsocket.disconnectFromServer()
@ -496,22 +494,19 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8')
(b'{"args": [], "target_arg": null}\n', 'invalid version'), (b'{"args": [], "target_arg": null}\n', 'invalid version'),
]) ])
def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg): def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
got_args_spy = QSignalSpy(ipc_server.got_args)
signals = [ipc_server.got_invalid_data, connected_socket.disconnected] signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
with qtbot.waitSignals(signals, raising=True): with qtbot.assertNotEmitted(ipc_server.got_args):
connected_socket.write(data) with qtbot.waitSignals(signals):
connected_socket.write(data)
messages = [r.message for r in caplog.records] messages = [r.message for r in caplog.records]
assert messages[-1] == 'Ignoring invalid IPC data.' assert messages[-1].startswith('Ignoring invalid IPC data from socket ')
assert messages[-2].startswith(msg) assert messages[-2].startswith(msg)
assert not got_args_spy
def test_multiline(qtbot, ipc_server, connected_socket): def test_multiline(qtbot, ipc_server, connected_socket):
spy = QSignalSpy(ipc_server.got_args) spy = QSignalSpy(ipc_server.got_args)
error_spy = QSignalSpy(ipc_server.got_invalid_data)
data = ('{{"args": ["one"], "target_arg": "tab",' data = ('{{"args": ["one"], "target_arg": "tab",'
' "protocol_version": {version}}}\n' ' "protocol_version": {version}}}\n'
@ -519,11 +514,10 @@ def test_multiline(qtbot, ipc_server, connected_socket):
' "protocol_version": {version}}}\n'.format( ' "protocol_version": {version}}}\n'.format(
version=ipc.PROTOCOL_VERSION)) version=ipc.PROTOCOL_VERSION))
with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args], with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
raising=True): with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args]):
connected_socket.write(data.encode('utf-8')) connected_socket.write(data.encode('utf-8'))
assert not error_spy
assert len(spy) == 2 assert len(spy) == 2
assert spy[0] == [['one'], 'tab', ''] assert spy[0] == [['one'], 'tab', '']
assert spy[1] == [['two'], '', ''] assert spy[1] == [['two'], '', '']
@ -542,19 +536,19 @@ class TestSendToRunningInstance:
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd): def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
ipc_server.listen() ipc_server.listen()
raw_spy = QSignalSpy(ipc_server.got_raw) raw_spy = QSignalSpy(ipc_server.got_raw)
error_spy = QSignalSpy(ipc_server.got_invalid_data)
with qtbot.waitSignal(ipc_server.got_args, raising=True, with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
timeout=5000) as blocker: with qtbot.waitSignal(ipc_server.got_args,
with tmpdir.as_cwd(): timeout=5000) as blocker:
if not has_cwd: with tmpdir.as_cwd():
m = mocker.patch('qutebrowser.misc.ipc.os') if not has_cwd:
m.getcwd.side_effect = OSError m = mocker.patch('qutebrowser.misc.ipc.os')
sent = ipc.send_to_running_instance('qute-test', ['foo'], None) m.getcwd.side_effect = OSError
sent = ipc.send_to_running_instance('qute-test', ['foo'],
None)
assert sent assert sent
assert not error_spy
expected_cwd = str(tmpdir) if has_cwd else '' expected_cwd = str(tmpdir) if has_cwd else ''
assert blocker.args == [['foo'], '', expected_cwd] assert blocker.args == [['foo'], '', expected_cwd]
@ -598,15 +592,14 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
ipc_server._timer.setInterval(100) ipc_server._timer.setInterval(100)
ipc_server.listen() ipc_server.listen()
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True): with qtbot.waitSignal(ipc_server._server.newConnection):
qlocalsocket.connectToServer('qute-test') qlocalsocket.connectToServer('qute-test')
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True, with qtbot.waitSignal(qlocalsocket.disconnected, timeout=5000):
timeout=5000):
pass pass
assert caplog.records[-1].message == "IPC connection timed out." assert caplog.records[-1].message.startswith("IPC connection timed out")
@pytest.mark.parametrize('method, args, is_warning', [ @pytest.mark.parametrize('method, args, is_warning', [
@ -679,14 +672,14 @@ class TestSendOrListen:
objreg_server = objreg.get('ipc-server') objreg_server = objreg.get('ipc-server')
assert objreg_server is ret_server assert objreg_server is ret_server
with qtbot.waitSignal(ret_server.got_args, raising=True): with qtbot.waitSignal(ret_server.got_args):
ret_client = ipc.send_or_listen(args) ret_client = ipc.send_or_listen(args)
assert ret_client is None assert ret_client is None
@pytest.mark.posix(reason="Unneeded on Windows") @pytest.mark.posix(reason="Unneeded on Windows")
def test_legacy_name(self, caplog, qtbot, args, legacy_server): def test_legacy_name(self, caplog, qtbot, args, legacy_server):
with qtbot.waitSignal(legacy_server.got_args, raising=True): with qtbot.waitSignal(legacy_server.got_args):
ret = ipc.send_or_listen(args) ret = ipc.send_or_listen(args)
assert ret is None assert ret is None
msgs = [e.message for e in caplog.records] msgs = [e.message for e in caplog.records]
@ -727,7 +720,7 @@ class TestSendOrListen:
assert isinstance(ret_server, ipc.IPCServer) assert isinstance(ret_server, ipc.IPCServer)
logging.debug('== Connecting ==') logging.debug('== Connecting ==')
with qtbot.waitSignal(ret_server.got_args, raising=True): with qtbot.waitSignal(ret_server.got_args):
ret_client = ipc.send_or_listen(args) ret_client = ipc.send_or_listen(args)
assert ret_client is None assert ret_client is None

View File

@ -74,7 +74,7 @@ def test_finished_signal(qtbot):
qtbot.add_widget(box) qtbot.add_widget(box)
with qtbot.waitSignal(box.finished, raising=True): with qtbot.waitSignal(box.finished):
box.accept() box.accept()
assert signal_triggered assert signal_triggered

View File

@ -439,9 +439,8 @@ class TestSave:
def test_update_completion_signal(self, sess_man, tmpdir, qtbot): def test_update_completion_signal(self, sess_man, tmpdir, qtbot):
session_path = tmpdir / 'foo.yml' session_path = tmpdir / 'foo.yml'
blocker = qtbot.waitSignal(sess_man.update_completion) with qtbot.waitSignal(sess_man.update_completion):
sess_man.save(str(session_path)) sess_man.save(str(session_path))
assert blocker.signal_triggered
def test_no_state_config(self, sess_man, tmpdir, state_config): def test_no_state_config(self, sess_man, tmpdir, state_config):
session_path = tmpdir / 'foo.yml' session_path = tmpdir / 'foo.yml'
@ -691,9 +690,8 @@ class TestDelete:
sess = tmpdir / 'foo.yml' sess = tmpdir / 'foo.yml'
sess.ensure() sess.ensure()
blocker = qtbot.waitSignal(sess_man.update_completion) with qtbot.waitSignal(sess_man.update_completion):
sess_man.delete(str(sess)) sess_man.delete(str(sess))
assert blocker.signal_triggered
def test_not_existing(self, sess_man, qtbot, tmpdir): def test_not_existing(self, sess_man, qtbot, tmpdir):
sess = tmpdir / 'foo.yml' sess = tmpdir / 'foo.yml'

View File

@ -166,13 +166,17 @@ class TestFuzzyUrl:
assert not os_mock.path.exists.called assert not os_mock.path.exists.called
assert url == QUrl('http://foo') assert url == QUrl('http://foo')
def test_file_absolute(self, os_mock): @pytest.mark.parametrize('path, expected', [
('/foo', QUrl('file:///foo')),
('/bar\n', QUrl('file:///bar')),
])
def test_file_absolute(self, path, expected, os_mock):
"""Test with an absolute path.""" """Test with an absolute path."""
os_mock.path.exists.return_value = True os_mock.path.exists.return_value = True
os_mock.path.isabs.return_value = True os_mock.path.isabs.return_value = True
url = urlutils.fuzzy_url('/foo') url = urlutils.fuzzy_url(path)
assert url == QUrl('file:///foo') assert url == expected
@pytest.mark.posix @pytest.mark.posix
def test_file_absolute_expanded(self, os_mock): def test_file_absolute_expanded(self, os_mock):

View File

@ -61,23 +61,21 @@ def test_done(mode, answer, signal_names, question, qtbot):
question.mode = mode question.mode = mode
question.answer = answer question.answer = answer
signals = [getattr(question, name) for name in signal_names] signals = [getattr(question, name) for name in signal_names]
with qtbot.waitSignals(signals, raising=True): with qtbot.waitSignals(signals):
question.done() question.done()
assert not question.is_aborted assert not question.is_aborted
def test_cancel(question, qtbot): def test_cancel(question, qtbot):
"""Test Question.cancel().""" """Test Question.cancel()."""
with qtbot.waitSignals([question.cancelled, question.completed], with qtbot.waitSignals([question.cancelled, question.completed]):
raising=True):
question.cancel() question.cancel()
assert not question.is_aborted assert not question.is_aborted
def test_abort(question, qtbot): def test_abort(question, qtbot):
"""Test Question.abort().""" """Test Question.abort()."""
with qtbot.waitSignals([question.aborted, question.completed], with qtbot.waitSignals([question.aborted, question.completed]):
raising=True):
question.abort() question.abort()
assert question.is_aborted assert question.is_aborted

View File

@ -72,13 +72,13 @@ def test_start_overflow():
def test_timeout_start(qtbot): def test_timeout_start(qtbot):
"""Make sure the timer works with start().""" """Make sure the timer works with start()."""
t = usertypes.Timer() t = usertypes.Timer()
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True): with qtbot.waitSignal(t.timeout, timeout=3000):
t.start(200) t.start(200)
def test_timeout_set_interval(qtbot): def test_timeout_set_interval(qtbot):
"""Make sure the timer works with setInterval().""" """Make sure the timer works with setInterval()."""
t = usertypes.Timer() t = usertypes.Timer()
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True): with qtbot.waitSignal(t.timeout, timeout=3000):
t.setInterval(200) t.setInterval(200)
t.start() t.start()

20
tox.ini
View File

@ -20,7 +20,7 @@ deps =
Flask==0.10.1 Flask==0.10.1
glob2==0.4.1 glob2==0.4.1
httpbin==0.4.0 httpbin==0.4.0
hypothesis==1.18.1 hypothesis==2.0.0
itsdangerous==0.24 itsdangerous==0.24
Mako==1.0.3 Mako==1.0.3
parse==1.6.6 parse==1.6.6
@ -33,9 +33,10 @@ deps =
pytest-faulthandler==1.3.0 pytest-faulthandler==1.3.0
pytest-html==1.7 pytest-html==1.7
pytest-mock==0.9.0 pytest-mock==0.9.0
pytest-qt==1.10.0 pytest-qt==1.11.0
pytest-sugar==0.5.1 pytest-instafail==0.3.0
pytest-travis-fold==1.2.0 pytest-travis-fold==1.2.0
pytest-repeat==0.2
six==1.10.0 six==1.10.0
termcolor==1.1.0 termcolor==1.1.0
vulture==0.8.1 vulture==0.8.1
@ -45,7 +46,7 @@ deps =
cherrypy==4.0.0 cherrypy==4.0.0
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} {envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --instafail --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
{envpython} scripts/dev/check_coverage.py {posargs} {envpython} scripts/dev/check_coverage.py {posargs}
[testenv:mkvenv] [testenv:mkvenv]
@ -73,7 +74,7 @@ passenv = {[testenv]passenv}
deps = {[testenv]deps} deps = {[testenv]deps}
setenv = setenv =
DISPLAY= DISPLAY=
QUTE_NO_DISPLAY_OK=1 QUTE_NO_DISPLAY=1
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEw {posargs:tests} {envpython} -m py.test --strict -rfEw {posargs:tests}
@ -105,8 +106,8 @@ passenv =
deps = deps =
{[testenv]deps} {[testenv]deps}
{[testenv:misc]deps} {[testenv:misc]deps}
astroid==1.4.3 astroid==1.4.4
pylint==1.5.2 pylint==1.5.4
requests==2.9.1 requests==2.9.1
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
@ -156,7 +157,6 @@ deps =
py==1.4.31 py==1.4.31
pyflakes==1.0.0 pyflakes==1.0.0
pytest==2.8.5 pytest==2.8.5
pytest-cache==1.0
pytest-flakes==1.0.1 pytest-flakes==1.0.1
commands = commands =
{envpython} -m py.test -q --flakes --ignore=tests --noconftest {envpython} -m py.test -q --flakes --ignore=tests --noconftest
@ -168,10 +168,9 @@ deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
apipkg==1.4 apipkg==1.4
execnet==1.4.1 execnet==1.4.1
pep8==1.6.2 pep8==1.7
py==1.4.31 py==1.4.31
pytest==2.8.5 pytest==2.8.5
pytest-cache==1.0
pytest-pep8==1.0.6 pytest-pep8==1.0.6
commands = commands =
{envpython} -m py.test -q --pep8 --ignore=tests --noconftest {envpython} -m py.test -q --pep8 --ignore=tests --noconftest
@ -187,7 +186,6 @@ deps =
mccabe==0.3.1 mccabe==0.3.1
py==1.4.31 py==1.4.31
pytest==2.8.5 pytest==2.8.5
pytest-cache==1.0
pytest-mccabe==0.1 pytest-mccabe==0.1
commands = commands =
{envpython} -m py.test -q --mccabe --ignore=tests --noconftest {envpython} -m py.test -q --mccabe --ignore=tests --noconftest