Merge remote-tracking branch 'source/master'
This commit is contained in:
commit
7ad871fab1
@ -14,4 +14,4 @@ install:
|
||||
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||
|
||||
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
1
.gitignore
vendored
@ -11,6 +11,7 @@ __pycache__
|
||||
/setuptools-*.egg
|
||||
/setuptools-*.zip
|
||||
/qutebrowser/git-commit-id
|
||||
/qutebrowser/3rdparty
|
||||
/doc/*.html
|
||||
/README.html
|
||||
/CHANGELOG.html
|
||||
|
@ -37,7 +37,7 @@ install:
|
||||
- python scripts/dev/ci_install.py
|
||||
|
||||
script:
|
||||
- tox -e $TESTENV -- -p no:sugar -v --cov-report term tests
|
||||
- tox -e $TESTENV -- -v --cov-report term tests
|
||||
|
||||
after_success:
|
||||
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
|
||||
|
@ -14,6 +14,31 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.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
|
||||
------
|
||||
|
||||
|
@ -152,9 +152,29 @@ $ nix-env -i qutebrowser
|
||||
On Windows
|
||||
----------
|
||||
|
||||
You can either use one of the
|
||||
https://github.com/The-Compiler/qutebrowser/releases[prebuilt standalone
|
||||
packages or MSI installers], or install manually:
|
||||
There are different ways to install qutebrowser on Windows:
|
||||
|
||||
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
|
||||
Python 3 (be sure to install pip).
|
||||
|
@ -160,6 +160,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* ZDarian
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Peter Vilim
|
||||
* Tarcisio Fedrizzi
|
||||
* Jonas Schürmann
|
||||
* Panagiotis Ktistakis
|
||||
* Jimmy
|
||||
@ -176,6 +177,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* jnphilipp
|
||||
* Tobias Patzl
|
||||
* Peter Michely
|
||||
* Link
|
||||
* Larry Hynes
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
@ -186,6 +188,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Corentin Jule
|
||||
* zwarag
|
||||
* xd1le
|
||||
* evan
|
||||
* dylan araps
|
||||
* Tim Harder
|
||||
* Thiago Barroso Perrotta
|
||||
|
@ -402,6 +402,8 @@ Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
|
||||
|
||||
Open a page from the clipboard.
|
||||
|
||||
If the pasted text contains newlines, each line gets opened in its own tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-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
|
||||
|<<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-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|
||||
|<<debug-console,debug-console>>|Show the debugging console.
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<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
|
||||
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.
|
||||
@ -1266,13 +1273,16 @@ Dump the current page's content to a file.
|
||||
|
||||
[[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.
|
||||
|
||||
==== positional arguments
|
||||
* +'s'+: The string to evaluate.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show the output in a new tab.
|
||||
|
||||
==== note
|
||||
* 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.
|
||||
|
@ -11,6 +11,9 @@ markers =
|
||||
not_xvfb: Tests which can't be run with Xvfb.
|
||||
frozen: Tests which can only be run if sys.frozen is True.
|
||||
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 =
|
||||
UnusedImport
|
||||
UnusedVariable
|
||||
@ -39,3 +42,6 @@ qt_log_ignore =
|
||||
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
||||
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
|
||||
^QXcbClipboard: SelectionRequest too old
|
||||
^QGeoclueMaster error creating GeoclueMasterClient\.
|
||||
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
|
||||
qt_wait_signal_raising = true
|
||||
|
@ -809,6 +809,9 @@ class CommandDispatcher:
|
||||
def paste(self, sel=False, tab=False, bg=False, window=False):
|
||||
"""Open a page from the clipboard.
|
||||
|
||||
If the pasted text contains newlines, each line gets opened in its own
|
||||
tab.
|
||||
|
||||
Args:
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
tab: Open in a new tab.
|
||||
@ -825,9 +828,15 @@ class CommandDispatcher:
|
||||
text = clipboard.text(mode)
|
||||
if not text:
|
||||
raise cmdexc.CommandError("{} is empty.".format(target))
|
||||
log.misc.debug("{} contained: '{}'".format(target, text))
|
||||
log.misc.debug("{} contained: '{}'".format(target,
|
||||
text.replace('\n', '\\n')))
|
||||
text_urls = enumerate(u for u in text.split('\n') if u)
|
||||
for i, text_url in text_urls:
|
||||
if not window and i > 0:
|
||||
tab = False
|
||||
bg = True
|
||||
try:
|
||||
url = urlutils.fuzzy_url(text)
|
||||
url = urlutils.fuzzy_url(text_url)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
self._open(url, tab, bg, window)
|
||||
@ -1462,11 +1471,11 @@ class CommandDispatcher:
|
||||
webview = self._current_widget()
|
||||
if not webview.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
act.append(QWebPage.MoveToPreviousChar)
|
||||
else:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if sys.platform == 'win32':
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
act.append(QWebPage.SelectPreviousChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
@ -1483,11 +1492,11 @@ class CommandDispatcher:
|
||||
webview = self._current_widget()
|
||||
if not webview.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if sys.platform != 'win32':
|
||||
if sys.platform != 'win32': # pragma: no branch
|
||||
act.append(QWebPage.MoveToNextChar)
|
||||
else:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if sys.platform != 'win32':
|
||||
if sys.platform != 'win32': # pragma: no branch
|
||||
act.append(QWebPage.SelectNextChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
@ -1755,3 +1764,10 @@ class CommandDispatcher:
|
||||
|
||||
QApplication.postEvent(receiver, press_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()
|
||||
|
@ -484,8 +484,10 @@ class HintManager(QObject):
|
||||
mode = QClipboard.Selection if sel else QClipboard.Clipboard
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
QApplication.clipboard().setText(urlstr, mode)
|
||||
message.info(self._win_id, "URL yanked to {}".format(
|
||||
"primary selection" if sel else "clipboard"))
|
||||
msg = "Yanked URL to {}: {}".format(
|
||||
"primary selection" if sel else "clipboard",
|
||||
urlstr)
|
||||
message.info(self._win_id, msg)
|
||||
|
||||
def _run_cmd(self, url, context):
|
||||
"""Run the command based on a hint URL.
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
"""Our own QNetworkAccessManager."""
|
||||
|
||||
import os
|
||||
import collections
|
||||
import netrc
|
||||
|
||||
@ -29,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils)
|
||||
urlutils, debug)
|
||||
from qutebrowser.browser import cookies
|
||||
from qutebrowser.browser.network import qutescheme, networkreply
|
||||
from qutebrowser.browser.network import filescheme
|
||||
@ -62,6 +63,11 @@ class SslError(QSslError):
|
||||
except TypeError:
|
||||
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):
|
||||
|
||||
@ -189,7 +195,9 @@ class NetworkManager(QNetworkAccessManager):
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
if ssl_strict == 'ask':
|
||||
log.webview.debug("SSL errors {!r}, strict {}".format(
|
||||
errors, ssl_strict))
|
||||
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
except ValueError:
|
||||
@ -201,32 +209,37 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self._accepted_ssl_errors[host_tpl])
|
||||
is_rejected = set(errors).issubset(
|
||||
self._rejected_ssl_errors[host_tpl])
|
||||
if is_accepted:
|
||||
|
||||
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
|
||||
return
|
||||
elif is_accepted:
|
||||
reply.ignoreSslErrors()
|
||||
elif is_rejected:
|
||||
pass
|
||||
else:
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
||||
errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(
|
||||
err_string), mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
return
|
||||
|
||||
if ssl_strict == 'ask':
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
|
||||
mode=usertypes.PromptMode.yesno, owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
d = self._accepted_ssl_errors
|
||||
err_dict = self._accepted_ssl_errors
|
||||
else:
|
||||
d = self._rejected_ssl_errors
|
||||
err_dict = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
d[host_tpl] += errors
|
||||
elif ssl_strict:
|
||||
pass
|
||||
err_dict[host_tpl] += errors
|
||||
else:
|
||||
for err in errors:
|
||||
# FIXME we might want to use warn here (non-fatal error)
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/114
|
||||
message.error(self._win_id,
|
||||
'SSL error: {}'.format(err.errorString()))
|
||||
message.error(self._win_id, 'SSL error: {}'.format(
|
||||
err.errorString()))
|
||||
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)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
@ -244,7 +257,10 @@ class NetworkManager(QNetworkAccessManager):
|
||||
def on_authentication_required(self, reply, authenticator):
|
||||
"""Called when a website needs authentication."""
|
||||
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
|
||||
try:
|
||||
net = netrc.netrc()
|
||||
|
@ -258,6 +258,13 @@ class String(BaseType):
|
||||
self._basic_validation(value)
|
||||
if not value:
|
||||
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
|
||||
for c in self.forbidden):
|
||||
raise configexc.ValidationError(value, "may not contain the chars "
|
||||
@ -270,7 +277,10 @@ class String(BaseType):
|
||||
"long!".format(self.maxlen))
|
||||
|
||||
def complete(self):
|
||||
if self._completions is not None:
|
||||
return self._completions
|
||||
else:
|
||||
return super().complete()
|
||||
|
||||
|
||||
class List(BaseType):
|
||||
|
@ -417,9 +417,6 @@ class MainWindow(QWidget):
|
||||
window=self.win_id)
|
||||
download_count = download_manager.rowCount()
|
||||
quit_texts = []
|
||||
# Close if set to never ask for confirmation
|
||||
if 'never' in confirm_quit:
|
||||
pass
|
||||
# Ask if multiple-tabs are open
|
||||
if 'multiple-tabs' in confirm_quit and tab_count > 1:
|
||||
quit_texts.append("{} {} open.".format(
|
||||
|
@ -229,8 +229,9 @@ class IPCServer(QObject):
|
||||
log.ipc.debug("In on_error with None socket!")
|
||||
return
|
||||
self._timer.stop()
|
||||
log.ipc.debug("Socket error {}: {}".format(
|
||||
self._socket.error(), self._socket.errorString()))
|
||||
log.ipc.debug("Socket 0x{:x}: error {}: {}".format(
|
||||
id(self._socket), self._socket.error(),
|
||||
self._socket.errorString()))
|
||||
if err != QLocalSocket.PeerClosedError:
|
||||
raise SocketError("handling IPC connection", self._socket)
|
||||
|
||||
@ -241,13 +242,14 @@ class IPCServer(QObject):
|
||||
return
|
||||
if self._socket is not None:
|
||||
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
|
||||
socket = self._server.nextPendingConnection()
|
||||
if socket is None:
|
||||
log.ipc.debug("No new connection to handle.")
|
||||
return
|
||||
log.ipc.debug("Client connected.")
|
||||
log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
|
||||
self._timer.start()
|
||||
self._socket = socket
|
||||
socket.readyRead.connect(self.on_ready_read)
|
||||
@ -267,7 +269,8 @@ class IPCServer(QObject):
|
||||
@pyqtSlot()
|
||||
def on_disconnected(self):
|
||||
"""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()
|
||||
if self._socket is None:
|
||||
log.ipc.debug("In on_disconnected with None socket!")
|
||||
@ -279,7 +282,8 @@ class IPCServer(QObject):
|
||||
|
||||
def _handle_invalid_data(self):
|
||||
"""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._socket.error.connect(self.on_error)
|
||||
self._socket.disconnectFromServer()
|
||||
@ -292,11 +296,12 @@ class IPCServer(QObject):
|
||||
# active for some reason.
|
||||
log.ipc.warning("In on_ready_read with None socket!")
|
||||
return
|
||||
self._timer.start()
|
||||
self._timer.stop()
|
||||
while self._socket is not None and self._socket.canReadLine():
|
||||
data = bytes(self._socket.readLine())
|
||||
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:
|
||||
decoded = data.decode('utf-8')
|
||||
@ -337,11 +342,13 @@ class IPCServer(QObject):
|
||||
|
||||
cwd = json_data.get('cwd', None)
|
||||
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
|
||||
self._timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_timeout(self):
|
||||
"""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()
|
||||
if self._socket is not None: # pragma: no cover
|
||||
# on_socket_disconnected sets it to None
|
||||
@ -369,7 +376,8 @@ class IPCServer(QObject):
|
||||
|
||||
def shutdown(self):
|
||||
"""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:
|
||||
self._socket.deleteLater()
|
||||
self._socket = None
|
||||
|
@ -35,6 +35,8 @@ from qutebrowser.config import style
|
||||
from qutebrowser.misc import consolewidget
|
||||
|
||||
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')
|
||||
@ -176,18 +178,23 @@ def debug_trace(expr=""):
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
Args:
|
||||
s: The string to evaluate.
|
||||
quiet: Don't show the output in a new tab.
|
||||
"""
|
||||
try:
|
||||
r = eval(s)
|
||||
out = repr(r)
|
||||
except Exception:
|
||||
out = traceback.format_exc()
|
||||
|
||||
qutescheme.pyeval_output = out
|
||||
if quiet:
|
||||
log.misc.debug("pyeval output: {}".format(out))
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
||||
|
@ -168,6 +168,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
Return:
|
||||
A target QUrl to a search page or the original URL.
|
||||
"""
|
||||
urlstr = urlstr.strip()
|
||||
expanded = os.path.expanduser(urlstr)
|
||||
if os.path.isabs(expanded):
|
||||
path = expanded
|
||||
@ -181,11 +182,10 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
else:
|
||||
path = None
|
||||
|
||||
stripped = urlstr.strip()
|
||||
if path is not None and os.path.exists(path):
|
||||
log.url.debug("URL is a local file")
|
||||
url = QUrl.fromLocalFile(path)
|
||||
elif (not do_search) or is_url(stripped):
|
||||
elif (not do_search) or is_url(urlstr):
|
||||
# probably an address
|
||||
log.url.debug("URL is a fuzzy address")
|
||||
url = qurl_from_user_input(urlstr)
|
||||
@ -194,7 +194,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
||||
try:
|
||||
url = _get_search_url(urlstr)
|
||||
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(
|
||||
urlstr, url.toDisplayString()))
|
||||
if do_search and config.get('general', 'auto-search'):
|
||||
|
@ -1,10 +1,8 @@
|
||||
Jinja2==2.8.0
|
||||
MarkupSafe==0.23
|
||||
Pygments==2.0.2
|
||||
Pygments==2.1
|
||||
pyPEG2==2.15.2
|
||||
PyYAML==3.11
|
||||
# "ValueError: I/O operation on closed file" with pytest since 0.3.5
|
||||
# WORKAROUND for https://github.com/tartley/colorama/issues/81
|
||||
colorama==0.3.3 # rq.filter: <=0.3.3
|
||||
colorama==0.3.6
|
||||
colorlog==2.6.0
|
||||
cssutils==1.0.1
|
||||
|
@ -100,6 +100,8 @@ PERFECT_FILES = [
|
||||
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_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',
|
||||
'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):
|
||||
|
||||
"""Exception raised when skipping coverage checks."""
|
||||
@ -199,7 +205,8 @@ def check(fileobj, perfect_files):
|
||||
text = "{} has {}% line and {}% branch coverage!".format(
|
||||
filename, line_cov, branch_cov)
|
||||
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 "
|
||||
"perfect_files!".format(filename))
|
||||
messages.append(Message(MsgType.perfect_file, text))
|
||||
|
@ -39,13 +39,16 @@ from helpers.messagemock import message_mock
|
||||
from qutebrowser.config import config
|
||||
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
|
||||
import xvfbwrapper
|
||||
|
||||
|
||||
# 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):
|
||||
@ -62,6 +65,9 @@ def _apply_platform_markers(item):
|
||||
"Can only run when frozen"),
|
||||
('not_xvfb', item.config.xvfb_display is not None,
|
||||
"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:
|
||||
@ -108,7 +114,7 @@ def pytest_collection_modifyitems(items):
|
||||
item.add_marker('gui')
|
||||
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
|
||||
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!")
|
||||
skip_marker = pytest.mark.skipif(
|
||||
True, reason="No DISPLAY available")
|
||||
@ -124,6 +130,8 @@ def pytest_collection_modifyitems(items):
|
||||
item.add_marker(pytest.mark.integration)
|
||||
|
||||
_apply_platform_markers(item)
|
||||
if item.get_marker('xfail_norun'):
|
||||
item.add_marker(pytest.mark.xfail(run=False))
|
||||
|
||||
|
||||
def pytest_ignore_collect(path):
|
||||
@ -161,6 +169,41 @@ class WinRegistryHelper:
|
||||
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
|
||||
def win_registry():
|
||||
"""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) == '':
|
||||
# xvfbwrapper doesn't handle DISPLAY="" correctly
|
||||
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
|
||||
try:
|
||||
disp = xvfbwrapper.Xvfb(width=800, height=600, colordepth=16)
|
||||
|
@ -21,6 +21,6 @@
|
||||
|
||||
"""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 testprocess import pytest_runtest_makereport
|
||||
|
@ -5,7 +5,7 @@
|
||||
<title>Caret mode</title>
|
||||
</head>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -11,8 +11,10 @@ body .c { color: #408080; font-style: italic } /* Comment */
|
||||
body .err { border: 1px solid #FF0000 } /* Error */
|
||||
body .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
body .o { color: #666666 } /* Operator */
|
||||
body .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
|
||||
body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
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 .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
body .gd { color: #A00000 } /* Generic.Deleted */
|
||||
@ -75,8 +77,8 @@ body .il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||
<h2></h2>
|
||||
|
||||
<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"><html><head></head><body><pre</span> <span class="na">style=</span><span class="s">"word-wrap: break-word; white-space: pre-wrap;"</span><span class="nt">></span>Hello World!
|
||||
<span class="nt"></pre></body></html></span>
|
||||
2</pre></div></td><td class="code"><div class="highlight"><pre><span class="p"><</span><span class="nt">html</span><span class="p">><</span><span class="nt">head</span><span class="p">></</span><span class="nt">head</span><span class="p">><</span><span class="nt">body</span><span class="p">><</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">></span>Hello World!
|
||||
<span class="p"></</span><span class="nt">pre</span><span class="p">></</span><span class="nt">body</span><span class="p">></</span><span class="nt">html</span><span class="p">></span>
|
||||
</pre></div>
|
||||
</td></tr></tbody></table>
|
||||
|
||||
|
1
tests/integration/data/numbers/10.txt
Normal file
1
tests/integration/data/numbers/10.txt
Normal file
@ -0,0 +1 @@
|
||||
ten
|
1
tests/integration/data/numbers/11.txt
Normal file
1
tests/integration/data/numbers/11.txt
Normal file
@ -0,0 +1 @@
|
||||
eleven
|
1
tests/integration/data/numbers/12.txt
Normal file
1
tests/integration/data/numbers/12.txt
Normal file
@ -0,0 +1 @@
|
||||
twelve
|
1
tests/integration/data/numbers/13.txt
Normal file
1
tests/integration/data/numbers/13.txt
Normal file
@ -0,0 +1 @@
|
||||
thirteen
|
1
tests/integration/data/numbers/14.txt
Normal file
1
tests/integration/data/numbers/14.txt
Normal file
@ -0,0 +1 @@
|
||||
fourteen
|
1
tests/integration/data/numbers/8.txt
Normal file
1
tests/integration/data/numbers/8.txt
Normal file
@ -0,0 +1 @@
|
||||
eight
|
1
tests/integration/data/numbers/9.txt
Normal file
1
tests/integration/data/numbers/9.txt
Normal file
@ -0,0 +1 @@
|
||||
nine
|
38
tests/integration/data/prompt/geolocation.html
Normal file
38
tests/integration/data/prompt/geolocation.html
Normal 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>
|
15
tests/integration/data/prompt/jsalert.html
Normal file
15
tests/integration/data/prompt/jsalert.html
Normal 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>
|
15
tests/integration/data/prompt/jsconfirm.html
Normal file
15
tests/integration/data/prompt/jsconfirm.html
Normal 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>
|
15
tests/integration/data/prompt/jsprompt.html
Normal file
15
tests/integration/data/prompt/jsprompt.html
Normal 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>
|
39
tests/integration/data/prompt/notifications.html
Normal file
39
tests/integration/data/prompt/notifications.html
Normal 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>
|
0
tests/integration/data/reload.txt
Normal file
0
tests/integration/data/reload.txt
Normal file
21
tests/integration/data/search.html
Normal file
21
tests/integration/data/search.html
Normal 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>
|
17
tests/integration/data/ssl/cert.csr
Normal file
17
tests/integration/data/ssl/cert.csr
Normal 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-----
|
20
tests/integration/data/ssl/cert.pem
Normal file
20
tests/integration/data/ssl/cert.pem
Normal 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-----
|
27
tests/integration/data/ssl/key.pem
Normal file
27
tests/integration/data/ssl/key.pem
Normal 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-----
|
30
tests/integration/data/ssl/privkey.pem
Normal file
30
tests/integration/data/ssl/privkey.pem
Normal 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-----
|
@ -92,6 +92,13 @@ Feature: Going back and forward.
|
||||
- url: http://localhost:*/data/backforward/2.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.
|
||||
Given I open data/backforward/1.txt
|
||||
When I run :back with count 99999999999
|
||||
@ -132,3 +139,11 @@ Feature: Going back and forward.
|
||||
Given I open data/backforward/1.txt
|
||||
When I run :forward
|
||||
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
|
||||
|
@ -3,7 +3,7 @@ Feature: Caret mode
|
||||
|
||||
Background:
|
||||
Given I open data/caret.html
|
||||
And I run :enter-mode caret
|
||||
And I run :tab-only ;; :enter-mode caret
|
||||
|
||||
# document
|
||||
|
||||
@ -258,3 +258,63 @@ Feature: Caret mode
|
||||
Then the message "3 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"
|
||||
|
||||
# :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)
|
||||
|
@ -25,6 +25,7 @@ import json
|
||||
import os.path
|
||||
import logging
|
||||
import collections
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
@ -35,6 +36,11 @@ from PyQt5.QtGui import QClipboard
|
||||
from helpers import utils
|
||||
|
||||
|
||||
class WaitForClipboardTimeout(Exception):
|
||||
|
||||
"""Raised when _wait_for_clipboard didn't get the expected message."""
|
||||
|
||||
|
||||
def _clipboard_mode(qapp, what):
|
||||
"""Get the QClipboard::Mode to use based on a string."""
|
||||
if what == 'clipboard':
|
||||
@ -68,6 +74,7 @@ def open_path_given(quteproc, path):
|
||||
It always opens a new tab, unlike "When I open ..."
|
||||
"""
|
||||
quteproc.open_path(path, new_tab=True)
|
||||
quteproc.wait_for_load_finished(path)
|
||||
|
||||
|
||||
@bdd.given(bdd.parsers.parse("I run {command}"))
|
||||
@ -93,17 +100,32 @@ def fresh_instance(quteproc):
|
||||
def open_path(quteproc, path):
|
||||
"""Open a URL.
|
||||
|
||||
If used like "When I open ... in a new tab", the URL is opened ina new
|
||||
tab.
|
||||
If used like "When I open ... in a new tab", the URL is opened in a new
|
||||
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_window_suffix = ' in a new window'
|
||||
do_not_wait_suffix = ' without waiting'
|
||||
|
||||
if path.endswith(new_tab_suffix):
|
||||
path = path[:-len(new_tab_suffix)]
|
||||
new_tab = True
|
||||
else:
|
||||
new_tab = False
|
||||
elif path.endswith(new_window_suffix):
|
||||
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}"))
|
||||
@ -131,7 +153,7 @@ def run_command(quteproc, httpbin, command):
|
||||
@bdd.when(bdd.parsers.parse("I reload"))
|
||||
def reload(qtbot, httpbin, quteproc, command):
|
||||
"""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')
|
||||
|
||||
|
||||
@ -142,8 +164,9 @@ def wait_until_loaded(quteproc, path):
|
||||
|
||||
|
||||
@bdd.when(bdd.parsers.re(r'I wait for (?P<is_regex>regex )?"'
|
||||
r'(?P<pattern>[^"]+)" in the log'))
|
||||
def wait_in_log(quteproc, is_regex, pattern):
|
||||
r'(?P<pattern>[^"]+)" in the log(?P<do_skip> or skip '
|
||||
r'the test)?'))
|
||||
def wait_in_log(quteproc, is_regex, pattern, do_skip):
|
||||
"""Wait for a given pattern in the qutebrowser log.
|
||||
|
||||
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:
|
||||
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) '
|
||||
@ -191,11 +216,33 @@ def fill_clipboard(qtbot, qapp, httpbin, what, content):
|
||||
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
|
||||
|
||||
|
||||
@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."""
|
||||
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."""
|
||||
if is_regex:
|
||||
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'))
|
||||
@ -256,8 +304,7 @@ def ensure_not_logged(quteproc, pattern):
|
||||
'logged'))
|
||||
def javascript_message_logged(quteproc, message):
|
||||
"""Make sure the given message was logged via javascript."""
|
||||
quteproc.wait_for(category='js', function='javaScriptConsoleMessage',
|
||||
message='[*] {}'.format(message))
|
||||
quteproc.wait_for_js(message)
|
||||
|
||||
|
||||
@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__)), '..',
|
||||
'data', os.path.join(*filename.split('/')))
|
||||
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}"))
|
||||
@ -335,6 +399,7 @@ def check_open_tabs(quteproc, tabs):
|
||||
|
||||
for i, line in enumerate(tabs):
|
||||
line = line.strip()
|
||||
assert line.startswith('- ')
|
||||
line = line[2:] # remove "- " prefix
|
||||
if line.endswith(active_suffix):
|
||||
path = line[:-len(active_suffix)]
|
||||
@ -359,16 +424,27 @@ def _wait_for_clipboard(qtbot, clipboard, mode, expected):
|
||||
while True:
|
||||
if clipboard.text(mode=mode) == expected:
|
||||
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
|
||||
if not blocker.signal_triggered or timer.hasExpired(timeout):
|
||||
|
||||
if timer.hasExpired(timeout):
|
||||
mode_names = {
|
||||
QClipboard.Clipboard: 'clipboard',
|
||||
QClipboard.Selection: 'primary selection',
|
||||
}
|
||||
raise WaitForTimeout(
|
||||
"Timed out after {}ms waiting for {} in {}.".format(
|
||||
timeout, expected, mode_names[mode]))
|
||||
raise WaitForClipboardTimeout(
|
||||
"Timed out after {timeout}ms waiting for {what}:\n"
|
||||
" 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 '
|
||||
@ -382,6 +458,13 @@ def clipboard_contains(qtbot, qapp, httpbin, what, content):
|
||||
|
||||
@bdd.then(bdd.parsers.parse('the clipboard should contain:\n{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,
|
||||
expected)
|
||||
|
||||
|
||||
@bdd.then("qutebrowser should quit")
|
||||
def should_quit(qtbot, quteproc):
|
||||
quteproc.exit_expected = True
|
||||
with qtbot.waitSignal(quteproc.proc.finished, timeout=5000):
|
||||
pass
|
||||
|
@ -117,8 +117,25 @@ Feature: Various utility commands.
|
||||
And I wait for "Focus object changed: *" in the log
|
||||
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
|
||||
|
||||
# WORKAROUND for https://bitbucket.org/cherrypy/cherrypy/pull-requests/117/
|
||||
@not_osx
|
||||
Scenario: :stop
|
||||
Given I have a fresh instance
|
||||
@ -135,13 +152,19 @@ Feature: Various utility commands.
|
||||
custom/redirect-later?delay=-1
|
||||
# no request on / because we stopped the redirect
|
||||
|
||||
Scenario: :reload
|
||||
Scenario: :stop with wrong count
|
||||
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 wait until data/hello.txt is loaded
|
||||
And I wait until data/reload.txt is loaded
|
||||
Then the requests should be:
|
||||
data/hello.txt
|
||||
data/hello.txt
|
||||
data/reload.txt
|
||||
data/reload.txt
|
||||
|
||||
Scenario: :reload with force
|
||||
When I open headers
|
||||
@ -149,6 +172,12 @@ Feature: Various utility commands.
|
||||
And I wait until headers is loaded
|
||||
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
|
||||
|
||||
Scenario: :view-source
|
||||
@ -260,3 +289,42 @@ Feature: Various utility commands.
|
||||
And I set storage -> prompt-download-directory to false
|
||||
And I open data/misc/test.pdf
|
||||
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
|
||||
|
195
tests/integration/features/prompts.feature
Normal file
195
tests/integration/features/prompts.feature
Normal 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"
|
||||
}
|
187
tests/integration/features/search.feature
Normal file
187
tests/integration/features/search.feature
Normal 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
|
@ -533,3 +533,105 @@ Feature: Tab management
|
||||
- tabs:
|
||||
- history:
|
||||
- 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
|
||||
|
50
tests/integration/features/test_prompts.py
Normal file
50
tests/integration/features/test_prompts.py
Normal 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)')
|
26
tests/integration/features/test_search.py
Normal file
26
tests/integration/features/test_search.py
Normal 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')
|
@ -26,20 +26,49 @@ from helpers import utils
|
||||
bdd.scenarios('urlmarks.feature')
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse('the bookmark file should contain "{expected}"'))
|
||||
def bookmark_file_contains(quteproc, expected):
|
||||
bookmark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks',
|
||||
def _check_marks(quteproc, quickmarks, expected, contains):
|
||||
"""Make sure the given line does (not) exist in the 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')
|
||||
|
||||
quteproc.clear_data() # So we don't match old messages
|
||||
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()
|
||||
|
||||
matched_line = any(
|
||||
utils.pattern_match(pattern=expected, value=line.rstrip('\n'))
|
||||
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)
|
||||
|
@ -33,7 +33,7 @@ def skip_with_broken_clipboard(qtbot, qapp):
|
||||
"""
|
||||
clipboard = qapp.clipboard()
|
||||
|
||||
with qtbot.waitSignal(clipboard.changed):
|
||||
with qtbot.waitSignal(clipboard.changed, raising=False):
|
||||
clipboard.setText("Does this work?")
|
||||
|
||||
if clipboard.text() != "Does this work?":
|
||||
|
@ -1,7 +1,174 @@
|
||||
Feature: quickmarks and bookmarks
|
||||
|
||||
## bookmarks
|
||||
|
||||
Scenario: Saving a bookmark
|
||||
When I open data/title.html
|
||||
And I run :bookmark-add
|
||||
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"
|
||||
|
||||
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 "
|
||||
|
@ -98,3 +98,75 @@ Feature: Yanking and pasting.
|
||||
history:
|
||||
- active: true
|
||||
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
|
||||
|
@ -55,3 +55,7 @@ Feature: Zooming in and out
|
||||
Scenario: Setting zoom with very big count
|
||||
When I run :zoom with count 99999999999
|
||||
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
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
"""Fixtures to run qutebrowser in a QProcess and communicate."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@ -35,6 +36,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
import testprocess
|
||||
from qutebrowser.misc import ipc
|
||||
from qutebrowser.utils import log, utils
|
||||
from helpers import utils as testutils
|
||||
|
||||
|
||||
def is_ignored_qt_message(message):
|
||||
@ -169,11 +171,6 @@ class QuteProc(testprocess.Process):
|
||||
else:
|
||||
raise
|
||||
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issues/717/
|
||||
# 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 = (
|
||||
@ -197,7 +194,7 @@ class QuteProc(testprocess.Process):
|
||||
log_line.function == 'init' and
|
||||
log_line.message.startswith('Base directory:')):
|
||||
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()
|
||||
|
||||
return log_line
|
||||
@ -214,7 +211,7 @@ class QuteProc(testprocess.Process):
|
||||
'about:blank']
|
||||
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.
|
||||
|
||||
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:'):
|
||||
return path
|
||||
else:
|
||||
return 'http://localhost:{}/{}'.format(
|
||||
self._httpbin.port,
|
||||
return '{}://localhost:{}/{}'.format(
|
||||
'https' if https else 'http',
|
||||
self._httpbin.port if port is None else port,
|
||||
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):
|
||||
bad_msgs = [msg for msg in self._data
|
||||
if msg.loglevel > logging.INFO and not msg.expected]
|
||||
super().after_test()
|
||||
if self._is_error_logline(msg) and not msg.expected]
|
||||
|
||||
try:
|
||||
if bad_msgs:
|
||||
text = 'Logged unexpected errors:\n\n' + '\n'.join(
|
||||
str(e) for e in bad_msgs)
|
||||
pytest.fail(text, pytrace=False)
|
||||
# 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):
|
||||
"""Send a command to the running qutebrowser instance."""
|
||||
@ -269,14 +301,19 @@ class QuteProc(testprocess.Process):
|
||||
yield
|
||||
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."""
|
||||
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:
|
||||
self.send_cmd(':open -t ' + url)
|
||||
elif new_window:
|
||||
self.send_cmd(':open -w ' + url)
|
||||
else:
|
||||
self.send_cmd(':open ' + url)
|
||||
self.wait_for_load_finished(path)
|
||||
|
||||
def mark_expected(self, category=None, loglevel=None, message=None):
|
||||
"""Mark a given logging message as expected."""
|
||||
@ -284,16 +321,24 @@ class QuteProc(testprocess.Process):
|
||||
message=message)
|
||||
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."""
|
||||
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
|
||||
# __repr__
|
||||
url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100)
|
||||
pattern = re.compile(
|
||||
r"(load status for <qutebrowser.browser.webview.WebView "
|
||||
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "
|
||||
r"PyQt5.QtCore.QUrl\('{url}'\) -> .*)".format(url=re.escape(url)))
|
||||
r"(load status for <qutebrowser\.browser\.webview\.WebView "
|
||||
r"tab_id=\d+ url='{url}'>: LoadStatus\.{load_status}|fetch: "
|
||||
r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format(
|
||||
load_status=re.escape(load_status), url=re.escape(url)))
|
||||
self.wait_for(message=pattern, timeout=timeout)
|
||||
|
||||
def get_session(self):
|
||||
|
@ -87,7 +87,10 @@ def test_mhtml(test_name, download_dir, quteproc, httpbin):
|
||||
'data', 'downloads', 'mhtml', 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,
|
||||
'{}-downloaded.mht'.format(test_name))
|
||||
|
||||
|
@ -29,22 +29,54 @@ import testprocess
|
||||
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."""
|
||||
with qtbot.waitSignal(quteproc.got_error, raising=True):
|
||||
quteproc.send_cmd(':message-error test')
|
||||
with qtbot.waitSignal(quteproc.got_error):
|
||||
quteproc.send_cmd(cmd)
|
||||
# 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.
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
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):
|
||||
"""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"')
|
||||
|
||||
|
||||
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', [
|
||||
(
|
||||
# Normal message
|
||||
|
@ -74,6 +74,29 @@ class PythonProcess(testprocess.Process):
|
||||
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
|
||||
def pyproc():
|
||||
proc = PythonProcess()
|
||||
@ -81,6 +104,35 @@ def pyproc():
|
||||
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:
|
||||
|
||||
def test_successful(self, pyproc):
|
||||
@ -144,6 +196,13 @@ class TestWaitFor:
|
||||
with pytest.raises(TypeError):
|
||||
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:
|
||||
|
||||
|
@ -33,7 +33,7 @@ import pytest
|
||||
('/data/hello.txt', 'Hello World!', True),
|
||||
])
|
||||
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)
|
||||
try:
|
||||
response = urllib.request.urlopen(url)
|
||||
|
@ -72,11 +72,21 @@ class Line:
|
||||
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)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""Add qutebrowser/httpbin sections to captured output if a test failed."""
|
||||
outcome = yield
|
||||
if call.when != 'call':
|
||||
if call.when not in ['call', 'teardown']:
|
||||
return
|
||||
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.
|
||||
return
|
||||
|
||||
# pylint: disable=no-member
|
||||
if pytest.config.getoption('--capture') == 'no':
|
||||
# Already printed live
|
||||
return
|
||||
|
||||
if quteproc_log is not None:
|
||||
report.longrepr.addsection("qutebrowser output",
|
||||
'\n'.join(quteproc_log))
|
||||
_render_log(quteproc_log))
|
||||
if httpbin_log is not None:
|
||||
report.longrepr.addsection("httpbin output",
|
||||
'\n'.join(httpbin_log))
|
||||
report.longrepr.addsection("httpbin output", _render_log(httpbin_log))
|
||||
|
||||
|
||||
class Process(QObject):
|
||||
@ -109,6 +123,7 @@ class Process(QObject):
|
||||
_invalid: A list of lines which could not be parsed.
|
||||
_data: A list of parsed lines.
|
||||
proc: The QProcess for the underlying process.
|
||||
exit_expected: Whether the process is expected to quit.
|
||||
|
||||
Signals:
|
||||
ready: Emitted when the server finished starting up.
|
||||
@ -126,9 +141,13 @@ class Process(QObject):
|
||||
self._data = []
|
||||
self.proc = QProcess()
|
||||
self.proc.setReadChannel(QProcess.StandardError)
|
||||
self.exit_expected = False
|
||||
|
||||
def _log(self, line):
|
||||
"""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)
|
||||
|
||||
def _parse_line(self, line):
|
||||
@ -225,8 +244,9 @@ class Process(QObject):
|
||||
raise InvalidLine(self._invalid)
|
||||
|
||||
self.clear_data()
|
||||
if not self.is_running():
|
||||
if not self.is_running() and not self.exit_expected:
|
||||
raise ProcessExited
|
||||
self.exit_expected = False
|
||||
|
||||
def clear_data(self):
|
||||
"""Clear the collected data."""
|
||||
@ -287,7 +307,41 @@ class Process(QObject):
|
||||
return line
|
||||
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.
|
||||
|
||||
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.
|
||||
override_waited_for: If set, gets triggered by previous messages
|
||||
again.
|
||||
do_skip: If set, call pytest.skip on a timeout.
|
||||
|
||||
Return:
|
||||
The matched line.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
if timeout is None:
|
||||
if 'CI' in os.environ:
|
||||
if do_skip:
|
||||
timeout = 2000
|
||||
elif 'CI' in os.environ:
|
||||
timeout = 15000
|
||||
else:
|
||||
timeout = 5000
|
||||
@ -324,27 +382,20 @@ class Process(QObject):
|
||||
elapsed_timer.start()
|
||||
|
||||
while True:
|
||||
# Skip if there are pending messages causing a skip
|
||||
self._maybe_skip()
|
||||
got_signal = spy.wait(timeout)
|
||||
if not got_signal or elapsed_timer.hasExpired(timeout):
|
||||
raise WaitForTimeout("Timed out after {}ms waiting for "
|
||||
"{!r}.".format(timeout, kwargs))
|
||||
msg = "Timed out after {}ms waiting for {!r}.".format(
|
||||
timeout, kwargs)
|
||||
if do_skip:
|
||||
pytest.skip(msg)
|
||||
else:
|
||||
raise WaitForTimeout(msg)
|
||||
|
||||
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
|
||||
match = self._wait_for_match(spy, kwargs)
|
||||
if match is not None:
|
||||
return match
|
||||
|
||||
def ensure_not_logged(self, delay=500, **kwargs):
|
||||
"""Make sure the data matching the given arguments is not logged.
|
||||
|
@ -19,13 +19,15 @@
|
||||
|
||||
"""Fixtures for the httpbin webserver."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import os.path
|
||||
import http.client
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
|
||||
import testprocess
|
||||
|
||||
@ -54,13 +56,23 @@ class Request(testprocess.Line):
|
||||
self.path = '/' if path == '/' else path.rstrip('/')
|
||||
|
||||
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:
|
||||
assert self.status == 404
|
||||
else:
|
||||
assert self.status < 400
|
||||
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
|
||||
expected_statuses = path_to_statuses.get(sanitized, [http.client.OK])
|
||||
assert self.status in expected_statuses
|
||||
|
||||
def __eq__(self, other):
|
||||
return NotImplemented
|
||||
@ -94,7 +106,7 @@ class ExpectedRequest:
|
||||
.format(self.verb, self.path))
|
||||
|
||||
|
||||
class HTTPBin(testprocess.Process):
|
||||
class WebserverProcess(testprocess.Process):
|
||||
|
||||
"""Abstraction over a running HTTPbin server process.
|
||||
|
||||
@ -110,8 +122,9 @@ class HTTPBin(testprocess.Process):
|
||||
|
||||
KEYS = ['verb', 'path']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, script, parent=None):
|
||||
super().__init__(parent)
|
||||
self._script = script
|
||||
self.port = self._get_port()
|
||||
self.new_data.connect(self.new_request)
|
||||
|
||||
@ -130,8 +143,9 @@ class HTTPBin(testprocess.Process):
|
||||
|
||||
def _parse_line(self, line):
|
||||
self._log(line)
|
||||
if line == (' * Running on http://127.0.0.1:{}/ (Press CTRL+C to '
|
||||
'quit)'.format(self.port)):
|
||||
started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/ '
|
||||
r'\(Press CTRL\+C to quit\)'.format(self.port))
|
||||
if started_re.fullmatch(line):
|
||||
self.ready.emit()
|
||||
return None
|
||||
return Request(line)
|
||||
@ -139,12 +153,12 @@ class HTTPBin(testprocess.Process):
|
||||
def _executable_args(self):
|
||||
if hasattr(sys, 'frozen'):
|
||||
executable = os.path.join(os.path.dirname(sys.executable),
|
||||
'webserver_sub')
|
||||
self._script)
|
||||
args = [str(self.port)]
|
||||
else:
|
||||
executable = sys.executable
|
||||
py_file = os.path.join(os.path.dirname(__file__),
|
||||
'webserver_sub.py')
|
||||
self._script + '.py')
|
||||
args = [py_file, str(self.port)]
|
||||
return executable, args
|
||||
|
||||
@ -157,7 +171,7 @@ class HTTPBin(testprocess.Process):
|
||||
@pytest.yield_fixture(scope='session', autouse=True)
|
||||
def httpbin(qapp):
|
||||
"""Fixture for a httpbin object which ensures clean setup/teardown."""
|
||||
httpbin = HTTPBin()
|
||||
httpbin = WebserverProcess('webserver_sub')
|
||||
httpbin.start()
|
||||
yield httpbin
|
||||
httpbin.cleanup()
|
||||
@ -169,3 +183,18 @@ def httpbin_after_test(httpbin, request):
|
||||
request.node._httpbin_log = httpbin.captured_log
|
||||
yield
|
||||
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()
|
||||
|
66
tests/integration/webserver_sub_ssl.py
Normal file
66
tests/integration/webserver_sub_ssl.py
Normal 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()
|
@ -53,7 +53,7 @@ class TestFixedDataNetworkReply:
|
||||
def test_data(self, qtbot, req, data):
|
||||
reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo')
|
||||
with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead,
|
||||
reply.finished], raising=True):
|
||||
reply.finished]):
|
||||
pass
|
||||
|
||||
assert reply.bytesAvailable() == len(data)
|
||||
@ -78,7 +78,7 @@ def test_error_network_reply(qtbot, req):
|
||||
reply = networkreply.ErrorNetworkReply(
|
||||
req, "This is an error", QNetworkReply.UnknownNetworkError)
|
||||
|
||||
with qtbot.waitSignals([reply.error, reply.finished], raising=True):
|
||||
with qtbot.waitSignals([reply.error, reply.finished]):
|
||||
pass
|
||||
|
||||
reply.abort() # shouldn't do anything
|
||||
|
@ -22,7 +22,6 @@
|
||||
from unittest import mock
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkCookie
|
||||
from PyQt5.QtTest import QSignalSpy
|
||||
from PyQt5.QtCore import QUrl
|
||||
import pytest
|
||||
|
||||
@ -79,7 +78,7 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch):
|
||||
ram_jar = cookies.RAMCookieJar()
|
||||
cookie = QNetworkCookie(b'foo', b'bar')
|
||||
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 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
|
||||
|
||||
|
||||
def test_set_cookies_never_accept(config_stub):
|
||||
def test_set_cookies_never_accept(qtbot, config_stub):
|
||||
"""Test setCookiesFromUrl when cookies are not accepted."""
|
||||
config_stub.data = CONFIG_NEVER_COOKIES
|
||||
ram_jar = cookies.RAMCookieJar()
|
||||
changed_signal_spy = QSignalSpy(ram_jar.changed)
|
||||
|
||||
url = QUrl('http://example.com/')
|
||||
|
||||
with qtbot.assertNotEmitted(ram_jar.changed):
|
||||
assert not ram_jar.setCookiesFromUrl(url, 'test')
|
||||
assert not changed_signal_spy
|
||||
assert not ram_jar.cookiesForUrl(url)
|
||||
|
||||
|
||||
@ -151,21 +150,10 @@ def test_cookies_changed_emit(config_stub, fake_save_manager,
|
||||
'LineParser', LineparserSaveStub)
|
||||
jar = cookies.CookieJar()
|
||||
|
||||
with qtbot.waitSignal(jar.changed, raising=True):
|
||||
with qtbot.waitSignal(jar.changed):
|
||||
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', [
|
||||
(True, False),
|
||||
(False, True)
|
||||
|
@ -628,7 +628,7 @@ class TestJavascriptEscape:
|
||||
with open(path, encoding='utf-8') as f:
|
||||
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)
|
||||
assert blocker.args == [True]
|
||||
|
||||
|
@ -79,8 +79,7 @@ def test_command(qtbot, py_proc, runner):
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as f:
|
||||
f.write('foo\n')
|
||||
""")
|
||||
with qtbot.waitSignal(runner.got_cmd, raising=True,
|
||||
timeout=10000) as blocker:
|
||||
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||
runner.run(cmd, *args)
|
||||
assert blocker.args == ['foo']
|
||||
|
||||
@ -100,8 +99,7 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
|
||||
f.write('\n')
|
||||
""")
|
||||
|
||||
with qtbot.waitSignal(runner.got_cmd, raising=True,
|
||||
timeout=10000) as blocker:
|
||||
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||
runner.run(cmd, *args, env=env)
|
||||
|
||||
data = blocker.args[0]
|
||||
@ -136,9 +134,8 @@ def test_temporary_files(qtbot, tmpdir, py_proc, runner):
|
||||
f.write('\n')
|
||||
""")
|
||||
|
||||
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000):
|
||||
with qtbot.waitSignal(runner.got_cmd, raising=True,
|
||||
timeout=10000) as blocker:
|
||||
with qtbot.waitSignal(runner.finished, timeout=10000):
|
||||
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||
runner.run(cmd, *args, env=env)
|
||||
|
||||
data = blocker.args[0]
|
||||
@ -160,7 +157,7 @@ def test_command_with_error(qtbot, tmpdir, py_proc, runner):
|
||||
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)
|
||||
|
||||
assert not text_file.exists()
|
||||
@ -191,14 +188,13 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner):
|
||||
""")
|
||||
args.append(str(pidfile))
|
||||
|
||||
with qtbot.waitSignal(watcher.directoryChanged, raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
|
||||
runner.run(cmd, *args, env=env)
|
||||
|
||||
# Make sure the PID was written to the file, not just the file created
|
||||
time.sleep(0.5)
|
||||
|
||||
with qtbot.waitSignal(runner.finished, raising=True):
|
||||
with qtbot.waitSignal(runner.finished):
|
||||
os.kill(int(pidfile.read()), signal.SIGTERM)
|
||||
|
||||
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 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)})
|
||||
|
||||
assert len(caplog.records) == 1
|
||||
|
@ -305,6 +305,8 @@ class TestString:
|
||||
({'minlen': 2}, 'fo'),
|
||||
({'minlen': 2, 'maxlen': 3}, 'fo'),
|
||||
({'minlen': 2, 'maxlen': 3}, 'foo'),
|
||||
# valid_values
|
||||
({'valid_values': configtypes.ValidValues('fooo')}, 'fooo'),
|
||||
])
|
||||
def test_validate_valid(self, klass, kwargs, val):
|
||||
klass(**kwargs).validate(val)
|
||||
@ -319,6 +321,8 @@ class TestString:
|
||||
({'maxlen': 2}, 'fob'),
|
||||
({'minlen': 2, 'maxlen': 3}, 'f'),
|
||||
({'minlen': 2, 'maxlen': 3}, 'fooo'),
|
||||
# valid_values
|
||||
({'valid_values': configtypes.ValidValues('blah')}, 'fooo'),
|
||||
])
|
||||
def test_validate_invalid(self, klass, kwargs, val):
|
||||
with pytest.raises(configexc.ValidationError):
|
||||
@ -335,6 +339,15 @@ class TestString:
|
||||
def test_complete(self, klass, 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:
|
||||
|
||||
|
@ -94,8 +94,7 @@ class JSTester:
|
||||
**kwargs: Passed to jinja's template.render().
|
||||
"""
|
||||
template = self._jinja_env.get_template(path)
|
||||
with self._qtbot.waitSignal(self.webview.loadFinished,
|
||||
raising=True) as blocker:
|
||||
with self._qtbot.waitSignal(self.webview.loadFinished) as blocker:
|
||||
self.webview.setHtml(template.render(**kwargs))
|
||||
assert blocker.args == [True]
|
||||
|
||||
|
@ -282,7 +282,7 @@ class TestKeyChain:
|
||||
assert not keyparser.execute.called
|
||||
assert keyparser._ambiguous_timer.isActive()
|
||||
# We wait for the timeout to occur.
|
||||
with qtbot.waitSignal(keyparser.keystring_updated, raising=True):
|
||||
with qtbot.waitSignal(keyparser.keystring_updated):
|
||||
pass
|
||||
assert keyparser.execute.called
|
||||
|
||||
|
@ -23,29 +23,11 @@
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
|
||||
from PyQt5.QtCore import QSize, Qt
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
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
|
||||
def progress_widget(qtbot, monkeypatch, config_stub):
|
||||
"""Create a Progress widget and checks its initial state."""
|
||||
@ -62,25 +44,6 @@ def progress_widget(qtbot, monkeypatch, config_stub):
|
||||
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):
|
||||
"""Ensure the Progress widget reacts properly when the page starts loading.
|
||||
|
||||
|
57
tests/unit/mainwindow/statusbar/test_prompt.py
Normal file
57
tests/unit/mainwindow/statusbar/test_prompt.py
Normal 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
|
@ -20,7 +20,6 @@
|
||||
"""Tests for qutebrowser.misc.autoupdate"""
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtTest import QSignalSpy
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.misc import autoupdate, httpclient
|
||||
@ -63,14 +62,10 @@ def test_get_version_success(qtbot):
|
||||
http_stub = HTTPGetStub(success=True)
|
||||
client = autoupdate.PyPIVersionClient(client=http_stub)
|
||||
|
||||
# Use a spy to inspect the signal
|
||||
error_spy = QSignalSpy(client.error)
|
||||
|
||||
with qtbot.waitSignal(client.success, raising=True):
|
||||
with qtbot.assertNotEmitted(client.error):
|
||||
with qtbot.waitSignal(client.success):
|
||||
client.get_version('test')
|
||||
|
||||
assert len(error_spy) == 0
|
||||
|
||||
assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json')
|
||||
|
||||
|
||||
@ -79,14 +74,10 @@ def test_get_version_error(qtbot):
|
||||
http_stub = HTTPGetStub(success=False)
|
||||
client = autoupdate.PyPIVersionClient(client=http_stub)
|
||||
|
||||
# Use a spy to inspect the signal
|
||||
success_spy = QSignalSpy(client.success)
|
||||
|
||||
with qtbot.waitSignal(client.error, raising=True):
|
||||
with qtbot.assertNotEmitted(client.success):
|
||||
with qtbot.waitSignal(client.error):
|
||||
client.get_version('test')
|
||||
|
||||
assert len(success_spy) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('json', INVALID_JSON)
|
||||
def test_invalid_json(qtbot, json):
|
||||
@ -95,10 +86,6 @@ def test_invalid_json(qtbot, json):
|
||||
client = autoupdate.PyPIVersionClient(client=http_stub)
|
||||
client.get_version('test')
|
||||
|
||||
# Use a spy to inspect the signal
|
||||
success_spy = QSignalSpy(client.success)
|
||||
|
||||
with qtbot.waitSignal(client.error, raising=True):
|
||||
with qtbot.assertNotEmitted(client.success):
|
||||
with qtbot.waitSignal(client.error):
|
||||
client.get_version('test')
|
||||
|
||||
assert len(success_spy) == 0
|
||||
|
@ -40,7 +40,8 @@ def proc(qtbot):
|
||||
p = guiprocess.GUIProcess(0, 'testprocess')
|
||||
yield p
|
||||
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()
|
||||
if not blocker.signal_triggered:
|
||||
p._proc.kill()
|
||||
@ -56,8 +57,7 @@ def fake_proc(monkeypatch, stubs):
|
||||
|
||||
def test_start(proc, qtbot, guiprocess_message_mock, py_proc):
|
||||
"""Test simply starting a process."""
|
||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
||||
proc.start(*argv)
|
||||
|
||||
@ -69,8 +69,7 @@ def test_start_verbose(proc, qtbot, guiprocess_message_mock, py_proc):
|
||||
"""Test starting a process verbosely."""
|
||||
proc.verbose = True
|
||||
|
||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
||||
proc.start(*argv)
|
||||
|
||||
@ -97,8 +96,7 @@ def test_start_env(monkeypatch, qtbot, py_proc):
|
||||
sys.exit(0)
|
||||
""")
|
||||
|
||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||
proc.start(*argv)
|
||||
|
||||
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)
|
||||
def test_start_mode(proc, qtbot, py_proc):
|
||||
"""Test simply starting a process with mode parameter."""
|
||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
||||
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):
|
||||
"""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)")
|
||||
proc.start(*argv)
|
||||
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):
|
||||
"""Test starting a GUIProcess twice (with the first call finished)."""
|
||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||
argv = py_proc("import sys; sys.exit(0)")
|
||||
proc.start(*argv)
|
||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
||||
timeout=10000):
|
||||
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||
argv = py_proc("import sys; sys.exit(0)")
|
||||
proc.start(*argv)
|
||||
|
||||
@ -180,7 +175,7 @@ def test_start_logging(fake_proc, caplog):
|
||||
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
||||
"""Test the process emitting an error."""
|
||||
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', [])
|
||||
|
||||
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):
|
||||
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)'))
|
||||
|
||||
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):
|
||||
"""When a process fails, its output should be logged."""
|
||||
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("""
|
||||
import 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
|
||||
logging.
|
||||
"""
|
||||
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000):
|
||||
with qtbot.waitSignal(proc.finished, timeout=10000):
|
||||
proc.start(*py_proc("""
|
||||
import sys
|
||||
print("test", file=sys.{})
|
||||
|
@ -343,13 +343,11 @@ class TestListen:
|
||||
ipc_server.listen()
|
||||
old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
|
||||
|
||||
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000,
|
||||
raising=True):
|
||||
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000):
|
||||
pass
|
||||
|
||||
# Make sure the timer is not singleShot
|
||||
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000,
|
||||
raising=True):
|
||||
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000):
|
||||
pass
|
||||
|
||||
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):
|
||||
ipc_server._socket = qlocalsocket
|
||||
ipc_server.handle_connection()
|
||||
message = ("Got new connection but ignoring it because we're still "
|
||||
"handling another one.")
|
||||
assert message in [rec.message for rec in caplog.records]
|
||||
msg = ("Got new connection but ignoring it because we're still "
|
||||
"handling another one")
|
||||
assert any(rec.message.startswith(msg) for rec in caplog.records)
|
||||
|
||||
def test_disconnected_immediately(self, ipc_server, caplog):
|
||||
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
|
||||
@ -444,7 +442,7 @@ class TestHandleConnection:
|
||||
|
||||
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()
|
||||
|
||||
assert blocker.args == [['foo'], 'tab', '']
|
||||
@ -458,7 +456,7 @@ def connected_socket(qtbot, qlocalsocket, ipc_server):
|
||||
pytest.skip("Skipping connected_socket test - "
|
||||
"https://github.com/The-Compiler/qutebrowser/issues/1045")
|
||||
ipc_server.listen()
|
||||
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
||||
with qtbot.waitSignal(ipc_server._server.newConnection):
|
||||
qlocalsocket.connectToServer('qute-test')
|
||||
yield qlocalsocket
|
||||
qlocalsocket.disconnectFromServer()
|
||||
@ -496,22 +494,19 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8')
|
||||
(b'{"args": [], "target_arg": null}\n', 'invalid version'),
|
||||
])
|
||||
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]
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with qtbot.waitSignals(signals, raising=True):
|
||||
with qtbot.assertNotEmitted(ipc_server.got_args):
|
||||
with qtbot.waitSignals(signals):
|
||||
connected_socket.write(data)
|
||||
|
||||
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 not got_args_spy
|
||||
|
||||
|
||||
def test_multiline(qtbot, ipc_server, connected_socket):
|
||||
spy = QSignalSpy(ipc_server.got_args)
|
||||
error_spy = QSignalSpy(ipc_server.got_invalid_data)
|
||||
|
||||
data = ('{{"args": ["one"], "target_arg": "tab",'
|
||||
' "protocol_version": {version}}}\n'
|
||||
@ -519,11 +514,10 @@ def test_multiline(qtbot, ipc_server, connected_socket):
|
||||
' "protocol_version": {version}}}\n'.format(
|
||||
version=ipc.PROTOCOL_VERSION))
|
||||
|
||||
with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args],
|
||||
raising=True):
|
||||
with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
|
||||
with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args]):
|
||||
connected_socket.write(data.encode('utf-8'))
|
||||
|
||||
assert not error_spy
|
||||
assert len(spy) == 2
|
||||
assert spy[0] == [['one'], 'tab', '']
|
||||
assert spy[1] == [['two'], '', '']
|
||||
@ -542,19 +536,19 @@ class TestSendToRunningInstance:
|
||||
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
|
||||
ipc_server.listen()
|
||||
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):
|
||||
with qtbot.waitSignal(ipc_server.got_args,
|
||||
timeout=5000) as blocker:
|
||||
with tmpdir.as_cwd():
|
||||
if not has_cwd:
|
||||
m = mocker.patch('qutebrowser.misc.ipc.os')
|
||||
m.getcwd.side_effect = OSError
|
||||
sent = ipc.send_to_running_instance('qute-test', ['foo'], None)
|
||||
sent = ipc.send_to_running_instance('qute-test', ['foo'],
|
||||
None)
|
||||
|
||||
assert sent
|
||||
|
||||
assert not error_spy
|
||||
expected_cwd = str(tmpdir) if has_cwd else ''
|
||||
|
||||
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.listen()
|
||||
|
||||
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
||||
with qtbot.waitSignal(ipc_server._server.newConnection):
|
||||
qlocalsocket.connectToServer('qute-test')
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
|
||||
timeout=5000):
|
||||
with qtbot.waitSignal(qlocalsocket.disconnected, timeout=5000):
|
||||
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', [
|
||||
@ -679,14 +672,14 @@ class TestSendOrListen:
|
||||
objreg_server = objreg.get('ipc-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)
|
||||
|
||||
assert ret_client is None
|
||||
|
||||
@pytest.mark.posix(reason="Unneeded on Windows")
|
||||
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)
|
||||
assert ret is None
|
||||
msgs = [e.message for e in caplog.records]
|
||||
@ -727,7 +720,7 @@ class TestSendOrListen:
|
||||
assert isinstance(ret_server, ipc.IPCServer)
|
||||
|
||||
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)
|
||||
|
||||
assert ret_client is None
|
||||
|
@ -74,7 +74,7 @@ def test_finished_signal(qtbot):
|
||||
|
||||
qtbot.add_widget(box)
|
||||
|
||||
with qtbot.waitSignal(box.finished, raising=True):
|
||||
with qtbot.waitSignal(box.finished):
|
||||
box.accept()
|
||||
|
||||
assert signal_triggered
|
||||
|
@ -439,9 +439,8 @@ class TestSave:
|
||||
|
||||
def test_update_completion_signal(self, sess_man, tmpdir, qtbot):
|
||||
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))
|
||||
assert blocker.signal_triggered
|
||||
|
||||
def test_no_state_config(self, sess_man, tmpdir, state_config):
|
||||
session_path = tmpdir / 'foo.yml'
|
||||
@ -691,9 +690,8 @@ class TestDelete:
|
||||
sess = tmpdir / 'foo.yml'
|
||||
sess.ensure()
|
||||
|
||||
blocker = qtbot.waitSignal(sess_man.update_completion)
|
||||
with qtbot.waitSignal(sess_man.update_completion):
|
||||
sess_man.delete(str(sess))
|
||||
assert blocker.signal_triggered
|
||||
|
||||
def test_not_existing(self, sess_man, qtbot, tmpdir):
|
||||
sess = tmpdir / 'foo.yml'
|
||||
|
@ -166,13 +166,17 @@ class TestFuzzyUrl:
|
||||
assert not os_mock.path.exists.called
|
||||
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."""
|
||||
os_mock.path.exists.return_value = True
|
||||
os_mock.path.isabs.return_value = True
|
||||
|
||||
url = urlutils.fuzzy_url('/foo')
|
||||
assert url == QUrl('file:///foo')
|
||||
url = urlutils.fuzzy_url(path)
|
||||
assert url == expected
|
||||
|
||||
@pytest.mark.posix
|
||||
def test_file_absolute_expanded(self, os_mock):
|
||||
|
@ -61,23 +61,21 @@ def test_done(mode, answer, signal_names, question, qtbot):
|
||||
question.mode = mode
|
||||
question.answer = answer
|
||||
signals = [getattr(question, name) for name in signal_names]
|
||||
with qtbot.waitSignals(signals, raising=True):
|
||||
with qtbot.waitSignals(signals):
|
||||
question.done()
|
||||
assert not question.is_aborted
|
||||
|
||||
|
||||
def test_cancel(question, qtbot):
|
||||
"""Test Question.cancel()."""
|
||||
with qtbot.waitSignals([question.cancelled, question.completed],
|
||||
raising=True):
|
||||
with qtbot.waitSignals([question.cancelled, question.completed]):
|
||||
question.cancel()
|
||||
assert not question.is_aborted
|
||||
|
||||
|
||||
def test_abort(question, qtbot):
|
||||
"""Test Question.abort()."""
|
||||
with qtbot.waitSignals([question.aborted, question.completed],
|
||||
raising=True):
|
||||
with qtbot.waitSignals([question.aborted, question.completed]):
|
||||
question.abort()
|
||||
assert question.is_aborted
|
||||
|
||||
|
@ -72,13 +72,13 @@ def test_start_overflow():
|
||||
def test_timeout_start(qtbot):
|
||||
"""Make sure the timer works with start()."""
|
||||
t = usertypes.Timer()
|
||||
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
|
||||
with qtbot.waitSignal(t.timeout, timeout=3000):
|
||||
t.start(200)
|
||||
|
||||
|
||||
def test_timeout_set_interval(qtbot):
|
||||
"""Make sure the timer works with setInterval()."""
|
||||
t = usertypes.Timer()
|
||||
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
|
||||
with qtbot.waitSignal(t.timeout, timeout=3000):
|
||||
t.setInterval(200)
|
||||
t.start()
|
||||
|
20
tox.ini
20
tox.ini
@ -20,7 +20,7 @@ deps =
|
||||
Flask==0.10.1
|
||||
glob2==0.4.1
|
||||
httpbin==0.4.0
|
||||
hypothesis==1.18.1
|
||||
hypothesis==2.0.0
|
||||
itsdangerous==0.24
|
||||
Mako==1.0.3
|
||||
parse==1.6.6
|
||||
@ -33,9 +33,10 @@ deps =
|
||||
pytest-faulthandler==1.3.0
|
||||
pytest-html==1.7
|
||||
pytest-mock==0.9.0
|
||||
pytest-qt==1.10.0
|
||||
pytest-sugar==0.5.1
|
||||
pytest-qt==1.11.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-travis-fold==1.2.0
|
||||
pytest-repeat==0.2
|
||||
six==1.10.0
|
||||
termcolor==1.1.0
|
||||
vulture==0.8.1
|
||||
@ -45,7 +46,7 @@ deps =
|
||||
cherrypy==4.0.0
|
||||
commands =
|
||||
{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}
|
||||
|
||||
[testenv:mkvenv]
|
||||
@ -73,7 +74,7 @@ passenv = {[testenv]passenv}
|
||||
deps = {[testenv]deps}
|
||||
setenv =
|
||||
DISPLAY=
|
||||
QUTE_NO_DISPLAY_OK=1
|
||||
QUTE_NO_DISPLAY=1
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} -m py.test --strict -rfEw {posargs:tests}
|
||||
@ -105,8 +106,8 @@ passenv =
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
{[testenv:misc]deps}
|
||||
astroid==1.4.3
|
||||
pylint==1.5.2
|
||||
astroid==1.4.4
|
||||
pylint==1.5.4
|
||||
requests==2.9.1
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
@ -156,7 +157,6 @@ deps =
|
||||
py==1.4.31
|
||||
pyflakes==1.0.0
|
||||
pytest==2.8.5
|
||||
pytest-cache==1.0
|
||||
pytest-flakes==1.0.1
|
||||
commands =
|
||||
{envpython} -m py.test -q --flakes --ignore=tests --noconftest
|
||||
@ -168,10 +168,9 @@ deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
apipkg==1.4
|
||||
execnet==1.4.1
|
||||
pep8==1.6.2
|
||||
pep8==1.7
|
||||
py==1.4.31
|
||||
pytest==2.8.5
|
||||
pytest-cache==1.0
|
||||
pytest-pep8==1.0.6
|
||||
commands =
|
||||
{envpython} -m py.test -q --pep8 --ignore=tests --noconftest
|
||||
@ -187,7 +186,6 @@ deps =
|
||||
mccabe==0.3.1
|
||||
py==1.4.31
|
||||
pytest==2.8.5
|
||||
pytest-cache==1.0
|
||||
pytest-mccabe==0.1
|
||||
commands =
|
||||
{envpython} -m py.test -q --mccabe --ignore=tests --noconftest
|
||||
|
Loading…
Reference in New Issue
Block a user