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
|
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml
|
- C:\Python34\Scripts\tox -e %TESTENV% -- -v --junitxml=junit.xml
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,6 +11,7 @@ __pycache__
|
|||||||
/setuptools-*.egg
|
/setuptools-*.egg
|
||||||
/setuptools-*.zip
|
/setuptools-*.zip
|
||||||
/qutebrowser/git-commit-id
|
/qutebrowser/git-commit-id
|
||||||
|
/qutebrowser/3rdparty
|
||||||
/doc/*.html
|
/doc/*.html
|
||||||
/README.html
|
/README.html
|
||||||
/CHANGELOG.html
|
/CHANGELOG.html
|
||||||
|
@ -37,7 +37,7 @@ install:
|
|||||||
- python scripts/dev/ci_install.py
|
- python scripts/dev/ci_install.py
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- tox -e $TESTENV -- -p no:sugar -v --cov-report term tests
|
- tox -e $TESTENV -- -v --cov-report term tests
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
|
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
|
||||||
|
@ -14,6 +14,31 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
|||||||
// `Fixed` for any bug fixes.
|
// `Fixed` for any bug fixes.
|
||||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||||
|
|
||||||
|
v0.6.0 (unreleased)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Added
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
- New `--quiet` argument for the `:debug-pyeval` command to not open a tab with
|
||||||
|
the results. Note `:debug-pyeval` is still only intended for debugging.
|
||||||
|
|
||||||
|
Changed
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
- Pasting multiple lines via `:paste` now opens each line in a new tab.
|
||||||
|
|
||||||
|
v0.5.1
|
||||||
|
------
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
- Fixed completion for various config values when using `:set`.
|
||||||
|
- Fixed config validation for various config values.
|
||||||
|
- Prevented an error being logged when a website with HTTP authentication was
|
||||||
|
opened on Windows.
|
||||||
|
|
||||||
v0.5.0
|
v0.5.0
|
||||||
------
|
------
|
||||||
|
|
||||||
|
@ -152,9 +152,29 @@ $ nix-env -i qutebrowser
|
|||||||
On Windows
|
On Windows
|
||||||
----------
|
----------
|
||||||
|
|
||||||
You can either use one of the
|
There are different ways to install qutebrowser on Windows:
|
||||||
https://github.com/The-Compiler/qutebrowser/releases[prebuilt standalone
|
|
||||||
packages or MSI installers], or install manually:
|
Prebuilt binaries
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Prebuilt standalone packages and MSI installers
|
||||||
|
https://github.com/The-Compiler/qutebrowser/releases[are built] for every
|
||||||
|
release.
|
||||||
|
|
||||||
|
https://chocolatey.org/packages/qutebrowser[Chocolatey package]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* PackageManagement PowerShell module
|
||||||
|
----
|
||||||
|
PS C:\> Install-Package qutebrowser
|
||||||
|
----
|
||||||
|
* Chocolatey's client
|
||||||
|
----
|
||||||
|
C:\> choco install qutebrowser
|
||||||
|
----
|
||||||
|
|
||||||
|
Manual install
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Use the installer from http://www.python.org/downloads[python.org] to get
|
* Use the installer from http://www.python.org/downloads[python.org] to get
|
||||||
Python 3 (be sure to install pip).
|
Python 3 (be sure to install pip).
|
||||||
|
@ -160,6 +160,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* ZDarian
|
* ZDarian
|
||||||
* John ShaggyTwoDope Jenkins
|
* John ShaggyTwoDope Jenkins
|
||||||
* Peter Vilim
|
* Peter Vilim
|
||||||
|
* Tarcisio Fedrizzi
|
||||||
* Jonas Schürmann
|
* Jonas Schürmann
|
||||||
* Panagiotis Ktistakis
|
* Panagiotis Ktistakis
|
||||||
* Jimmy
|
* Jimmy
|
||||||
@ -176,6 +177,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* jnphilipp
|
* jnphilipp
|
||||||
* Tobias Patzl
|
* Tobias Patzl
|
||||||
* Peter Michely
|
* Peter Michely
|
||||||
|
* Link
|
||||||
* Larry Hynes
|
* Larry Hynes
|
||||||
* Johannes Altmanninger
|
* Johannes Altmanninger
|
||||||
* Samir Benmendil
|
* Samir Benmendil
|
||||||
@ -186,6 +188,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Corentin Jule
|
* Corentin Jule
|
||||||
* zwarag
|
* zwarag
|
||||||
* xd1le
|
* xd1le
|
||||||
|
* evan
|
||||||
* dylan araps
|
* dylan araps
|
||||||
* Tim Harder
|
* Tim Harder
|
||||||
* Thiago Barroso Perrotta
|
* Thiago Barroso Perrotta
|
||||||
|
@ -402,6 +402,8 @@ Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
|
|||||||
|
|
||||||
Open a page from the clipboard.
|
Open a page from the clipboard.
|
||||||
|
|
||||||
|
If the pasted text contains newlines, each line gets opened in its own tab.
|
||||||
|
|
||||||
==== optional arguments
|
==== optional arguments
|
||||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||||
@ -1224,6 +1226,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
|||||||
|Command|Description
|
|Command|Description
|
||||||
|<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log.
|
|<<debug-all-objects,debug-all-objects>>|Print a list of all objects to the debug log.
|
||||||
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|
||||||
|
|<<debug-clear-ssl-errors,debug-clear-ssl-errors>>|Clear remembered SSL error answers.
|
||||||
|<<debug-console,debug-console>>|Show the debugging console.
|
|<<debug-console,debug-console>>|Show the debugging console.
|
||||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||||
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|
||||||
@ -1239,6 +1242,10 @@ Print a list of all objects to the debug log.
|
|||||||
=== debug-cache-stats
|
=== debug-cache-stats
|
||||||
Print LRU cache stats.
|
Print LRU cache stats.
|
||||||
|
|
||||||
|
[[debug-clear-ssl-errors]]
|
||||||
|
=== debug-clear-ssl-errors
|
||||||
|
Clear remembered SSL error answers.
|
||||||
|
|
||||||
[[debug-console]]
|
[[debug-console]]
|
||||||
=== debug-console
|
=== debug-console
|
||||||
Show the debugging console.
|
Show the debugging console.
|
||||||
@ -1266,13 +1273,16 @@ Dump the current page's content to a file.
|
|||||||
|
|
||||||
[[debug-pyeval]]
|
[[debug-pyeval]]
|
||||||
=== debug-pyeval
|
=== debug-pyeval
|
||||||
Syntax: +:debug-pyeval 's'+
|
Syntax: +:debug-pyeval [*--quiet*] 's'+
|
||||||
|
|
||||||
Evaluate a python string and display the results as a web page.
|
Evaluate a python string and display the results as a web page.
|
||||||
|
|
||||||
==== positional arguments
|
==== positional arguments
|
||||||
* +'s'+: The string to evaluate.
|
* +'s'+: The string to evaluate.
|
||||||
|
|
||||||
|
==== optional arguments
|
||||||
|
* +*-q*+, +*--quiet*+: Don't show the output in a new tab.
|
||||||
|
|
||||||
==== note
|
==== note
|
||||||
* This command does not split arguments after the last argument and handles quotes literally.
|
* This command does not split arguments after the last argument and handles quotes literally.
|
||||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||||
|
@ -11,6 +11,9 @@ markers =
|
|||||||
not_xvfb: Tests which can't be run with Xvfb.
|
not_xvfb: Tests which can't be run with Xvfb.
|
||||||
frozen: Tests which can only be run if sys.frozen is True.
|
frozen: Tests which can only be run if sys.frozen is True.
|
||||||
integration: Tests which test a bigger portion of code, run without coverage.
|
integration: Tests which test a bigger portion of code, run without coverage.
|
||||||
|
skip: Always skipped test.
|
||||||
|
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
|
||||||
|
xfail_norun: xfail the test with out running it
|
||||||
flakes-ignore =
|
flakes-ignore =
|
||||||
UnusedImport
|
UnusedImport
|
||||||
UnusedVariable
|
UnusedVariable
|
||||||
@ -39,3 +42,6 @@ qt_log_ignore =
|
|||||||
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
||||||
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
|
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
|
||||||
^QXcbClipboard: SelectionRequest too old
|
^QXcbClipboard: SelectionRequest too old
|
||||||
|
^QGeoclueMaster error creating GeoclueMasterClient\.
|
||||||
|
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
|
||||||
|
qt_wait_signal_raising = true
|
||||||
|
@ -809,6 +809,9 @@ class CommandDispatcher:
|
|||||||
def paste(self, sel=False, tab=False, bg=False, window=False):
|
def paste(self, sel=False, tab=False, bg=False, window=False):
|
||||||
"""Open a page from the clipboard.
|
"""Open a page from the clipboard.
|
||||||
|
|
||||||
|
If the pasted text contains newlines, each line gets opened in its own
|
||||||
|
tab.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sel: Use the primary selection instead of the clipboard.
|
sel: Use the primary selection instead of the clipboard.
|
||||||
tab: Open in a new tab.
|
tab: Open in a new tab.
|
||||||
@ -825,12 +828,18 @@ class CommandDispatcher:
|
|||||||
text = clipboard.text(mode)
|
text = clipboard.text(mode)
|
||||||
if not text:
|
if not text:
|
||||||
raise cmdexc.CommandError("{} is empty.".format(target))
|
raise cmdexc.CommandError("{} is empty.".format(target))
|
||||||
log.misc.debug("{} contained: '{}'".format(target, text))
|
log.misc.debug("{} contained: '{}'".format(target,
|
||||||
try:
|
text.replace('\n', '\\n')))
|
||||||
url = urlutils.fuzzy_url(text)
|
text_urls = enumerate(u for u in text.split('\n') if u)
|
||||||
except urlutils.InvalidUrlError as e:
|
for i, text_url in text_urls:
|
||||||
raise cmdexc.CommandError(e)
|
if not window and i > 0:
|
||||||
self._open(url, tab, bg, window)
|
tab = False
|
||||||
|
bg = True
|
||||||
|
try:
|
||||||
|
url = urlutils.fuzzy_url(text_url)
|
||||||
|
except urlutils.InvalidUrlError as e:
|
||||||
|
raise cmdexc.CommandError(e)
|
||||||
|
self._open(url, tab, bg, window)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
count='count')
|
count='count')
|
||||||
@ -1462,11 +1471,11 @@ class CommandDispatcher:
|
|||||||
webview = self._current_widget()
|
webview = self._current_widget()
|
||||||
if not webview.selection_enabled:
|
if not webview.selection_enabled:
|
||||||
act = [QWebPage.MoveToNextWord]
|
act = [QWebPage.MoveToNextWord]
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32': # pragma: no cover
|
||||||
act.append(QWebPage.MoveToPreviousChar)
|
act.append(QWebPage.MoveToPreviousChar)
|
||||||
else:
|
else:
|
||||||
act = [QWebPage.SelectNextWord]
|
act = [QWebPage.SelectNextWord]
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32': # pragma: no cover
|
||||||
act.append(QWebPage.SelectPreviousChar)
|
act.append(QWebPage.SelectPreviousChar)
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
for a in act:
|
for a in act:
|
||||||
@ -1483,11 +1492,11 @@ class CommandDispatcher:
|
|||||||
webview = self._current_widget()
|
webview = self._current_widget()
|
||||||
if not webview.selection_enabled:
|
if not webview.selection_enabled:
|
||||||
act = [QWebPage.MoveToNextWord]
|
act = [QWebPage.MoveToNextWord]
|
||||||
if sys.platform != 'win32':
|
if sys.platform != 'win32': # pragma: no branch
|
||||||
act.append(QWebPage.MoveToNextChar)
|
act.append(QWebPage.MoveToNextChar)
|
||||||
else:
|
else:
|
||||||
act = [QWebPage.SelectNextWord]
|
act = [QWebPage.SelectNextWord]
|
||||||
if sys.platform != 'win32':
|
if sys.platform != 'win32': # pragma: no branch
|
||||||
act.append(QWebPage.SelectNextChar)
|
act.append(QWebPage.SelectNextChar)
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
for a in act:
|
for a in act:
|
||||||
@ -1755,3 +1764,10 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
QApplication.postEvent(receiver, press_event)
|
QApplication.postEvent(receiver, press_event)
|
||||||
QApplication.postEvent(receiver, release_event)
|
QApplication.postEvent(receiver, release_event)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
|
debug=True)
|
||||||
|
def debug_clear_ssl_errors(self):
|
||||||
|
"""Clear remembered SSL error answers."""
|
||||||
|
nam = self._current_widget().page().networkAccessManager()
|
||||||
|
nam.clear_all_ssl_errors()
|
||||||
|
@ -484,8 +484,10 @@ class HintManager(QObject):
|
|||||||
mode = QClipboard.Selection if sel else QClipboard.Clipboard
|
mode = QClipboard.Selection if sel else QClipboard.Clipboard
|
||||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||||
QApplication.clipboard().setText(urlstr, mode)
|
QApplication.clipboard().setText(urlstr, mode)
|
||||||
message.info(self._win_id, "URL yanked to {}".format(
|
msg = "Yanked URL to {}: {}".format(
|
||||||
"primary selection" if sel else "clipboard"))
|
"primary selection" if sel else "clipboard",
|
||||||
|
urlstr)
|
||||||
|
message.info(self._win_id, msg)
|
||||||
|
|
||||||
def _run_cmd(self, url, context):
|
def _run_cmd(self, url, context):
|
||||||
"""Run the command based on a hint URL.
|
"""Run the command based on a hint URL.
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
"""Our own QNetworkAccessManager."""
|
"""Our own QNetworkAccessManager."""
|
||||||
|
|
||||||
|
import os
|
||||||
import collections
|
import collections
|
||||||
import netrc
|
import netrc
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
|||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||||
urlutils)
|
urlutils, debug)
|
||||||
from qutebrowser.browser import cookies
|
from qutebrowser.browser import cookies
|
||||||
from qutebrowser.browser.network import qutescheme, networkreply
|
from qutebrowser.browser.network import qutescheme, networkreply
|
||||||
from qutebrowser.browser.network import filescheme
|
from qutebrowser.browser.network import filescheme
|
||||||
@ -62,6 +63,11 @@ class SslError(QSslError):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
return hash((self.certificate().toDer(), self.error()))
|
return hash((self.certificate().toDer(), self.error()))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return utils.get_repr(
|
||||||
|
self, error=debug.qenum_key(QSslError, self.error()),
|
||||||
|
string=self.errorString())
|
||||||
|
|
||||||
|
|
||||||
class NetworkManager(QNetworkAccessManager):
|
class NetworkManager(QNetworkAccessManager):
|
||||||
|
|
||||||
@ -189,44 +195,51 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
"""
|
"""
|
||||||
errors = [SslError(e) for e in errors]
|
errors = [SslError(e) for e in errors]
|
||||||
ssl_strict = config.get('network', 'ssl-strict')
|
ssl_strict = config.get('network', 'ssl-strict')
|
||||||
|
log.webview.debug("SSL errors {!r}, strict {}".format(
|
||||||
|
errors, ssl_strict))
|
||||||
|
|
||||||
|
try:
|
||||||
|
host_tpl = urlutils.host_tuple(reply.url())
|
||||||
|
except ValueError:
|
||||||
|
host_tpl = None
|
||||||
|
is_accepted = False
|
||||||
|
is_rejected = False
|
||||||
|
else:
|
||||||
|
is_accepted = set(errors).issubset(
|
||||||
|
self._accepted_ssl_errors[host_tpl])
|
||||||
|
is_rejected = set(errors).issubset(
|
||||||
|
self._rejected_ssl_errors[host_tpl])
|
||||||
|
|
||||||
|
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
|
||||||
|
return
|
||||||
|
elif is_accepted:
|
||||||
|
reply.ignoreSslErrors()
|
||||||
|
return
|
||||||
|
|
||||||
if ssl_strict == 'ask':
|
if ssl_strict == 'ask':
|
||||||
try:
|
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
||||||
host_tpl = urlutils.host_tuple(reply.url())
|
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
|
||||||
except ValueError:
|
mode=usertypes.PromptMode.yesno, owner=reply)
|
||||||
host_tpl = None
|
if answer:
|
||||||
is_accepted = False
|
|
||||||
is_rejected = False
|
|
||||||
else:
|
|
||||||
is_accepted = set(errors).issubset(
|
|
||||||
self._accepted_ssl_errors[host_tpl])
|
|
||||||
is_rejected = set(errors).issubset(
|
|
||||||
self._rejected_ssl_errors[host_tpl])
|
|
||||||
if is_accepted:
|
|
||||||
reply.ignoreSslErrors()
|
reply.ignoreSslErrors()
|
||||||
elif is_rejected:
|
err_dict = self._accepted_ssl_errors
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
err_dict = self._rejected_ssl_errors
|
||||||
errors)
|
if host_tpl is not None:
|
||||||
answer = self._ask('SSL errors - continue?\n{}'.format(
|
err_dict[host_tpl] += errors
|
||||||
err_string), mode=usertypes.PromptMode.yesno,
|
|
||||||
owner=reply)
|
|
||||||
if answer:
|
|
||||||
reply.ignoreSslErrors()
|
|
||||||
d = self._accepted_ssl_errors
|
|
||||||
else:
|
|
||||||
d = self._rejected_ssl_errors
|
|
||||||
if host_tpl is not None:
|
|
||||||
d[host_tpl] += errors
|
|
||||||
elif ssl_strict:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
for err in errors:
|
for err in errors:
|
||||||
# FIXME we might want to use warn here (non-fatal error)
|
# FIXME we might want to use warn here (non-fatal error)
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/114
|
# https://github.com/The-Compiler/qutebrowser/issues/114
|
||||||
message.error(self._win_id,
|
message.error(self._win_id, 'SSL error: {}'.format(
|
||||||
'SSL error: {}'.format(err.errorString()))
|
err.errorString()))
|
||||||
reply.ignoreSslErrors()
|
reply.ignoreSslErrors()
|
||||||
|
self._accepted_ssl_errors[host_tpl] += errors
|
||||||
|
|
||||||
|
def clear_all_ssl_errors(self):
|
||||||
|
"""Clear all remembered SSL errors."""
|
||||||
|
self._accepted_ssl_errors.clear()
|
||||||
|
self._rejected_ssl_errors.clear()
|
||||||
|
|
||||||
@pyqtSlot(QUrl)
|
@pyqtSlot(QUrl)
|
||||||
def clear_rejected_ssl_errors(self, url):
|
def clear_rejected_ssl_errors(self, url):
|
||||||
@ -244,7 +257,10 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
def on_authentication_required(self, reply, authenticator):
|
def on_authentication_required(self, reply, authenticator):
|
||||||
"""Called when a website needs authentication."""
|
"""Called when a website needs authentication."""
|
||||||
user, password = None, None
|
user, password = None, None
|
||||||
if not hasattr(reply, "netrc_used"):
|
if not hasattr(reply, "netrc_used") and 'HOME' in os.environ:
|
||||||
|
# We'll get an OSError by netrc if 'HOME' isn't available in
|
||||||
|
# os.environ. We don't want to log that, so we prevent it
|
||||||
|
# altogether.
|
||||||
reply.netrc_used = True
|
reply.netrc_used = True
|
||||||
try:
|
try:
|
||||||
net = netrc.netrc()
|
net = netrc.netrc()
|
||||||
|
@ -258,6 +258,13 @@ class String(BaseType):
|
|||||||
self._basic_validation(value)
|
self._basic_validation(value)
|
||||||
if not value:
|
if not value:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.valid_values is not None:
|
||||||
|
if value not in self.valid_values:
|
||||||
|
raise configexc.ValidationError(
|
||||||
|
value, "valid values: {}".format(', '.join(
|
||||||
|
self.valid_values)))
|
||||||
|
|
||||||
if self.forbidden is not None and any(c in value
|
if self.forbidden is not None and any(c in value
|
||||||
for c in self.forbidden):
|
for c in self.forbidden):
|
||||||
raise configexc.ValidationError(value, "may not contain the chars "
|
raise configexc.ValidationError(value, "may not contain the chars "
|
||||||
@ -270,7 +277,10 @@ class String(BaseType):
|
|||||||
"long!".format(self.maxlen))
|
"long!".format(self.maxlen))
|
||||||
|
|
||||||
def complete(self):
|
def complete(self):
|
||||||
return self._completions
|
if self._completions is not None:
|
||||||
|
return self._completions
|
||||||
|
else:
|
||||||
|
return super().complete()
|
||||||
|
|
||||||
|
|
||||||
class List(BaseType):
|
class List(BaseType):
|
||||||
|
@ -417,9 +417,6 @@ class MainWindow(QWidget):
|
|||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
download_count = download_manager.rowCount()
|
download_count = download_manager.rowCount()
|
||||||
quit_texts = []
|
quit_texts = []
|
||||||
# Close if set to never ask for confirmation
|
|
||||||
if 'never' in confirm_quit:
|
|
||||||
pass
|
|
||||||
# Ask if multiple-tabs are open
|
# Ask if multiple-tabs are open
|
||||||
if 'multiple-tabs' in confirm_quit and tab_count > 1:
|
if 'multiple-tabs' in confirm_quit and tab_count > 1:
|
||||||
quit_texts.append("{} {} open.".format(
|
quit_texts.append("{} {} open.".format(
|
||||||
|
@ -229,8 +229,9 @@ class IPCServer(QObject):
|
|||||||
log.ipc.debug("In on_error with None socket!")
|
log.ipc.debug("In on_error with None socket!")
|
||||||
return
|
return
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
log.ipc.debug("Socket error {}: {}".format(
|
log.ipc.debug("Socket 0x{:x}: error {}: {}".format(
|
||||||
self._socket.error(), self._socket.errorString()))
|
id(self._socket), self._socket.error(),
|
||||||
|
self._socket.errorString()))
|
||||||
if err != QLocalSocket.PeerClosedError:
|
if err != QLocalSocket.PeerClosedError:
|
||||||
raise SocketError("handling IPC connection", self._socket)
|
raise SocketError("handling IPC connection", self._socket)
|
||||||
|
|
||||||
@ -241,13 +242,14 @@ class IPCServer(QObject):
|
|||||||
return
|
return
|
||||||
if self._socket is not None:
|
if self._socket is not None:
|
||||||
log.ipc.debug("Got new connection but ignoring it because we're "
|
log.ipc.debug("Got new connection but ignoring it because we're "
|
||||||
"still handling another one.")
|
"still handling another one (0x{:x}).".format(
|
||||||
|
id(self._socket)))
|
||||||
return
|
return
|
||||||
socket = self._server.nextPendingConnection()
|
socket = self._server.nextPendingConnection()
|
||||||
if socket is None:
|
if socket is None:
|
||||||
log.ipc.debug("No new connection to handle.")
|
log.ipc.debug("No new connection to handle.")
|
||||||
return
|
return
|
||||||
log.ipc.debug("Client connected.")
|
log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
|
||||||
self._timer.start()
|
self._timer.start()
|
||||||
self._socket = socket
|
self._socket = socket
|
||||||
socket.readyRead.connect(self.on_ready_read)
|
socket.readyRead.connect(self.on_ready_read)
|
||||||
@ -267,7 +269,8 @@ class IPCServer(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_disconnected(self):
|
def on_disconnected(self):
|
||||||
"""Clean up socket when the client disconnected."""
|
"""Clean up socket when the client disconnected."""
|
||||||
log.ipc.debug("Client disconnected.")
|
log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
|
||||||
|
id(self._socket)))
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
if self._socket is None:
|
if self._socket is None:
|
||||||
log.ipc.debug("In on_disconnected with None socket!")
|
log.ipc.debug("In on_disconnected with None socket!")
|
||||||
@ -279,7 +282,8 @@ class IPCServer(QObject):
|
|||||||
|
|
||||||
def _handle_invalid_data(self):
|
def _handle_invalid_data(self):
|
||||||
"""Handle invalid data we got from a QLocalSocket."""
|
"""Handle invalid data we got from a QLocalSocket."""
|
||||||
log.ipc.error("Ignoring invalid IPC data.")
|
log.ipc.error("Ignoring invalid IPC data from socket 0x{:x}.".format(
|
||||||
|
id(self._socket)))
|
||||||
self.got_invalid_data.emit()
|
self.got_invalid_data.emit()
|
||||||
self._socket.error.connect(self.on_error)
|
self._socket.error.connect(self.on_error)
|
||||||
self._socket.disconnectFromServer()
|
self._socket.disconnectFromServer()
|
||||||
@ -292,11 +296,12 @@ class IPCServer(QObject):
|
|||||||
# active for some reason.
|
# active for some reason.
|
||||||
log.ipc.warning("In on_ready_read with None socket!")
|
log.ipc.warning("In on_ready_read with None socket!")
|
||||||
return
|
return
|
||||||
self._timer.start()
|
self._timer.stop()
|
||||||
while self._socket is not None and self._socket.canReadLine():
|
while self._socket is not None and self._socket.canReadLine():
|
||||||
data = bytes(self._socket.readLine())
|
data = bytes(self._socket.readLine())
|
||||||
self.got_raw.emit(data)
|
self.got_raw.emit(data)
|
||||||
log.ipc.debug("Read from socket: {}".format(data))
|
log.ipc.debug("Read from socket 0x{:x}: {}".format(
|
||||||
|
id(self._socket), data))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decoded = data.decode('utf-8')
|
decoded = data.decode('utf-8')
|
||||||
@ -337,11 +342,13 @@ class IPCServer(QObject):
|
|||||||
|
|
||||||
cwd = json_data.get('cwd', None)
|
cwd = json_data.get('cwd', None)
|
||||||
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
|
self.got_args.emit(json_data['args'], json_data['target_arg'], cwd)
|
||||||
|
self._timer.start()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_timeout(self):
|
def on_timeout(self):
|
||||||
"""Cancel the current connection if it was idle for too long."""
|
"""Cancel the current connection if it was idle for too long."""
|
||||||
log.ipc.error("IPC connection timed out.")
|
log.ipc.error("IPC connection timed out "
|
||||||
|
"(socket 0x{:x}).".format(id(self._socket)))
|
||||||
self._socket.disconnectFromServer()
|
self._socket.disconnectFromServer()
|
||||||
if self._socket is not None: # pragma: no cover
|
if self._socket is not None: # pragma: no cover
|
||||||
# on_socket_disconnected sets it to None
|
# on_socket_disconnected sets it to None
|
||||||
@ -369,7 +376,8 @@ class IPCServer(QObject):
|
|||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Shut down the IPC server cleanly."""
|
"""Shut down the IPC server cleanly."""
|
||||||
log.ipc.debug("Shutting down IPC")
|
log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
|
||||||
|
id(self._socket)))
|
||||||
if self._socket is not None:
|
if self._socket is not None:
|
||||||
self._socket.deleteLater()
|
self._socket.deleteLater()
|
||||||
self._socket = None
|
self._socket = None
|
||||||
|
@ -35,6 +35,8 @@ from qutebrowser.config import style
|
|||||||
from qutebrowser.misc import consolewidget
|
from qutebrowser.misc import consolewidget
|
||||||
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
|
# so it's available for :debug-pyeval
|
||||||
|
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
|
@cmdutils.register(maxsplit=1, no_cmd_split=True, win_id='win_id')
|
||||||
@ -176,18 +178,23 @@ def debug_trace(expr=""):
|
|||||||
|
|
||||||
|
|
||||||
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
|
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
|
||||||
def debug_pyeval(s):
|
def debug_pyeval(s, quiet=False):
|
||||||
"""Evaluate a python string and display the results as a web page.
|
"""Evaluate a python string and display the results as a web page.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
s: The string to evaluate.
|
s: The string to evaluate.
|
||||||
|
quiet: Don't show the output in a new tab.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
r = eval(s)
|
r = eval(s)
|
||||||
out = repr(r)
|
out = repr(r)
|
||||||
except Exception:
|
except Exception:
|
||||||
out = traceback.format_exc()
|
out = traceback.format_exc()
|
||||||
|
|
||||||
qutescheme.pyeval_output = out
|
qutescheme.pyeval_output = out
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
if quiet:
|
||||||
window='last-focused')
|
log.misc.debug("pyeval output: {}".format(out))
|
||||||
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
else:
|
||||||
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window='last-focused')
|
||||||
|
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
|
||||||
|
@ -168,6 +168,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
|||||||
Return:
|
Return:
|
||||||
A target QUrl to a search page or the original URL.
|
A target QUrl to a search page or the original URL.
|
||||||
"""
|
"""
|
||||||
|
urlstr = urlstr.strip()
|
||||||
expanded = os.path.expanduser(urlstr)
|
expanded = os.path.expanduser(urlstr)
|
||||||
if os.path.isabs(expanded):
|
if os.path.isabs(expanded):
|
||||||
path = expanded
|
path = expanded
|
||||||
@ -181,11 +182,10 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
|||||||
else:
|
else:
|
||||||
path = None
|
path = None
|
||||||
|
|
||||||
stripped = urlstr.strip()
|
|
||||||
if path is not None and os.path.exists(path):
|
if path is not None and os.path.exists(path):
|
||||||
log.url.debug("URL is a local file")
|
log.url.debug("URL is a local file")
|
||||||
url = QUrl.fromLocalFile(path)
|
url = QUrl.fromLocalFile(path)
|
||||||
elif (not do_search) or is_url(stripped):
|
elif (not do_search) or is_url(urlstr):
|
||||||
# probably an address
|
# probably an address
|
||||||
log.url.debug("URL is a fuzzy address")
|
log.url.debug("URL is a fuzzy address")
|
||||||
url = qurl_from_user_input(urlstr)
|
url = qurl_from_user_input(urlstr)
|
||||||
@ -194,7 +194,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True):
|
|||||||
try:
|
try:
|
||||||
url = _get_search_url(urlstr)
|
url = _get_search_url(urlstr)
|
||||||
except ValueError: # invalid search engine
|
except ValueError: # invalid search engine
|
||||||
url = qurl_from_user_input(stripped)
|
url = qurl_from_user_input(urlstr)
|
||||||
log.url.debug("Converting fuzzy term {} to URL -> {}".format(
|
log.url.debug("Converting fuzzy term {} to URL -> {}".format(
|
||||||
urlstr, url.toDisplayString()))
|
urlstr, url.toDisplayString()))
|
||||||
if do_search and config.get('general', 'auto-search'):
|
if do_search and config.get('general', 'auto-search'):
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
Jinja2==2.8.0
|
Jinja2==2.8.0
|
||||||
MarkupSafe==0.23
|
MarkupSafe==0.23
|
||||||
Pygments==2.0.2
|
Pygments==2.1
|
||||||
pyPEG2==2.15.2
|
pyPEG2==2.15.2
|
||||||
PyYAML==3.11
|
PyYAML==3.11
|
||||||
# "ValueError: I/O operation on closed file" with pytest since 0.3.5
|
colorama==0.3.6
|
||||||
# WORKAROUND for https://github.com/tartley/colorama/issues/81
|
|
||||||
colorama==0.3.3 # rq.filter: <=0.3.3
|
|
||||||
colorlog==2.6.0
|
colorlog==2.6.0
|
||||||
cssutils==1.0.1
|
cssutils==1.0.1
|
||||||
|
@ -100,6 +100,8 @@ PERFECT_FILES = [
|
|||||||
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
||||||
('tests/unit/mainwindow/statusbar/test_textbase.py',
|
('tests/unit/mainwindow/statusbar/test_textbase.py',
|
||||||
'qutebrowser/mainwindow/statusbar/textbase.py'),
|
'qutebrowser/mainwindow/statusbar/textbase.py'),
|
||||||
|
('tests/unit/mainwindow/statusbar/test_prompt.py',
|
||||||
|
'qutebrowser/mainwindow/statusbar/prompt.py'),
|
||||||
|
|
||||||
('tests/unit/config/test_configtypes.py',
|
('tests/unit/config/test_configtypes.py',
|
||||||
'qutebrowser/config/configtypes.py'),
|
'qutebrowser/config/configtypes.py'),
|
||||||
@ -133,6 +135,10 @@ PERFECT_FILES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# 100% coverage because of integration tests, but no perfect unit tests yet.
|
||||||
|
WHITELISTED_FILES = []
|
||||||
|
|
||||||
|
|
||||||
class Skipped(Exception):
|
class Skipped(Exception):
|
||||||
|
|
||||||
"""Exception raised when skipping coverage checks."""
|
"""Exception raised when skipping coverage checks."""
|
||||||
@ -199,7 +205,8 @@ def check(fileobj, perfect_files):
|
|||||||
text = "{} has {}% line and {}% branch coverage!".format(
|
text = "{} has {}% line and {}% branch coverage!".format(
|
||||||
filename, line_cov, branch_cov)
|
filename, line_cov, branch_cov)
|
||||||
messages.append(Message(MsgType.insufficent_coverage, text))
|
messages.append(Message(MsgType.insufficent_coverage, text))
|
||||||
elif filename not in perfect_src_files and not is_bad:
|
elif (filename not in perfect_src_files and not is_bad and
|
||||||
|
filename not in WHITELISTED_FILES):
|
||||||
text = ("{} has 100% coverage but is not in "
|
text = ("{} has 100% coverage but is not in "
|
||||||
"perfect_files!".format(filename))
|
"perfect_files!".format(filename))
|
||||||
messages.append(Message(MsgType.perfect_file, text))
|
messages.append(Message(MsgType.perfect_file, text))
|
||||||
|
@ -39,13 +39,16 @@ from helpers.messagemock import message_mock
|
|||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import objreg
|
from qutebrowser.utils import objreg
|
||||||
|
|
||||||
from PyQt5.QtCore import QEvent
|
from PyQt5.QtCore import QEvent, QSize, Qt, PYQT_VERSION
|
||||||
|
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
|
||||||
from PyQt5.QtNetwork import QNetworkCookieJar
|
from PyQt5.QtNetwork import QNetworkCookieJar
|
||||||
import xvfbwrapper
|
import xvfbwrapper
|
||||||
|
|
||||||
|
|
||||||
# Set hypothesis settings
|
# Set hypothesis settings
|
||||||
hypothesis.Settings.default.strict = True # pylint: disable=no-member
|
hypothesis.settings.register_profile('default',
|
||||||
|
hypothesis.settings(strict=True))
|
||||||
|
hypothesis.settings.load_profile('default')
|
||||||
|
|
||||||
|
|
||||||
def _apply_platform_markers(item):
|
def _apply_platform_markers(item):
|
||||||
@ -62,6 +65,9 @@ def _apply_platform_markers(item):
|
|||||||
"Can only run when frozen"),
|
"Can only run when frozen"),
|
||||||
('not_xvfb', item.config.xvfb_display is not None,
|
('not_xvfb', item.config.xvfb_display is not None,
|
||||||
"Can't be run with Xvfb."),
|
"Can't be run with Xvfb."),
|
||||||
|
('skip', True, "Always skipped."),
|
||||||
|
('pyqt531_or_newer', PYQT_VERSION < 0x050301,
|
||||||
|
"Needs PyQt 5.3.1 or newer"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for searched_marker, condition, default_reason in markers:
|
for searched_marker, condition, default_reason in markers:
|
||||||
@ -108,7 +114,7 @@ def pytest_collection_modifyitems(items):
|
|||||||
item.add_marker('gui')
|
item.add_marker('gui')
|
||||||
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
|
if sys.platform == 'linux' and not os.environ.get('DISPLAY', ''):
|
||||||
if ('CI' in os.environ and
|
if ('CI' in os.environ and
|
||||||
not os.environ.get('QUTE_NO_DISPLAY_OK', '')):
|
not os.environ.get('QUTE_NO_DISPLAY', '')):
|
||||||
raise Exception("No display available on CI!")
|
raise Exception("No display available on CI!")
|
||||||
skip_marker = pytest.mark.skipif(
|
skip_marker = pytest.mark.skipif(
|
||||||
True, reason="No DISPLAY available")
|
True, reason="No DISPLAY available")
|
||||||
@ -124,6 +130,8 @@ def pytest_collection_modifyitems(items):
|
|||||||
item.add_marker(pytest.mark.integration)
|
item.add_marker(pytest.mark.integration)
|
||||||
|
|
||||||
_apply_platform_markers(item)
|
_apply_platform_markers(item)
|
||||||
|
if item.get_marker('xfail_norun'):
|
||||||
|
item.add_marker(pytest.mark.xfail(run=False))
|
||||||
|
|
||||||
|
|
||||||
def pytest_ignore_collect(path):
|
def pytest_ignore_collect(path):
|
||||||
@ -161,6 +169,41 @@ class WinRegistryHelper:
|
|||||||
del objreg.window_registry[win_id]
|
del objreg.window_registry[win_id]
|
||||||
|
|
||||||
|
|
||||||
|
class FakeStatusBar(QWidget):
|
||||||
|
|
||||||
|
"""Fake statusbar to test progressbar sizing."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.hbox = QHBoxLayout(self)
|
||||||
|
self.hbox.addStretch()
|
||||||
|
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||||
|
self.setStyleSheet('background-color: red;')
|
||||||
|
|
||||||
|
def minimumSizeHint(self):
|
||||||
|
return QSize(1, self.fontMetrics().height())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_statusbar(qtbot):
|
||||||
|
"""Fixture providing a statusbar in a container window."""
|
||||||
|
container = QWidget()
|
||||||
|
qtbot.add_widget(container)
|
||||||
|
vbox = QVBoxLayout(container)
|
||||||
|
vbox.addStretch()
|
||||||
|
|
||||||
|
statusbar = FakeStatusBar(container)
|
||||||
|
# to make sure container isn't GCed
|
||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
statusbar.container = container
|
||||||
|
vbox.addWidget(statusbar)
|
||||||
|
|
||||||
|
container.show()
|
||||||
|
qtbot.waitForWindowShown(container)
|
||||||
|
return statusbar
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
@pytest.yield_fixture
|
||||||
def win_registry():
|
def win_registry():
|
||||||
"""Fixture providing a window registry for win_id 0 and 1."""
|
"""Fixture providing a window registry for win_id 0 and 1."""
|
||||||
@ -373,7 +416,10 @@ def pytest_configure(config):
|
|||||||
if os.environ.get('DISPLAY', None) == '':
|
if os.environ.get('DISPLAY', None) == '':
|
||||||
# xvfbwrapper doesn't handle DISPLAY="" correctly
|
# xvfbwrapper doesn't handle DISPLAY="" correctly
|
||||||
del os.environ['DISPLAY']
|
del os.environ['DISPLAY']
|
||||||
if sys.platform.startswith('linux') and not config.getoption('--no-xvfb'):
|
|
||||||
|
if (sys.platform.startswith('linux') and
|
||||||
|
not config.getoption('--no-xvfb') and
|
||||||
|
'QUTE_NO_DISPLAY' not in os.environ):
|
||||||
assert 'QUTE_BUILDBOT' not in os.environ
|
assert 'QUTE_BUILDBOT' not in os.environ
|
||||||
try:
|
try:
|
||||||
disp = xvfbwrapper.Xvfb(width=800, height=600, colordepth=16)
|
disp = xvfbwrapper.Xvfb(width=800, height=600, colordepth=16)
|
||||||
|
@ -21,6 +21,6 @@
|
|||||||
|
|
||||||
"""Things needed for integration testing."""
|
"""Things needed for integration testing."""
|
||||||
|
|
||||||
from webserver import httpbin, httpbin_after_test
|
from webserver import httpbin, httpbin_after_test, ssl_server
|
||||||
from quteprocess import quteproc_process, quteproc
|
from quteprocess import quteproc_process, quteproc
|
||||||
from testprocess import pytest_runtest_makereport
|
from testprocess import pytest_runtest_makereport
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<title>Caret mode</title>
|
<title>Caret mode</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>one two three<br/>eins zwei drei</p>
|
<p><a href="/data/hello.txt">one</a> two three<br/>eins zwei drei</p>
|
||||||
<p>four five six<br/>vier fünf sechs</p>
|
<p>four five six<br/>vier fünf sechs</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -11,8 +11,10 @@ body .c { color: #408080; font-style: italic } /* Comment */
|
|||||||
body .err { border: 1px solid #FF0000 } /* Error */
|
body .err { border: 1px solid #FF0000 } /* Error */
|
||||||
body .k { color: #008000; font-weight: bold } /* Keyword */
|
body .k { color: #008000; font-weight: bold } /* Keyword */
|
||||||
body .o { color: #666666 } /* Operator */
|
body .o { color: #666666 } /* Operator */
|
||||||
|
body .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
|
||||||
body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||||
body .cp { color: #BC7A00 } /* Comment.Preproc */
|
body .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||||
|
body .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
|
||||||
body .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
body .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||||
body .cs { color: #408080; font-style: italic } /* Comment.Special */
|
body .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||||
body .gd { color: #A00000 } /* Generic.Deleted */
|
body .gd { color: #A00000 } /* Generic.Deleted */
|
||||||
@ -75,8 +77,8 @@ body .il { color: #666666 } /* Literal.Number.Integer.Long */
|
|||||||
<h2></h2>
|
<h2></h2>
|
||||||
|
|
||||||
<table class="highlighttable"><tbody><tr><td class="linenos"><div class="linenodiv"><pre>1
|
<table class="highlighttable"><tbody><tr><td class="linenos"><div class="linenodiv"><pre>1
|
||||||
2</pre></div></td><td class="code"><div class="highlight"><pre><span class="nt"><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!
|
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="nt"></pre></body></html></span>
|
<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>
|
</pre></div>
|
||||||
</td></tr></tbody></table>
|
</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/2.txt
|
||||||
- url: http://localhost:*/data/backforward/3.txt
|
- url: http://localhost:*/data/backforward/3.txt
|
||||||
|
|
||||||
|
Scenario: Going back too much with count.
|
||||||
|
Given I open data/backforward/1.txt
|
||||||
|
When I open data/backforward/2.txt
|
||||||
|
And I open data/backforward/3.txt
|
||||||
|
And I run :back with count 3
|
||||||
|
Then the error "At beginning of history." should be shown
|
||||||
|
|
||||||
Scenario: Going back with very big count.
|
Scenario: Going back with very big count.
|
||||||
Given I open data/backforward/1.txt
|
Given I open data/backforward/1.txt
|
||||||
When I run :back with count 99999999999
|
When I run :back with count 99999999999
|
||||||
@ -132,3 +139,11 @@ Feature: Going back and forward.
|
|||||||
Given I open data/backforward/1.txt
|
Given I open data/backforward/1.txt
|
||||||
When I run :forward
|
When I run :forward
|
||||||
Then the error "At end of history." should be shown
|
Then the error "At end of history." should be shown
|
||||||
|
|
||||||
|
Scenario: Going forward too much with count.
|
||||||
|
Given I open data/backforward/1.txt
|
||||||
|
When I open data/backforward/2.txt
|
||||||
|
And I open data/backforward/3.txt
|
||||||
|
And I run :back with count 2
|
||||||
|
And I run :forward with count 3
|
||||||
|
Then the error "At end of history." should be shown
|
||||||
|
@ -3,7 +3,7 @@ Feature: Caret mode
|
|||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given I open data/caret.html
|
Given I open data/caret.html
|
||||||
And I run :enter-mode caret
|
And I run :tab-only ;; :enter-mode caret
|
||||||
|
|
||||||
# document
|
# document
|
||||||
|
|
||||||
@ -258,3 +258,63 @@ Feature: Caret mode
|
|||||||
Then the message "3 chars yanked to clipboard" should be shown.
|
Then the message "3 chars yanked to clipboard" should be shown.
|
||||||
And the message "7 chars yanked to clipboard" should be shown.
|
And the message "7 chars yanked to clipboard" should be shown.
|
||||||
And the clipboard should contain "one two"
|
And the clipboard should contain "one two"
|
||||||
|
|
||||||
|
# :drop-selection
|
||||||
|
|
||||||
|
Scenario: :drop-selection
|
||||||
|
When I run :toggle-selection
|
||||||
|
And I run :move-to-end-of-word
|
||||||
|
And I run :drop-selection
|
||||||
|
And I run :yank-selected
|
||||||
|
Then the message "Nothing to yank" should be shown.
|
||||||
|
|
||||||
|
# :follow-selected
|
||||||
|
|
||||||
|
Scenario: :follow-selected without a selection
|
||||||
|
When I run :follow-selected
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
Scenario: :follow-selected with text
|
||||||
|
When I run :move-to-next-word
|
||||||
|
And I run :toggle-selection
|
||||||
|
And I run :move-to-end-of-word
|
||||||
|
And I run :follow-selected
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
Scenario: :follow-selected with link (with JS)
|
||||||
|
When I set content -> allow-javascript to true
|
||||||
|
And I run :toggle-selection
|
||||||
|
And I run :move-to-end-of-word
|
||||||
|
And I run :follow-selected
|
||||||
|
Then data/hello.txt should be loaded
|
||||||
|
|
||||||
|
Scenario: :follow-selected with link (without JS)
|
||||||
|
When I set content -> allow-javascript to false
|
||||||
|
And I run :toggle-selection
|
||||||
|
And I run :move-to-end-of-word
|
||||||
|
And I run :follow-selected
|
||||||
|
Then data/hello.txt should be loaded
|
||||||
|
|
||||||
|
Scenario: :follow-selected with --tab (with JS)
|
||||||
|
When I set content -> allow-javascript to true
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :enter-mode caret
|
||||||
|
And I run :toggle-selection
|
||||||
|
And I run :move-to-end-of-word
|
||||||
|
And I run :follow-selected --tab
|
||||||
|
Then data/hello.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- data/caret.html
|
||||||
|
- data/hello.txt (active)
|
||||||
|
|
||||||
|
Scenario: :follow-selected with --tab (without JS)
|
||||||
|
When I set content -> allow-javascript to false
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :enter-mode caret
|
||||||
|
And I run :toggle-selection
|
||||||
|
And I run :move-to-end-of-word
|
||||||
|
And I run :follow-selected --tab
|
||||||
|
Then data/hello.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- data/caret.html
|
||||||
|
- data/hello.txt (active)
|
||||||
|
@ -25,6 +25,7 @@ import json
|
|||||||
import os.path
|
import os.path
|
||||||
import logging
|
import logging
|
||||||
import collections
|
import collections
|
||||||
|
import textwrap
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
@ -35,6 +36,11 @@ from PyQt5.QtGui import QClipboard
|
|||||||
from helpers import utils
|
from helpers import utils
|
||||||
|
|
||||||
|
|
||||||
|
class WaitForClipboardTimeout(Exception):
|
||||||
|
|
||||||
|
"""Raised when _wait_for_clipboard didn't get the expected message."""
|
||||||
|
|
||||||
|
|
||||||
def _clipboard_mode(qapp, what):
|
def _clipboard_mode(qapp, what):
|
||||||
"""Get the QClipboard::Mode to use based on a string."""
|
"""Get the QClipboard::Mode to use based on a string."""
|
||||||
if what == 'clipboard':
|
if what == 'clipboard':
|
||||||
@ -68,6 +74,7 @@ def open_path_given(quteproc, path):
|
|||||||
It always opens a new tab, unlike "When I open ..."
|
It always opens a new tab, unlike "When I open ..."
|
||||||
"""
|
"""
|
||||||
quteproc.open_path(path, new_tab=True)
|
quteproc.open_path(path, new_tab=True)
|
||||||
|
quteproc.wait_for_load_finished(path)
|
||||||
|
|
||||||
|
|
||||||
@bdd.given(bdd.parsers.parse("I run {command}"))
|
@bdd.given(bdd.parsers.parse("I run {command}"))
|
||||||
@ -93,17 +100,32 @@ def fresh_instance(quteproc):
|
|||||||
def open_path(quteproc, path):
|
def open_path(quteproc, path):
|
||||||
"""Open a URL.
|
"""Open a URL.
|
||||||
|
|
||||||
If used like "When I open ... in a new tab", the URL is opened ina new
|
If used like "When I open ... in a new tab", the URL is opened in a new
|
||||||
tab.
|
tab. With "... in a new window", it's opened in a new window.
|
||||||
"""
|
"""
|
||||||
|
new_tab = False
|
||||||
|
new_window = False
|
||||||
|
wait_for_load_finished = True
|
||||||
|
|
||||||
new_tab_suffix = ' in a new tab'
|
new_tab_suffix = ' in a new tab'
|
||||||
|
new_window_suffix = ' in a new window'
|
||||||
|
do_not_wait_suffix = ' without waiting'
|
||||||
|
|
||||||
if path.endswith(new_tab_suffix):
|
if path.endswith(new_tab_suffix):
|
||||||
path = path[:-len(new_tab_suffix)]
|
path = path[:-len(new_tab_suffix)]
|
||||||
new_tab = True
|
new_tab = True
|
||||||
else:
|
elif path.endswith(new_window_suffix):
|
||||||
new_tab = False
|
path = path[:-len(new_window_suffix)]
|
||||||
|
new_window = True
|
||||||
|
|
||||||
quteproc.open_path(path, new_tab=new_tab)
|
if path.endswith(do_not_wait_suffix):
|
||||||
|
path = path[:-len(do_not_wait_suffix)]
|
||||||
|
wait_for_load_finished = False
|
||||||
|
|
||||||
|
quteproc.open_path(path, new_tab=new_tab, new_window=new_window)
|
||||||
|
|
||||||
|
if wait_for_load_finished:
|
||||||
|
quteproc.wait_for_load_finished(path)
|
||||||
|
|
||||||
|
|
||||||
@bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}"))
|
@bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}"))
|
||||||
@ -131,7 +153,7 @@ def run_command(quteproc, httpbin, command):
|
|||||||
@bdd.when(bdd.parsers.parse("I reload"))
|
@bdd.when(bdd.parsers.parse("I reload"))
|
||||||
def reload(qtbot, httpbin, quteproc, command):
|
def reload(qtbot, httpbin, quteproc, command):
|
||||||
"""Reload and wait until a new request is received."""
|
"""Reload and wait until a new request is received."""
|
||||||
with qtbot.waitSignal(httpbin.new_request, raising=True):
|
with qtbot.waitSignal(httpbin.new_request):
|
||||||
quteproc.send_cmd(':reload')
|
quteproc.send_cmd(':reload')
|
||||||
|
|
||||||
|
|
||||||
@ -142,8 +164,9 @@ def wait_until_loaded(quteproc, path):
|
|||||||
|
|
||||||
|
|
||||||
@bdd.when(bdd.parsers.re(r'I wait for (?P<is_regex>regex )?"'
|
@bdd.when(bdd.parsers.re(r'I wait for (?P<is_regex>regex )?"'
|
||||||
r'(?P<pattern>[^"]+)" in the log'))
|
r'(?P<pattern>[^"]+)" in the log(?P<do_skip> or skip '
|
||||||
def wait_in_log(quteproc, is_regex, pattern):
|
r'the test)?'))
|
||||||
|
def wait_in_log(quteproc, is_regex, pattern, do_skip):
|
||||||
"""Wait for a given pattern in the qutebrowser log.
|
"""Wait for a given pattern in the qutebrowser log.
|
||||||
|
|
||||||
If used like "When I wait for regex ... in the log" the argument is treated
|
If used like "When I wait for regex ... in the log" the argument is treated
|
||||||
@ -151,7 +174,9 @@ def wait_in_log(quteproc, is_regex, pattern):
|
|||||||
"""
|
"""
|
||||||
if is_regex:
|
if is_regex:
|
||||||
pattern = re.compile(pattern)
|
pattern = re.compile(pattern)
|
||||||
quteproc.wait_for(message=pattern)
|
|
||||||
|
line = quteproc.wait_for(message=pattern, do_skip=bool(do_skip))
|
||||||
|
line.expected = True
|
||||||
|
|
||||||
|
|
||||||
@bdd.when(bdd.parsers.re(r'I wait for the (?P<category>error|message|warning) '
|
@bdd.when(bdd.parsers.re(r'I wait for the (?P<category>error|message|warning) '
|
||||||
@ -191,11 +216,33 @@ def fill_clipboard(qtbot, qapp, httpbin, what, content):
|
|||||||
clipboard.setText(content, mode)
|
clipboard.setText(content, mode)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.when(bdd.parsers.re(r'I put the following lines into the '
|
||||||
|
r'(?P<what>primary selection|clipboard):\n'
|
||||||
|
r'(?P<content>.+)$', flags=re.DOTALL))
|
||||||
|
def fill_clipboard_multiline(qtbot, qapp, httpbin, what, content):
|
||||||
|
fill_clipboard(qtbot, qapp, httpbin, what, textwrap.dedent(content))
|
||||||
|
|
||||||
|
|
||||||
## Then
|
## Then
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.parse("{path} should be loaded"))
|
@bdd.then(bdd.parsers.parse("{path} should be loaded"))
|
||||||
def path_should_be_loaded(httpbin, path):
|
def path_should_be_loaded(quteproc, path):
|
||||||
|
"""Make sure the given path was loaded according to the log.
|
||||||
|
|
||||||
|
This is usally the better check compared to "should be requested" as the
|
||||||
|
page could be loaded from local cache.
|
||||||
|
"""
|
||||||
|
url = quteproc.path_to_url(path)
|
||||||
|
pattern = re.compile(
|
||||||
|
r"load status for <qutebrowser\.browser\.webview\.WebView "
|
||||||
|
r"tab_id=\d+ url='{url}/?'>: LoadStatus\.success".format(
|
||||||
|
url=re.escape(url)))
|
||||||
|
quteproc.wait_for(message=pattern)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse("{path} should be requested"))
|
||||||
|
def path_should_be_requested(httpbin, path):
|
||||||
"""Make sure the given path was loaded from the webserver."""
|
"""Make sure the given path was loaded from the webserver."""
|
||||||
httpbin.wait_for(verb='GET', path='/' + path)
|
httpbin.wait_for(verb='GET', path='/' + path)
|
||||||
|
|
||||||
@ -243,7 +290,8 @@ def should_be_logged(quteproc, is_regex, pattern):
|
|||||||
"""Expect the given pattern on regex in the log."""
|
"""Expect the given pattern on regex in the log."""
|
||||||
if is_regex:
|
if is_regex:
|
||||||
pattern = re.compile(pattern)
|
pattern = re.compile(pattern)
|
||||||
quteproc.wait_for(message=pattern)
|
line = quteproc.wait_for(message=pattern)
|
||||||
|
line.expected = True
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.parse('"{pattern}" should not be logged'))
|
@bdd.then(bdd.parsers.parse('"{pattern}" should not be logged'))
|
||||||
@ -256,8 +304,7 @@ def ensure_not_logged(quteproc, pattern):
|
|||||||
'logged'))
|
'logged'))
|
||||||
def javascript_message_logged(quteproc, message):
|
def javascript_message_logged(quteproc, message):
|
||||||
"""Make sure the given message was logged via javascript."""
|
"""Make sure the given message was logged via javascript."""
|
||||||
quteproc.wait_for(category='js', function='javaScriptConsoleMessage',
|
quteproc.wait_for_js(message)
|
||||||
message='[*] {}'.format(message))
|
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.parse('the javascript message "{message}" should not be '
|
@bdd.then(bdd.parsers.parse('the javascript message "{message}" should not be '
|
||||||
@ -316,7 +363,24 @@ def check_contents(quteproc, filename):
|
|||||||
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..',
|
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..',
|
||||||
'data', os.path.join(*filename.split('/')))
|
'data', os.path.join(*filename.split('/')))
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
assert content == f.read()
|
file_content = f.read()
|
||||||
|
assert content == file_content
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('the page should contain the plaintext "{text}"'))
|
||||||
|
def check_contents_plain(quteproc, text):
|
||||||
|
"""Check the current page's content based on a substring."""
|
||||||
|
content = quteproc.get_content().strip()
|
||||||
|
assert text in content
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('the json on the page should be:\n{text}'))
|
||||||
|
def check_contents_json(quteproc, text):
|
||||||
|
"""Check the current page's content as json."""
|
||||||
|
content = quteproc.get_content().strip()
|
||||||
|
expected = json.loads(text)
|
||||||
|
actual = json.loads(content)
|
||||||
|
assert actual == expected
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}"))
|
@bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}"))
|
||||||
@ -335,6 +399,7 @@ def check_open_tabs(quteproc, tabs):
|
|||||||
|
|
||||||
for i, line in enumerate(tabs):
|
for i, line in enumerate(tabs):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
|
assert line.startswith('- ')
|
||||||
line = line[2:] # remove "- " prefix
|
line = line[2:] # remove "- " prefix
|
||||||
if line.endswith(active_suffix):
|
if line.endswith(active_suffix):
|
||||||
path = line[:-len(active_suffix)]
|
path = line[:-len(active_suffix)]
|
||||||
@ -359,16 +424,27 @@ def _wait_for_clipboard(qtbot, clipboard, mode, expected):
|
|||||||
while True:
|
while True:
|
||||||
if clipboard.text(mode=mode) == expected:
|
if clipboard.text(mode=mode) == expected:
|
||||||
return
|
return
|
||||||
with qtbot.waitSignal(clipboard.changed, timeout=timeout) as blocker:
|
|
||||||
|
# We need to poll the clipboard, as for some reason it can change with
|
||||||
|
# emitting changed (?).
|
||||||
|
with qtbot.waitSignal(clipboard.changed, timeout=100, raising=False):
|
||||||
pass
|
pass
|
||||||
if not blocker.signal_triggered or timer.hasExpired(timeout):
|
|
||||||
|
if timer.hasExpired(timeout):
|
||||||
mode_names = {
|
mode_names = {
|
||||||
QClipboard.Clipboard: 'clipboard',
|
QClipboard.Clipboard: 'clipboard',
|
||||||
QClipboard.Selection: 'primary selection',
|
QClipboard.Selection: 'primary selection',
|
||||||
}
|
}
|
||||||
raise WaitForTimeout(
|
raise WaitForClipboardTimeout(
|
||||||
"Timed out after {}ms waiting for {} in {}.".format(
|
"Timed out after {timeout}ms waiting for {what}:\n"
|
||||||
timeout, expected, mode_names[mode]))
|
" expected: {expected!r}\n"
|
||||||
|
" clipboard: {clipboard!r}\n"
|
||||||
|
" primary: {primary!r}.".format(
|
||||||
|
timeout=timeout, what=mode_names[mode],
|
||||||
|
expected=expected,
|
||||||
|
clipboard=clipboard.text(mode=QClipboard.Clipboard),
|
||||||
|
primary=clipboard.text(mode=QClipboard.Selection))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
|
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
|
||||||
@ -382,6 +458,13 @@ def clipboard_contains(qtbot, qapp, httpbin, what, content):
|
|||||||
|
|
||||||
@bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}'))
|
@bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}'))
|
||||||
def clipboard_contains_multiline(qtbot, qapp, content):
|
def clipboard_contains_multiline(qtbot, qapp, content):
|
||||||
expected = '\n'.join(line.strip() for line in content.splitlines())
|
expected = textwrap.dedent(content)
|
||||||
_wait_for_clipboard(qtbot, qapp.clipboard(), QClipboard.Clipboard,
|
_wait_for_clipboard(qtbot, qapp.clipboard(), QClipboard.Clipboard,
|
||||||
expected)
|
expected)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then("qutebrowser should quit")
|
||||||
|
def should_quit(qtbot, quteproc):
|
||||||
|
quteproc.exit_expected = True
|
||||||
|
with qtbot.waitSignal(quteproc.proc.finished, timeout=5000):
|
||||||
|
pass
|
||||||
|
@ -117,8 +117,25 @@ Feature: Various utility commands.
|
|||||||
And I wait for "Focus object changed: *" in the log
|
And I wait for "Focus object changed: *" in the log
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
|
# Different code path as an inspector got created now
|
||||||
|
Scenario: Inspector without developer extras (after smoke)
|
||||||
|
When I set general -> developer-extras to false
|
||||||
|
And I run :inspector
|
||||||
|
Then the error "Please enable developer-extras before using the webinspector!" should be shown
|
||||||
|
|
||||||
|
# Different code path as an inspector got created now
|
||||||
|
@not_xvfb @posix
|
||||||
|
Scenario: Inspector smoke test 2
|
||||||
|
When I set general -> developer-extras to true
|
||||||
|
And I run :inspector
|
||||||
|
And I wait for "Focus object changed: <PyQt5.QtWebKitWidgets.QWebView object at *>" in the log
|
||||||
|
And I run :inspector
|
||||||
|
And I wait for "Focus object changed: *" in the log
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
# :stop/:reload
|
# :stop/:reload
|
||||||
|
|
||||||
|
# WORKAROUND for https://bitbucket.org/cherrypy/cherrypy/pull-requests/117/
|
||||||
@not_osx
|
@not_osx
|
||||||
Scenario: :stop
|
Scenario: :stop
|
||||||
Given I have a fresh instance
|
Given I have a fresh instance
|
||||||
@ -135,13 +152,19 @@ Feature: Various utility commands.
|
|||||||
custom/redirect-later?delay=-1
|
custom/redirect-later?delay=-1
|
||||||
# no request on / because we stopped the redirect
|
# no request on / because we stopped the redirect
|
||||||
|
|
||||||
Scenario: :reload
|
Scenario: :stop with wrong count
|
||||||
When I open data/hello.txt
|
When I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :stop with count 2
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
Scenario: :reload
|
||||||
|
When I open data/reload.txt
|
||||||
And I run :reload
|
And I run :reload
|
||||||
And I wait until data/hello.txt is loaded
|
And I wait until data/reload.txt is loaded
|
||||||
Then the requests should be:
|
Then the requests should be:
|
||||||
data/hello.txt
|
data/reload.txt
|
||||||
data/hello.txt
|
data/reload.txt
|
||||||
|
|
||||||
Scenario: :reload with force
|
Scenario: :reload with force
|
||||||
When I open headers
|
When I open headers
|
||||||
@ -149,6 +172,12 @@ Feature: Various utility commands.
|
|||||||
And I wait until headers is loaded
|
And I wait until headers is loaded
|
||||||
Then the header Cache-Control should be set to no-cache
|
Then the header Cache-Control should be set to no-cache
|
||||||
|
|
||||||
|
Scenario: :reload with wrong count
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :reload with count 2
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
# :view-source
|
# :view-source
|
||||||
|
|
||||||
Scenario: :view-source
|
Scenario: :view-source
|
||||||
@ -260,3 +289,42 @@ Feature: Various utility commands.
|
|||||||
And I set storage -> prompt-download-directory to false
|
And I set storage -> prompt-download-directory to false
|
||||||
And I open data/misc/test.pdf
|
And I open data/misc/test.pdf
|
||||||
Then "Download finished" should be logged
|
Then "Download finished" should be logged
|
||||||
|
|
||||||
|
# :print
|
||||||
|
|
||||||
|
# Disabled because it causes weird segfaults and QPainter warnings in Qt...
|
||||||
|
@xfail_norun
|
||||||
|
Scenario: print preview
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :print --preview
|
||||||
|
And I wait for "Focus object changed: *" in the log
|
||||||
|
And I run :debug-pyeval QApplication.instance().activeModalWidget().close()
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
# On Windows/OS X, we get a "QPrintDialog: Cannot be used on non-native
|
||||||
|
# printers" qWarning.
|
||||||
|
#
|
||||||
|
# Disabled because it causes weird segfaults and QPainter warnings in Qt...
|
||||||
|
@xfail_norun
|
||||||
|
Scenario: print
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :print
|
||||||
|
And I wait for "Focus object changed: *" in the log or skip the test
|
||||||
|
And I run :debug-pyeval QApplication.instance().activeModalWidget().close()
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
|
# :pyeval
|
||||||
|
|
||||||
|
Scenario: Running :pyeval
|
||||||
|
When I run :debug-pyeval 1+1
|
||||||
|
And I wait until qute:pyeval is loaded
|
||||||
|
Then the page should contain the plaintext "2"
|
||||||
|
|
||||||
|
Scenario: Causing exception in :pyeval
|
||||||
|
When I run :debug-pyeval 1/0
|
||||||
|
And I wait until qute:pyeval is loaded
|
||||||
|
Then the page should contain the plaintext "ZeroDivisionError"
|
||||||
|
|
||||||
|
Scenario: Running :pyeval with --quiet
|
||||||
|
When I run :debug-pyeval --quiet 1+1
|
||||||
|
Then "pyeval output: 2" should be logged
|
||||||
|
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:
|
- tabs:
|
||||||
- history:
|
- history:
|
||||||
- url: http://localhost:*/data/numbers/2.txt
|
- url: http://localhost:*/data/numbers/2.txt
|
||||||
|
|
||||||
|
# :undo
|
||||||
|
|
||||||
|
Scenario: Undo without any closed tabs
|
||||||
|
Given I have a fresh instance
|
||||||
|
When I run :undo
|
||||||
|
Then the error "Nothing to undo!" should be shown
|
||||||
|
|
||||||
|
Scenario: Undo closing a tab
|
||||||
|
When I open data/numbers/1.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I open data/numbers/2.txt in a new tab
|
||||||
|
And I open data/numbers/3.txt
|
||||||
|
And I run :tab-close
|
||||||
|
And I run :undo
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- history:
|
||||||
|
- url: about:blank
|
||||||
|
- url: http://localhost:*/data/numbers/1.txt
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- url: http://localhost:*/data/numbers/2.txt
|
||||||
|
- url: http://localhost:*/data/numbers/3.txt
|
||||||
|
|
||||||
|
Scenario: Undo with auto-created last tab
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I set tabs -> last-close to blank
|
||||||
|
And I run :tab-close
|
||||||
|
And I run :undo
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- data/hello.txt (active)
|
||||||
|
|
||||||
|
Scenario: Undo with auto-created last tab, with history
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I open data/hello2.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I set tabs -> last-close to blank
|
||||||
|
And I run :tab-close
|
||||||
|
And I run :undo
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- data/hello2.txt (active)
|
||||||
|
|
||||||
|
Scenario: Undo with auto-created last tab (startpage)
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I set tabs -> last-close to startpage
|
||||||
|
And I set general -> startpage to http://localhost:(port)/data/numbers/4.txt,http://localhost:(port)/data/numbers/5.txt
|
||||||
|
And I run :tab-close
|
||||||
|
And I run :undo
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- data/hello.txt (active)
|
||||||
|
|
||||||
|
Scenario: Undo with auto-created last tab (default-page)
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I set tabs -> last-close to default-page
|
||||||
|
And I set general -> default-page to http://localhost:(port)/data/numbers/6.txt
|
||||||
|
And I run :tab-close
|
||||||
|
And I run :undo
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- data/hello.txt (active)
|
||||||
|
|
||||||
|
# last-close
|
||||||
|
|
||||||
|
Scenario: last-close = blank
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I set tabs -> last-close to blank
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :tab-close
|
||||||
|
And I wait until about:blank is loaded
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- about:blank (active)
|
||||||
|
|
||||||
|
Scenario: last-close = startpage
|
||||||
|
When I set general -> startpage to http://localhost:(port)/data/numbers/7.txt,http://localhost:(port)/data/numbers/8.txt
|
||||||
|
And I set tabs -> last-close to startpage
|
||||||
|
And I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :tab-close
|
||||||
|
And I wait until data/numbers/7.txt is loaded
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- data/numbers/7.txt (active)
|
||||||
|
|
||||||
|
Scenario: last-close = default-page
|
||||||
|
When I set general -> default-page to http://localhost:(port)/data/numbers/9.txt
|
||||||
|
And I set tabs -> last-close to default-page
|
||||||
|
And I open data/hello.txt
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :tab-close
|
||||||
|
And I wait until data/numbers/9.txt is loaded
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- data/numbers/9.txt (active)
|
||||||
|
|
||||||
|
Scenario: last-close = close
|
||||||
|
When I open data/hello.txt
|
||||||
|
And I set tabs -> last-close to close
|
||||||
|
And I run :tab-only
|
||||||
|
And I run :tab-close
|
||||||
|
Then qutebrowser should quit
|
||||||
|
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.scenarios('urlmarks.feature')
|
||||||
|
|
||||||
|
|
||||||
@bdd.then(bdd.parsers.parse('the bookmark file should contain "{expected}"'))
|
def _check_marks(quteproc, quickmarks, expected, contains):
|
||||||
def bookmark_file_contains(quteproc, expected):
|
"""Make sure the given line does (not) exist in the bookmarks.
|
||||||
bookmark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks',
|
|
||||||
|
Args:
|
||||||
|
quickmarks: True to check the quickmarks file instead of bookmarks.
|
||||||
|
expected: The line to search for.
|
||||||
|
contains: True if the line should be there, False otherwise.
|
||||||
|
"""
|
||||||
|
if quickmarks:
|
||||||
|
mark_file = os.path.join(quteproc.basedir, 'config', 'quickmarks')
|
||||||
|
else:
|
||||||
|
mark_file = os.path.join(quteproc.basedir, 'config', 'bookmarks',
|
||||||
'urls')
|
'urls')
|
||||||
|
|
||||||
quteproc.clear_data() # So we don't match old messages
|
quteproc.clear_data() # So we don't match old messages
|
||||||
quteproc.send_cmd(':save')
|
quteproc.send_cmd(':save')
|
||||||
quteproc.wait_for(message='Saved to {}'.format(bookmark_file))
|
quteproc.wait_for(message='Saved to {}'.format(mark_file))
|
||||||
|
|
||||||
with open(bookmark_file, 'r', encoding='utf-8') as f:
|
with open(mark_file, 'r', encoding='utf-8') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|
||||||
matched_line = any(
|
matched_line = any(
|
||||||
utils.pattern_match(pattern=expected, value=line.rstrip('\n'))
|
utils.pattern_match(pattern=expected, value=line.rstrip('\n'))
|
||||||
for line in lines)
|
for line in lines)
|
||||||
|
|
||||||
assert matched_line, lines
|
assert matched_line == contains, lines
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('the bookmark file should contain "{line}"'))
|
||||||
|
def bookmark_file_contains(quteproc, line):
|
||||||
|
_check_marks(quteproc, quickmarks=False, expected=line, contains=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('the bookmark file should not contain "{line}"'))
|
||||||
|
def bookmark_file_does_not_contain(quteproc, line):
|
||||||
|
_check_marks(quteproc, quickmarks=False, expected=line, contains=False)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('the quickmark file should contain "{line}"'))
|
||||||
|
def quickmark_file_contains(quteproc, line):
|
||||||
|
_check_marks(quteproc, quickmarks=True, expected=line, contains=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.then(bdd.parsers.parse('the quickmark file should not contain "{line}"'))
|
||||||
|
def quickmark_file_does_not_contain(quteproc, line):
|
||||||
|
_check_marks(quteproc, quickmarks=True, expected=line, contains=False)
|
||||||
|
@ -33,7 +33,7 @@ def skip_with_broken_clipboard(qtbot, qapp):
|
|||||||
"""
|
"""
|
||||||
clipboard = qapp.clipboard()
|
clipboard = qapp.clipboard()
|
||||||
|
|
||||||
with qtbot.waitSignal(clipboard.changed):
|
with qtbot.waitSignal(clipboard.changed, raising=False):
|
||||||
clipboard.setText("Does this work?")
|
clipboard.setText("Does this work?")
|
||||||
|
|
||||||
if clipboard.text() != "Does this work?":
|
if clipboard.text() != "Does this work?":
|
||||||
|
@ -1,7 +1,174 @@
|
|||||||
Feature: quickmarks and bookmarks
|
Feature: quickmarks and bookmarks
|
||||||
|
|
||||||
|
## bookmarks
|
||||||
|
|
||||||
Scenario: Saving a bookmark
|
Scenario: Saving a bookmark
|
||||||
When I open data/title.html
|
When I open data/title.html
|
||||||
And I run :bookmark-add
|
And I run :bookmark-add
|
||||||
Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown
|
Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown
|
||||||
And the bookmark file should contain "http://localhost:*/data/title.html Test title"
|
And the bookmark file should contain "http://localhost:*/data/title.html Test title"
|
||||||
|
|
||||||
|
Scenario: Saving a duplicate bookmark
|
||||||
|
Given I have a fresh instance
|
||||||
|
When I open data/title.html
|
||||||
|
And I run :bookmark-add
|
||||||
|
And I run :bookmark-add
|
||||||
|
Then the error "Bookmark already exists!" should be shown
|
||||||
|
|
||||||
|
Scenario: Loading a bookmark
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :bookmark-load http://localhost:(port)/data/numbers/1.txt
|
||||||
|
Then data/numbers/1.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- data/numbers/1.txt (active)
|
||||||
|
|
||||||
|
Scenario: Loading a bookmark in a new tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :bookmark-load -t http://localhost:(port)/data/numbers/2.txt
|
||||||
|
Then data/numbers/2.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- about:blank
|
||||||
|
- data/numbers/2.txt (active)
|
||||||
|
|
||||||
|
Scenario: Loading a bookmark in a background tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :bookmark-load -b http://localhost:(port)/data/numbers/3.txt
|
||||||
|
Then data/numbers/3.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- about:blank (active)
|
||||||
|
- data/numbers/3.txt
|
||||||
|
|
||||||
|
Scenario: Loading a bookmark in a new window
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :bookmark-load -w http://localhost:(port)/data/numbers/4.txt
|
||||||
|
And I wait until data/numbers/4.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: about:blank
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/numbers/4.txt
|
||||||
|
|
||||||
|
Scenario: Loading a bookmark with -t and -b
|
||||||
|
When I run :bookmark-load -t -b about:blank
|
||||||
|
Then the error "Only one of -t/-b/-w can be given!" should be shown
|
||||||
|
|
||||||
|
Scenario: Deleting a bookmark which does not exist
|
||||||
|
When I run :bookmark-del doesnotexist
|
||||||
|
Then the error "Bookmark 'doesnotexist' not found!" should be shown
|
||||||
|
|
||||||
|
Scenario: Deleting a bookmark
|
||||||
|
When I open data/numbers/5.txt
|
||||||
|
And I run :bookmark-add
|
||||||
|
And I run :bookmark-del http://localhost:(port)/data/numbers/5.txt
|
||||||
|
Then the bookmark file should not contain "http://localhost:*/data/numbers/5.txt "
|
||||||
|
|
||||||
|
## quickmarks
|
||||||
|
|
||||||
|
Scenario: Saving a quickmark (:quickmark-add)
|
||||||
|
When I run :quickmark-add http://localhost:(port)/data/numbers/6.txt six
|
||||||
|
Then the quickmark file should contain "six http://localhost:*/data/numbers/6.txt"
|
||||||
|
|
||||||
|
Scenario: Saving a quickmark (:quickmark-save)
|
||||||
|
When I open http://localhost:(port)/data/numbers/7.txt
|
||||||
|
And I run :quickmark-save
|
||||||
|
And I wait for "Entering mode KeyMode.prompt (reason: question asked)" in the log
|
||||||
|
And I press the keys "seven"
|
||||||
|
And I press the keys "<Enter>"
|
||||||
|
Then the quickmark file should contain "seven http://localhost:*/data/numbers/7.txt"
|
||||||
|
|
||||||
|
Scenario: Saving a duplicate quickmark (without override)
|
||||||
|
When I run :quickmark-add http://localhost:(port)/data/numbers/8.txt eight
|
||||||
|
And I run :quickmark-add http://localhost:(port)/data/numbers/8_2.txt eight
|
||||||
|
And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log
|
||||||
|
And I run :prompt-no
|
||||||
|
Then the quickmark file should contain "eight http://localhost:*/data/numbers/8.txt"
|
||||||
|
|
||||||
|
Scenario: Saving a duplicate quickmark (with override)
|
||||||
|
When I run :quickmark-add http://localhost:(port)/data/numbers/9.txt nine
|
||||||
|
And I run :quickmark-add http://localhost:(port)/data/numbers/9_2.txt nine
|
||||||
|
And I wait for "Entering mode KeyMode.yesno (reason: question asked)" in the log
|
||||||
|
And I run :prompt-yes
|
||||||
|
Then the quickmark file should contain "nine http://localhost:*/data/numbers/9_2.txt"
|
||||||
|
|
||||||
|
Scenario: Adding a quickmark with an empty name
|
||||||
|
When I run :quickmark-add about:blank ""
|
||||||
|
Then the error "Can't set mark with empty name!" should be shown
|
||||||
|
|
||||||
|
Scenario: Adding a quickmark with an empty URL
|
||||||
|
When I run :quickmark-add "" foo
|
||||||
|
Then the error "Can't set mark with empty URL!" should be shown
|
||||||
|
|
||||||
|
Scenario: Loading a quickmark
|
||||||
|
Given I have a fresh instance
|
||||||
|
When I run :quickmark-add http://localhost:(port)/data/numbers/10.txt ten
|
||||||
|
And I run :quickmark-load ten
|
||||||
|
Then data/numbers/10.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- data/numbers/10.txt (active)
|
||||||
|
|
||||||
|
Scenario: Loading a quickmark in a new tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :quickmark-add http://localhost:(port)/data/numbers/11.txt eleven
|
||||||
|
And I run :quickmark-load -t eleven
|
||||||
|
Then data/numbers/11.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- about:blank
|
||||||
|
- data/numbers/11.txt (active)
|
||||||
|
|
||||||
|
Scenario: Loading a quickmark in a background tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :quickmark-add http://localhost:(port)/data/numbers/12.txt twelve
|
||||||
|
And I run :quickmark-load -b twelve
|
||||||
|
Then data/numbers/12.txt should be loaded
|
||||||
|
And the following tabs should be open:
|
||||||
|
- about:blank (active)
|
||||||
|
- data/numbers/12.txt
|
||||||
|
|
||||||
|
Scenario: Loading a quickmark in a new window
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I run :quickmark-add http://localhost:(port)/data/numbers/13.txt thirteen
|
||||||
|
And I run :quickmark-load -w thirteen
|
||||||
|
And I wait until data/numbers/13.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: about:blank
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/numbers/13.txt
|
||||||
|
|
||||||
|
Scenario: Loading a quickmark which does not exist
|
||||||
|
When I run :quickmark-load -b doesnotexist
|
||||||
|
Then the error "Quickmark 'doesnotexist' does not exist!" should be shown
|
||||||
|
|
||||||
|
Scenario: Loading a quickmark with -t and -b
|
||||||
|
When I run :quickmark-add http://localhost:(port)/data/numbers/14.txt fourteen
|
||||||
|
When I run :quickmark-load -t -b fourteen
|
||||||
|
Then the error "Only one of -t/-b/-w can be given!" should be shown
|
||||||
|
|
||||||
|
Scenario: Deleting a quickmark which does not exist
|
||||||
|
When I run :quickmark-del doesnotexist
|
||||||
|
Then the error "Quickmark 'doesnotexist' not found!" should be shown
|
||||||
|
|
||||||
|
Scenario: Deleting a quickmark
|
||||||
|
When I run :quickmark-add http://localhost:(port)/data/numbers/15.txt fifteen
|
||||||
|
And I run :quickmark-del fifteen
|
||||||
|
Then the quickmark file should not contain "fourteen http://localhost:*/data/numbers/15.txt "
|
||||||
|
@ -98,3 +98,75 @@ Feature: Yanking and pasting.
|
|||||||
history:
|
history:
|
||||||
- active: true
|
- active: true
|
||||||
url: http://localhost:*/data/hello.txt
|
url: http://localhost:*/data/hello.txt
|
||||||
|
|
||||||
|
Scenario: Pasting an invalid URL
|
||||||
|
When I set general -> auto-search to false
|
||||||
|
And I put "foo bar" into the clipboard
|
||||||
|
And I run :paste
|
||||||
|
Then the error "Invalid URL" should be shown
|
||||||
|
|
||||||
|
Scenario: Pasting multiple urls in a new tab
|
||||||
|
Given I have a fresh instance
|
||||||
|
When I put the following lines into the clipboard:
|
||||||
|
http://localhost:(port)/data/hello.txt
|
||||||
|
http://localhost:(port)/data/hello2.txt
|
||||||
|
http://localhost:(port)/data/hello3.txt
|
||||||
|
And I run :paste -t
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
And I wait until data/hello2.txt is loaded
|
||||||
|
And I wait until data/hello3.txt is loaded
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- about:blank
|
||||||
|
- data/hello.txt (active)
|
||||||
|
- data/hello2.txt
|
||||||
|
- data/hello3.txt
|
||||||
|
|
||||||
|
Scenario: Pasting multiple urls in a background tab
|
||||||
|
Given I open about:blank
|
||||||
|
When I run :tab-only
|
||||||
|
And I put the following lines into the clipboard:
|
||||||
|
http://localhost:(port)/data/hello.txt
|
||||||
|
http://localhost:(port)/data/hello2.txt
|
||||||
|
http://localhost:(port)/data/hello3.txt
|
||||||
|
And I run :paste -b
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
And I wait until data/hello2.txt is loaded
|
||||||
|
And I wait until data/hello3.txt is loaded
|
||||||
|
Then the following tabs should be open:
|
||||||
|
- about:blank (active)
|
||||||
|
- data/hello.txt
|
||||||
|
- data/hello2.txt
|
||||||
|
- data/hello3.txt
|
||||||
|
|
||||||
|
Scenario: Pasting multiple urls in new windows
|
||||||
|
Given I have a fresh instance
|
||||||
|
When I put the following lines into the clipboard:
|
||||||
|
http://localhost:(port)/data/hello.txt
|
||||||
|
http://localhost:(port)/data/hello2.txt
|
||||||
|
http://localhost:(port)/data/hello3.txt
|
||||||
|
And I run :paste -w
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
And I wait until data/hello2.txt is loaded
|
||||||
|
And I wait until data/hello3.txt is loaded
|
||||||
|
Then the session should look like:
|
||||||
|
windows:
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: about:blank
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/hello.txt
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/hello2.txt
|
||||||
|
- tabs:
|
||||||
|
- active: true
|
||||||
|
history:
|
||||||
|
- active: true
|
||||||
|
url: http://localhost:*/data/hello3.txt
|
||||||
|
@ -55,3 +55,7 @@ Feature: Zooming in and out
|
|||||||
Scenario: Setting zoom with very big count
|
Scenario: Setting zoom with very big count
|
||||||
When I run :zoom with count 99999999999
|
When I run :zoom with count 99999999999
|
||||||
Then the message "Zoom level: 99999999999%" should be shown
|
Then the message "Zoom level: 99999999999%" should be shown
|
||||||
|
|
||||||
|
Scenario: Setting zoom with argument and count
|
||||||
|
When I run :zoom 50 with count 60
|
||||||
|
Then the error "Both count and argument given!" should be shown
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
"""Fixtures to run qutebrowser in a QProcess and communicate."""
|
"""Fixtures to run qutebrowser in a QProcess and communicate."""
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@ -35,6 +36,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl
|
|||||||
import testprocess
|
import testprocess
|
||||||
from qutebrowser.misc import ipc
|
from qutebrowser.misc import ipc
|
||||||
from qutebrowser.utils import log, utils
|
from qutebrowser.utils import log, utils
|
||||||
|
from helpers import utils as testutils
|
||||||
|
|
||||||
|
|
||||||
def is_ignored_qt_message(message):
|
def is_ignored_qt_message(message):
|
||||||
@ -169,12 +171,7 @@ class QuteProc(testprocess.Process):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issues/717/
|
self._log(line)
|
||||||
# we should switch to generated-members after that
|
|
||||||
# pylint: disable=no-member
|
|
||||||
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
|
|
||||||
pytest.config.getoption('--verbose')):
|
|
||||||
self._log(line)
|
|
||||||
|
|
||||||
start_okay_message_load = (
|
start_okay_message_load = (
|
||||||
"load status for <qutebrowser.browser.webview.WebView tab_id=0 "
|
"load status for <qutebrowser.browser.webview.WebView tab_id=0 "
|
||||||
@ -197,7 +194,7 @@ class QuteProc(testprocess.Process):
|
|||||||
log_line.function == 'init' and
|
log_line.function == 'init' and
|
||||||
log_line.message.startswith('Base directory:')):
|
log_line.message.startswith('Base directory:')):
|
||||||
self.basedir = log_line.message.split(':', maxsplit=1)[1].strip()
|
self.basedir = log_line.message.split(':', maxsplit=1)[1].strip()
|
||||||
elif log_line.loglevel > logging.INFO:
|
elif self._is_error_logline(log_line):
|
||||||
self.got_error.emit()
|
self.got_error.emit()
|
||||||
|
|
||||||
return log_line
|
return log_line
|
||||||
@ -214,7 +211,7 @@ class QuteProc(testprocess.Process):
|
|||||||
'about:blank']
|
'about:blank']
|
||||||
return executable, args
|
return executable, args
|
||||||
|
|
||||||
def path_to_url(self, path):
|
def path_to_url(self, path, *, port=None, https=False):
|
||||||
"""Get a URL based on a filename for the localhost webserver.
|
"""Get a URL based on a filename for the localhost webserver.
|
||||||
|
|
||||||
URLs like about:... and qute:... are handled specially and returned
|
URLs like about:... and qute:... are handled specially and returned
|
||||||
@ -223,18 +220,53 @@ class QuteProc(testprocess.Process):
|
|||||||
if path.startswith('about:') or path.startswith('qute:'):
|
if path.startswith('about:') or path.startswith('qute:'):
|
||||||
return path
|
return path
|
||||||
else:
|
else:
|
||||||
return 'http://localhost:{}/{}'.format(
|
return '{}://localhost:{}/{}'.format(
|
||||||
self._httpbin.port,
|
'https' if https else 'http',
|
||||||
|
self._httpbin.port if port is None else port,
|
||||||
path if path != '/' else '')
|
path if path != '/' else '')
|
||||||
|
|
||||||
|
def wait_for_js(self, message):
|
||||||
|
"""Wait for the given javascript console message."""
|
||||||
|
self.wait_for(category='js', function='javaScriptConsoleMessage',
|
||||||
|
message='[*] {}'.format(message))
|
||||||
|
|
||||||
|
def _is_error_logline(self, msg):
|
||||||
|
"""Check if the given LogLine is some kind of error message."""
|
||||||
|
is_js_error = (msg.category == 'js' and
|
||||||
|
msg.function == 'javaScriptConsoleMessage' and
|
||||||
|
testutils.pattern_match(pattern='[*] [FAIL] *',
|
||||||
|
value=msg.message))
|
||||||
|
return msg.loglevel > logging.INFO or is_js_error
|
||||||
|
|
||||||
|
def _maybe_skip(self):
|
||||||
|
"""Skip the test if [SKIP] lines were logged."""
|
||||||
|
skip_texts = []
|
||||||
|
|
||||||
|
for msg in self._data:
|
||||||
|
if (msg.category == 'js' and
|
||||||
|
msg.function == 'javaScriptConsoleMessage' and
|
||||||
|
testutils.pattern_match(pattern='[*] [SKIP] *',
|
||||||
|
value=msg.message)):
|
||||||
|
skip_texts.append(msg.message.partition(' [SKIP] ')[2])
|
||||||
|
|
||||||
|
if skip_texts:
|
||||||
|
pytest.skip(', '.join(skip_texts))
|
||||||
|
|
||||||
def after_test(self):
|
def after_test(self):
|
||||||
bad_msgs = [msg for msg in self._data
|
bad_msgs = [msg for msg in self._data
|
||||||
if msg.loglevel > logging.INFO and not msg.expected]
|
if self._is_error_logline(msg) and not msg.expected]
|
||||||
super().after_test()
|
|
||||||
if bad_msgs:
|
try:
|
||||||
text = 'Logged unexpected errors:\n\n' + '\n'.join(
|
if bad_msgs:
|
||||||
str(e) for e in bad_msgs)
|
text = 'Logged unexpected errors:\n\n' + '\n'.join(
|
||||||
pytest.fail(text, pytrace=False)
|
str(e) for e in bad_msgs)
|
||||||
|
# We'd like to use pytrace=False here but don't as a WORKAROUND
|
||||||
|
# for https://github.com/pytest-dev/pytest/issues/1316
|
||||||
|
pytest.fail(text)
|
||||||
|
else:
|
||||||
|
self._maybe_skip()
|
||||||
|
finally:
|
||||||
|
super().after_test()
|
||||||
|
|
||||||
def send_cmd(self, command, count=None):
|
def send_cmd(self, command, count=None):
|
||||||
"""Send a command to the running qutebrowser instance."""
|
"""Send a command to the running qutebrowser instance."""
|
||||||
@ -269,14 +301,19 @@ class QuteProc(testprocess.Process):
|
|||||||
yield
|
yield
|
||||||
self.set_setting(sect, opt, old_value)
|
self.set_setting(sect, opt, old_value)
|
||||||
|
|
||||||
def open_path(self, path, new_tab=False):
|
def open_path(self, path, *, new_tab=False, new_window=False, port=None,
|
||||||
|
https=False):
|
||||||
"""Open the given path on the local webserver in qutebrowser."""
|
"""Open the given path on the local webserver in qutebrowser."""
|
||||||
url = self.path_to_url(path)
|
if new_tab and new_window:
|
||||||
|
raise ValueError("new_tab and new_window given!")
|
||||||
|
|
||||||
|
url = self.path_to_url(path, port=port, https=https)
|
||||||
if new_tab:
|
if new_tab:
|
||||||
self.send_cmd(':open -t ' + url)
|
self.send_cmd(':open -t ' + url)
|
||||||
|
elif new_window:
|
||||||
|
self.send_cmd(':open -w ' + url)
|
||||||
else:
|
else:
|
||||||
self.send_cmd(':open ' + url)
|
self.send_cmd(':open ' + url)
|
||||||
self.wait_for_load_finished(path)
|
|
||||||
|
|
||||||
def mark_expected(self, category=None, loglevel=None, message=None):
|
def mark_expected(self, category=None, loglevel=None, message=None):
|
||||||
"""Mark a given logging message as expected."""
|
"""Mark a given logging message as expected."""
|
||||||
@ -284,16 +321,24 @@ class QuteProc(testprocess.Process):
|
|||||||
message=message)
|
message=message)
|
||||||
line.expected = True
|
line.expected = True
|
||||||
|
|
||||||
def wait_for_load_finished(self, path, timeout=15000):
|
def wait_for_load_finished(self, path, *, port=None, https=False,
|
||||||
|
timeout=None, load_status='success'):
|
||||||
"""Wait until any tab has finished loading."""
|
"""Wait until any tab has finished loading."""
|
||||||
url = self.path_to_url(path)
|
if timeout is None:
|
||||||
|
if 'CI' in os.environ:
|
||||||
|
timeout = 15000
|
||||||
|
else:
|
||||||
|
timeout = 5000
|
||||||
|
|
||||||
|
url = self.path_to_url(path, port=port, https=https)
|
||||||
# We really need the same representation that the webview uses in its
|
# We really need the same representation that the webview uses in its
|
||||||
# __repr__
|
# __repr__
|
||||||
url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100)
|
url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100)
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r"(load status for <qutebrowser.browser.webview.WebView "
|
r"(load status for <qutebrowser\.browser\.webview\.WebView "
|
||||||
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "
|
r"tab_id=\d+ url='{url}'>: LoadStatus\.{load_status}|fetch: "
|
||||||
r"PyQt5.QtCore.QUrl\('{url}'\) -> .*)".format(url=re.escape(url)))
|
r"PyQt5\.QtCore\.QUrl\('{url}'\) -> .*)".format(
|
||||||
|
load_status=re.escape(load_status), url=re.escape(url)))
|
||||||
self.wait_for(message=pattern, timeout=timeout)
|
self.wait_for(message=pattern, timeout=timeout)
|
||||||
|
|
||||||
def get_session(self):
|
def get_session(self):
|
||||||
|
@ -87,7 +87,10 @@ def test_mhtml(test_name, download_dir, quteproc, httpbin):
|
|||||||
'data', 'downloads', 'mhtml', test_name)
|
'data', 'downloads', 'mhtml', test_name)
|
||||||
test_path = 'data/downloads/mhtml/{}'.format(test_name)
|
test_path = 'data/downloads/mhtml/{}'.format(test_name)
|
||||||
|
|
||||||
quteproc.open_path('{}/{}.html'.format(test_path, test_name))
|
url_path = '{}/{}.html'.format(test_path, test_name)
|
||||||
|
quteproc.open_path(url_path)
|
||||||
|
quteproc.wait_for_load_finished(url_path)
|
||||||
|
|
||||||
download_dest = os.path.join(download_dir.location,
|
download_dest = os.path.join(download_dir.location,
|
||||||
'{}-downloaded.mht'.format(test_name))
|
'{}-downloaded.mht'.format(test_name))
|
||||||
|
|
||||||
|
@ -29,22 +29,54 @@ import testprocess
|
|||||||
from qutebrowser.utils import log
|
from qutebrowser.utils import log
|
||||||
|
|
||||||
|
|
||||||
def test_quteproc_error_message(qtbot, quteproc):
|
@pytest.mark.parametrize('cmd', [
|
||||||
|
':message-error test',
|
||||||
|
':jseval console.log("[FAIL] test");'
|
||||||
|
])
|
||||||
|
def test_quteproc_error_message(qtbot, quteproc, cmd):
|
||||||
"""Make sure the test fails with an unexpected error message."""
|
"""Make sure the test fails with an unexpected error message."""
|
||||||
with qtbot.waitSignal(quteproc.got_error, raising=True):
|
with qtbot.waitSignal(quteproc.got_error):
|
||||||
quteproc.send_cmd(':message-error test')
|
quteproc.send_cmd(cmd)
|
||||||
# Usually we wouldn't call this from inside a test, but here we force the
|
# Usually we wouldn't call this from inside a test, but here we force the
|
||||||
# error to occur during the test rather than at teardown time.
|
# error to occur during the test rather than at teardown time.
|
||||||
with pytest.raises(pytest.fail.Exception):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
quteproc.after_test()
|
quteproc.after_test()
|
||||||
|
|
||||||
|
|
||||||
|
def test_quteproc_skip_via_js(qtbot, quteproc):
|
||||||
|
with pytest.raises(pytest.skip.Exception) as excinfo:
|
||||||
|
quteproc.send_cmd(':jseval console.log("[SKIP] test");')
|
||||||
|
quteproc.wait_for_js('[SKIP] test')
|
||||||
|
|
||||||
|
# Usually we wouldn't call this from inside a test, but here we force
|
||||||
|
# the error to occur during the test rather than at teardown time.
|
||||||
|
quteproc.after_test()
|
||||||
|
|
||||||
|
assert str(excinfo.value) == 'test'
|
||||||
|
|
||||||
|
|
||||||
|
def test_quteproc_skip_and_wait_for(qtbot, quteproc):
|
||||||
|
"""This test will skip *again* during teardown, but we don't care."""
|
||||||
|
with pytest.raises(pytest.skip.Exception):
|
||||||
|
quteproc.send_cmd(':jseval console.log("[SKIP] foo");')
|
||||||
|
quteproc.wait_for_js("[SKIP] foo")
|
||||||
|
quteproc.wait_for(message='This will not match')
|
||||||
|
|
||||||
|
|
||||||
def test_qt_log_ignore(qtbot, quteproc):
|
def test_qt_log_ignore(qtbot, quteproc):
|
||||||
"""Make sure the test passes when logging a qt_log_ignore message."""
|
"""Make sure the test passes when logging a qt_log_ignore message."""
|
||||||
with qtbot.waitSignal(quteproc.got_error, raising=True):
|
with qtbot.waitSignal(quteproc.got_error):
|
||||||
quteproc.send_cmd(':message-error "SpellCheck: test"')
|
quteproc.send_cmd(':message-error "SpellCheck: test"')
|
||||||
|
|
||||||
|
|
||||||
|
def test_quteprocess_quitting(qtbot, quteproc_process):
|
||||||
|
"""When qutebrowser quits, after_test should fail."""
|
||||||
|
with qtbot.waitSignal(quteproc_process.proc.finished, timeout=5000):
|
||||||
|
quteproc_process.send_cmd(':quit')
|
||||||
|
with pytest.raises(testprocess.ProcessExited):
|
||||||
|
quteproc_process.after_test()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('data, attrs', [
|
@pytest.mark.parametrize('data, attrs', [
|
||||||
(
|
(
|
||||||
# Normal message
|
# Normal message
|
||||||
|
@ -74,6 +74,29 @@ class PythonProcess(testprocess.Process):
|
|||||||
return (sys.executable, ['-c', ';'.join(code)])
|
return (sys.executable, ['-c', ';'.join(code)])
|
||||||
|
|
||||||
|
|
||||||
|
class QuitPythonProcess(testprocess.Process):
|
||||||
|
|
||||||
|
"""A testprocess which quits immediately."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.proc.setReadChannel(QProcess.StandardOutput)
|
||||||
|
|
||||||
|
def _parse_line(self, line):
|
||||||
|
print("LINE: {}".format(line))
|
||||||
|
if line.strip() == 'ready':
|
||||||
|
self.ready.emit()
|
||||||
|
return testprocess.Line(line)
|
||||||
|
|
||||||
|
def _executable_args(self):
|
||||||
|
code = [
|
||||||
|
'import sys',
|
||||||
|
'print("ready")',
|
||||||
|
'sys.exit(0)',
|
||||||
|
]
|
||||||
|
return (sys.executable, ['-c', ';'.join(code)])
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
@pytest.yield_fixture
|
||||||
def pyproc():
|
def pyproc():
|
||||||
proc = PythonProcess()
|
proc = PythonProcess()
|
||||||
@ -81,6 +104,35 @@ def pyproc():
|
|||||||
proc.terminate()
|
proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def quit_pyproc():
|
||||||
|
proc = QuitPythonProcess()
|
||||||
|
yield proc
|
||||||
|
proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_quitting_process(qtbot, quit_pyproc):
|
||||||
|
with qtbot.waitSignal(quit_pyproc.proc.finished):
|
||||||
|
quit_pyproc.start()
|
||||||
|
with pytest.raises(testprocess.ProcessExited):
|
||||||
|
quit_pyproc.after_test()
|
||||||
|
|
||||||
|
|
||||||
|
def test_quitting_process_expected(qtbot, quit_pyproc):
|
||||||
|
quit_pyproc.exit_expected = True
|
||||||
|
with qtbot.waitSignal(quit_pyproc.proc.finished):
|
||||||
|
quit_pyproc.start()
|
||||||
|
quit_pyproc.after_test()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_signal_raising(qtbot):
|
||||||
|
"""testprocess._wait_signal should raise by default."""
|
||||||
|
proc = testprocess.Process()
|
||||||
|
with pytest.raises(qtbot.SignalTimeoutError):
|
||||||
|
with proc._wait_signal(proc.proc.started, timeout=0):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestWaitFor:
|
class TestWaitFor:
|
||||||
|
|
||||||
def test_successful(self, pyproc):
|
def test_successful(self, pyproc):
|
||||||
@ -144,6 +196,13 @@ class TestWaitFor:
|
|||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
pyproc.wait_for()
|
pyproc.wait_for()
|
||||||
|
|
||||||
|
def test_do_skip(self, pyproc):
|
||||||
|
"""Test wait_for when getting no text at all, with do_skip."""
|
||||||
|
pyproc.code = "pass"
|
||||||
|
pyproc.start()
|
||||||
|
with pytest.raises(pytest.skip.Exception):
|
||||||
|
pyproc.wait_for(data="foobar", timeout=100, do_skip=True)
|
||||||
|
|
||||||
|
|
||||||
class TestEnsureNotLogged:
|
class TestEnsureNotLogged:
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ import pytest
|
|||||||
('/data/hello.txt', 'Hello World!', True),
|
('/data/hello.txt', 'Hello World!', True),
|
||||||
])
|
])
|
||||||
def test_httpbin(httpbin, qtbot, path, content, expected):
|
def test_httpbin(httpbin, qtbot, path, content, expected):
|
||||||
with qtbot.waitSignal(httpbin.new_request, raising=True, timeout=100):
|
with qtbot.waitSignal(httpbin.new_request, timeout=100):
|
||||||
url = 'http://localhost:{}{}'.format(httpbin.port, path)
|
url = 'http://localhost:{}{}'.format(httpbin.port, path)
|
||||||
try:
|
try:
|
||||||
response = urllib.request.urlopen(url)
|
response = urllib.request.urlopen(url)
|
||||||
|
@ -72,11 +72,21 @@ class Line:
|
|||||||
return '{}({!r})'.format(self.__class__.__name__, self.data)
|
return '{}({!r})'.format(self.__class__.__name__, self.data)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_log(data, threshold=50):
|
||||||
|
"""Shorten the given log without -v and convert to a string."""
|
||||||
|
# pylint: disable=no-member
|
||||||
|
if len(data) > threshold and not pytest.config.getoption('--verbose'):
|
||||||
|
msg = '[{} lines suppressed, use -v to show]'.format(
|
||||||
|
len(data) - threshold)
|
||||||
|
data = [msg] + data[-threshold:]
|
||||||
|
return '\n'.join(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
"""Add qutebrowser/httpbin sections to captured output if a test failed."""
|
"""Add qutebrowser/httpbin sections to captured output if a test failed."""
|
||||||
outcome = yield
|
outcome = yield
|
||||||
if call.when != 'call':
|
if call.when not in ['call', 'teardown']:
|
||||||
return
|
return
|
||||||
report = outcome.get_result()
|
report = outcome.get_result()
|
||||||
|
|
||||||
@ -91,12 +101,16 @@ def pytest_runtest_makereport(item, call):
|
|||||||
# actually a tuple. This is handled similarily in pytest-qt too.
|
# actually a tuple. This is handled similarily in pytest-qt too.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
if pytest.config.getoption('--capture') == 'no':
|
||||||
|
# Already printed live
|
||||||
|
return
|
||||||
|
|
||||||
if quteproc_log is not None:
|
if quteproc_log is not None:
|
||||||
report.longrepr.addsection("qutebrowser output",
|
report.longrepr.addsection("qutebrowser output",
|
||||||
'\n'.join(quteproc_log))
|
_render_log(quteproc_log))
|
||||||
if httpbin_log is not None:
|
if httpbin_log is not None:
|
||||||
report.longrepr.addsection("httpbin output",
|
report.longrepr.addsection("httpbin output", _render_log(httpbin_log))
|
||||||
'\n'.join(httpbin_log))
|
|
||||||
|
|
||||||
|
|
||||||
class Process(QObject):
|
class Process(QObject):
|
||||||
@ -109,6 +123,7 @@ class Process(QObject):
|
|||||||
_invalid: A list of lines which could not be parsed.
|
_invalid: A list of lines which could not be parsed.
|
||||||
_data: A list of parsed lines.
|
_data: A list of parsed lines.
|
||||||
proc: The QProcess for the underlying process.
|
proc: The QProcess for the underlying process.
|
||||||
|
exit_expected: Whether the process is expected to quit.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
ready: Emitted when the server finished starting up.
|
ready: Emitted when the server finished starting up.
|
||||||
@ -126,9 +141,13 @@ class Process(QObject):
|
|||||||
self._data = []
|
self._data = []
|
||||||
self.proc = QProcess()
|
self.proc = QProcess()
|
||||||
self.proc.setReadChannel(QProcess.StandardError)
|
self.proc.setReadChannel(QProcess.StandardError)
|
||||||
|
self.exit_expected = False
|
||||||
|
|
||||||
def _log(self, line):
|
def _log(self, line):
|
||||||
"""Add the given line to the captured log output."""
|
"""Add the given line to the captured log output."""
|
||||||
|
# pylint: disable=no-member
|
||||||
|
if pytest.config.getoption('--capture') == 'no':
|
||||||
|
print(line)
|
||||||
self.captured_log.append(line)
|
self.captured_log.append(line)
|
||||||
|
|
||||||
def _parse_line(self, line):
|
def _parse_line(self, line):
|
||||||
@ -225,8 +244,9 @@ class Process(QObject):
|
|||||||
raise InvalidLine(self._invalid)
|
raise InvalidLine(self._invalid)
|
||||||
|
|
||||||
self.clear_data()
|
self.clear_data()
|
||||||
if not self.is_running():
|
if not self.is_running() and not self.exit_expected:
|
||||||
raise ProcessExited
|
raise ProcessExited
|
||||||
|
self.exit_expected = False
|
||||||
|
|
||||||
def clear_data(self):
|
def clear_data(self):
|
||||||
"""Clear the collected data."""
|
"""Clear the collected data."""
|
||||||
@ -287,7 +307,41 @@ class Process(QObject):
|
|||||||
return line
|
return line
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def wait_for(self, timeout=None, *, override_waited_for=False, **kwargs):
|
def _wait_for_match(self, spy, kwargs):
|
||||||
|
"""Try matching the kwargs with the given QSignalSpy."""
|
||||||
|
for args in spy:
|
||||||
|
assert len(args) == 1
|
||||||
|
line = args[0]
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
|
||||||
|
for key, expected in kwargs.items():
|
||||||
|
value = getattr(line, key)
|
||||||
|
matches.append(self._match_data(value, expected))
|
||||||
|
|
||||||
|
if all(matches):
|
||||||
|
# If we waited for this line, chances are we don't mean the
|
||||||
|
# same thing the next time we use wait_for and it matches
|
||||||
|
# this line again.
|
||||||
|
line.waited_for = True
|
||||||
|
return line
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _maybe_skip(self):
|
||||||
|
"""Can be overridden by subclasses to skip on certain log lines.
|
||||||
|
|
||||||
|
We can't run pytest.skip directly while parsing the log, as that would
|
||||||
|
lead to a pytest.skip.Exception error in a virtual Qt method, which
|
||||||
|
means pytest-qt fails the test.
|
||||||
|
|
||||||
|
Instead, we check for skip messages periodically in
|
||||||
|
QuteProc._maybe_skip, and call _maybe_skip after every parsed message
|
||||||
|
in wait_for (where it's most likely that new messages arrive).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def wait_for(self, timeout=None, *, override_waited_for=False,
|
||||||
|
do_skip=False, **kwargs):
|
||||||
"""Wait until a given value is found in the data.
|
"""Wait until a given value is found in the data.
|
||||||
|
|
||||||
Keyword arguments to this function get interpreted as attributes of the
|
Keyword arguments to this function get interpreted as attributes of the
|
||||||
@ -298,13 +352,17 @@ class Process(QObject):
|
|||||||
timeout: How long to wait for the message.
|
timeout: How long to wait for the message.
|
||||||
override_waited_for: If set, gets triggered by previous messages
|
override_waited_for: If set, gets triggered by previous messages
|
||||||
again.
|
again.
|
||||||
|
do_skip: If set, call pytest.skip on a timeout.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
The matched line.
|
The matched line.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
if 'CI' in os.environ:
|
if do_skip:
|
||||||
|
timeout = 2000
|
||||||
|
elif 'CI' in os.environ:
|
||||||
timeout = 15000
|
timeout = 15000
|
||||||
else:
|
else:
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
@ -324,27 +382,20 @@ class Process(QObject):
|
|||||||
elapsed_timer.start()
|
elapsed_timer.start()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# Skip if there are pending messages causing a skip
|
||||||
|
self._maybe_skip()
|
||||||
got_signal = spy.wait(timeout)
|
got_signal = spy.wait(timeout)
|
||||||
if not got_signal or elapsed_timer.hasExpired(timeout):
|
if not got_signal or elapsed_timer.hasExpired(timeout):
|
||||||
raise WaitForTimeout("Timed out after {}ms waiting for "
|
msg = "Timed out after {}ms waiting for {!r}.".format(
|
||||||
"{!r}.".format(timeout, kwargs))
|
timeout, kwargs)
|
||||||
|
if do_skip:
|
||||||
|
pytest.skip(msg)
|
||||||
|
else:
|
||||||
|
raise WaitForTimeout(msg)
|
||||||
|
|
||||||
for args in spy:
|
match = self._wait_for_match(spy, kwargs)
|
||||||
assert len(args) == 1
|
if match is not None:
|
||||||
line = args[0]
|
return match
|
||||||
|
|
||||||
matches = []
|
|
||||||
|
|
||||||
for key, expected in kwargs.items():
|
|
||||||
value = getattr(line, key)
|
|
||||||
matches.append(self._match_data(value, expected))
|
|
||||||
|
|
||||||
if all(matches):
|
|
||||||
# If we waited for this line, chances are we don't mean the
|
|
||||||
# same thing the next time we use wait_for and it matches
|
|
||||||
# this line again.
|
|
||||||
line.waited_for = True
|
|
||||||
return line
|
|
||||||
|
|
||||||
def ensure_not_logged(self, delay=500, **kwargs):
|
def ensure_not_logged(self, delay=500, **kwargs):
|
||||||
"""Make sure the data matching the given arguments is not logged.
|
"""Make sure the data matching the given arguments is not logged.
|
||||||
|
@ -19,13 +19,15 @@
|
|||||||
|
|
||||||
"""Fixtures for the httpbin webserver."""
|
"""Fixtures for the httpbin webserver."""
|
||||||
|
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
import os.path
|
import os.path
|
||||||
|
import http.client
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PyQt5.QtCore import pyqtSignal
|
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||||
|
|
||||||
import testprocess
|
import testprocess
|
||||||
|
|
||||||
@ -54,13 +56,23 @@ class Request(testprocess.Line):
|
|||||||
self.path = '/' if path == '/' else path.rstrip('/')
|
self.path = '/' if path == '/' else path.rstrip('/')
|
||||||
|
|
||||||
self.status = parsed['status']
|
self.status = parsed['status']
|
||||||
|
self._check_status()
|
||||||
|
|
||||||
missing_paths = ['/favicon.ico', '/does-not-exist']
|
def _check_status(self):
|
||||||
|
"""Check if the http status is what we expected."""
|
||||||
|
# WORKAROUND for https://github.com/PyCQA/pylint/issues/399 (?)
|
||||||
|
# pylint: disable=no-member, useless-suppression
|
||||||
|
path_to_statuses = {
|
||||||
|
'/favicon.ico': [http.client.NOT_FOUND],
|
||||||
|
'/does-not-exist': [http.client.NOT_FOUND],
|
||||||
|
'/custom/redirect-later': [http.client.FOUND],
|
||||||
|
'/basic-auth/user/password':
|
||||||
|
[http.client.UNAUTHORIZED, http.client.OK],
|
||||||
|
}
|
||||||
|
|
||||||
if self.path in missing_paths:
|
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
|
||||||
assert self.status == 404
|
expected_statuses = path_to_statuses.get(sanitized, [http.client.OK])
|
||||||
else:
|
assert self.status in expected_statuses
|
||||||
assert self.status < 400
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
@ -94,7 +106,7 @@ class ExpectedRequest:
|
|||||||
.format(self.verb, self.path))
|
.format(self.verb, self.path))
|
||||||
|
|
||||||
|
|
||||||
class HTTPBin(testprocess.Process):
|
class WebserverProcess(testprocess.Process):
|
||||||
|
|
||||||
"""Abstraction over a running HTTPbin server process.
|
"""Abstraction over a running HTTPbin server process.
|
||||||
|
|
||||||
@ -110,8 +122,9 @@ class HTTPBin(testprocess.Process):
|
|||||||
|
|
||||||
KEYS = ['verb', 'path']
|
KEYS = ['verb', 'path']
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, script, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._script = script
|
||||||
self.port = self._get_port()
|
self.port = self._get_port()
|
||||||
self.new_data.connect(self.new_request)
|
self.new_data.connect(self.new_request)
|
||||||
|
|
||||||
@ -130,8 +143,9 @@ class HTTPBin(testprocess.Process):
|
|||||||
|
|
||||||
def _parse_line(self, line):
|
def _parse_line(self, line):
|
||||||
self._log(line)
|
self._log(line)
|
||||||
if line == (' * Running on http://127.0.0.1:{}/ (Press CTRL+C to '
|
started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/ '
|
||||||
'quit)'.format(self.port)):
|
r'\(Press CTRL\+C to quit\)'.format(self.port))
|
||||||
|
if started_re.fullmatch(line):
|
||||||
self.ready.emit()
|
self.ready.emit()
|
||||||
return None
|
return None
|
||||||
return Request(line)
|
return Request(line)
|
||||||
@ -139,12 +153,12 @@ class HTTPBin(testprocess.Process):
|
|||||||
def _executable_args(self):
|
def _executable_args(self):
|
||||||
if hasattr(sys, 'frozen'):
|
if hasattr(sys, 'frozen'):
|
||||||
executable = os.path.join(os.path.dirname(sys.executable),
|
executable = os.path.join(os.path.dirname(sys.executable),
|
||||||
'webserver_sub')
|
self._script)
|
||||||
args = [str(self.port)]
|
args = [str(self.port)]
|
||||||
else:
|
else:
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
py_file = os.path.join(os.path.dirname(__file__),
|
py_file = os.path.join(os.path.dirname(__file__),
|
||||||
'webserver_sub.py')
|
self._script + '.py')
|
||||||
args = [py_file, str(self.port)]
|
args = [py_file, str(self.port)]
|
||||||
return executable, args
|
return executable, args
|
||||||
|
|
||||||
@ -157,7 +171,7 @@ class HTTPBin(testprocess.Process):
|
|||||||
@pytest.yield_fixture(scope='session', autouse=True)
|
@pytest.yield_fixture(scope='session', autouse=True)
|
||||||
def httpbin(qapp):
|
def httpbin(qapp):
|
||||||
"""Fixture for a httpbin object which ensures clean setup/teardown."""
|
"""Fixture for a httpbin object which ensures clean setup/teardown."""
|
||||||
httpbin = HTTPBin()
|
httpbin = WebserverProcess('webserver_sub')
|
||||||
httpbin.start()
|
httpbin.start()
|
||||||
yield httpbin
|
yield httpbin
|
||||||
httpbin.cleanup()
|
httpbin.cleanup()
|
||||||
@ -169,3 +183,18 @@ def httpbin_after_test(httpbin, request):
|
|||||||
request.node._httpbin_log = httpbin.captured_log
|
request.node._httpbin_log = httpbin.captured_log
|
||||||
yield
|
yield
|
||||||
httpbin.after_test()
|
httpbin.after_test()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def ssl_server(request, qapp):
|
||||||
|
"""Fixture for a webserver with a self-signed SSL certificate.
|
||||||
|
|
||||||
|
This needs to be explicitly used in a test, and overwrites the httpbin log
|
||||||
|
used in that test.
|
||||||
|
"""
|
||||||
|
server = WebserverProcess('webserver_sub_ssl')
|
||||||
|
request.node._httpbin_log = server.captured_log
|
||||||
|
server.start()
|
||||||
|
yield server
|
||||||
|
server.after_test()
|
||||||
|
server.cleanup()
|
||||||
|
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):
|
def test_data(self, qtbot, req, data):
|
||||||
reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo')
|
reply = networkreply.FixedDataNetworkReply(req, data, 'test/foo')
|
||||||
with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead,
|
with qtbot.waitSignals([reply.metaDataChanged, reply.readyRead,
|
||||||
reply.finished], raising=True):
|
reply.finished]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert reply.bytesAvailable() == len(data)
|
assert reply.bytesAvailable() == len(data)
|
||||||
@ -78,7 +78,7 @@ def test_error_network_reply(qtbot, req):
|
|||||||
reply = networkreply.ErrorNetworkReply(
|
reply = networkreply.ErrorNetworkReply(
|
||||||
req, "This is an error", QNetworkReply.UnknownNetworkError)
|
req, "This is an error", QNetworkReply.UnknownNetworkError)
|
||||||
|
|
||||||
with qtbot.waitSignals([reply.error, reply.finished], raising=True):
|
with qtbot.waitSignals([reply.error, reply.finished]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
reply.abort() # shouldn't do anything
|
reply.abort() # shouldn't do anything
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QNetworkCookie
|
from PyQt5.QtNetwork import QNetworkCookie
|
||||||
from PyQt5.QtTest import QSignalSpy
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch):
|
|||||||
ram_jar = cookies.RAMCookieJar()
|
ram_jar = cookies.RAMCookieJar()
|
||||||
cookie = QNetworkCookie(b'foo', b'bar')
|
cookie = QNetworkCookie(b'foo', b'bar')
|
||||||
url = QUrl('http://example.com/')
|
url = QUrl('http://example.com/')
|
||||||
with qtbot.waitSignal(ram_jar.changed, raising=True):
|
with qtbot.waitSignal(ram_jar.changed):
|
||||||
assert ram_jar.setCookiesFromUrl([cookie], url)
|
assert ram_jar.setCookiesFromUrl([cookie], url)
|
||||||
|
|
||||||
# assert the cookies are added correctly
|
# assert the cookies are added correctly
|
||||||
@ -90,15 +89,15 @@ def test_set_cookies_accept(config_stub, qtbot, monkeypatch):
|
|||||||
assert saved_cookie.name(), saved_cookie.value() == expected
|
assert saved_cookie.name(), saved_cookie.value() == expected
|
||||||
|
|
||||||
|
|
||||||
def test_set_cookies_never_accept(config_stub):
|
def test_set_cookies_never_accept(qtbot, config_stub):
|
||||||
"""Test setCookiesFromUrl when cookies are not accepted."""
|
"""Test setCookiesFromUrl when cookies are not accepted."""
|
||||||
config_stub.data = CONFIG_NEVER_COOKIES
|
config_stub.data = CONFIG_NEVER_COOKIES
|
||||||
ram_jar = cookies.RAMCookieJar()
|
ram_jar = cookies.RAMCookieJar()
|
||||||
changed_signal_spy = QSignalSpy(ram_jar.changed)
|
|
||||||
|
|
||||||
url = QUrl('http://example.com/')
|
url = QUrl('http://example.com/')
|
||||||
assert not ram_jar.setCookiesFromUrl(url, 'test')
|
|
||||||
assert not changed_signal_spy
|
with qtbot.assertNotEmitted(ram_jar.changed):
|
||||||
|
assert not ram_jar.setCookiesFromUrl(url, 'test')
|
||||||
assert not ram_jar.cookiesForUrl(url)
|
assert not ram_jar.cookiesForUrl(url)
|
||||||
|
|
||||||
|
|
||||||
@ -151,21 +150,10 @@ def test_cookies_changed_emit(config_stub, fake_save_manager,
|
|||||||
'LineParser', LineparserSaveStub)
|
'LineParser', LineparserSaveStub)
|
||||||
jar = cookies.CookieJar()
|
jar = cookies.CookieJar()
|
||||||
|
|
||||||
with qtbot.waitSignal(jar.changed, raising=True):
|
with qtbot.waitSignal(jar.changed):
|
||||||
config_stub.set('content', 'cookies-store', False)
|
config_stub.set('content', 'cookies-store', False)
|
||||||
|
|
||||||
|
|
||||||
def test_cookies_changed_not_emitted(config_stub, fake_save_manager,
|
|
||||||
monkeypatch, qapp):
|
|
||||||
"""Test that changed is not emitted when nothing changes."""
|
|
||||||
config_stub.data = CONFIG_COOKIES_ENABLED
|
|
||||||
monkeypatch.setattr(lineparser,
|
|
||||||
'LineParser', LineparserSaveStub)
|
|
||||||
jar = cookies.CookieJar()
|
|
||||||
changed_spy = QSignalSpy(jar.changed)
|
|
||||||
assert not changed_spy
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('store_cookies,empty', [
|
@pytest.mark.parametrize('store_cookies,empty', [
|
||||||
(True, False),
|
(True, False),
|
||||||
(False, True)
|
(False, True)
|
||||||
|
@ -628,7 +628,7 @@ class TestJavascriptEscape:
|
|||||||
with open(path, encoding='utf-8') as f:
|
with open(path, encoding='utf-8') as f:
|
||||||
html_source = f.read().replace('%INPUT%', escaped)
|
html_source = f.read().replace('%INPUT%', escaped)
|
||||||
|
|
||||||
with qtbot.waitSignal(webframe.loadFinished, raising=True) as blocker:
|
with qtbot.waitSignal(webframe.loadFinished) as blocker:
|
||||||
webframe.setHtml(html_source)
|
webframe.setHtml(html_source)
|
||||||
assert blocker.args == [True]
|
assert blocker.args == [True]
|
||||||
|
|
||||||
|
@ -79,8 +79,7 @@ def test_command(qtbot, py_proc, runner):
|
|||||||
with open(os.environ['QUTE_FIFO'], 'w') as f:
|
with open(os.environ['QUTE_FIFO'], 'w') as f:
|
||||||
f.write('foo\n')
|
f.write('foo\n')
|
||||||
""")
|
""")
|
||||||
with qtbot.waitSignal(runner.got_cmd, raising=True,
|
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||||
timeout=10000) as blocker:
|
|
||||||
runner.run(cmd, *args)
|
runner.run(cmd, *args)
|
||||||
assert blocker.args == ['foo']
|
assert blocker.args == ['foo']
|
||||||
|
|
||||||
@ -100,8 +99,7 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
|
|||||||
f.write('\n')
|
f.write('\n')
|
||||||
""")
|
""")
|
||||||
|
|
||||||
with qtbot.waitSignal(runner.got_cmd, raising=True,
|
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||||
timeout=10000) as blocker:
|
|
||||||
runner.run(cmd, *args, env=env)
|
runner.run(cmd, *args, env=env)
|
||||||
|
|
||||||
data = blocker.args[0]
|
data = blocker.args[0]
|
||||||
@ -136,9 +134,8 @@ def test_temporary_files(qtbot, tmpdir, py_proc, runner):
|
|||||||
f.write('\n')
|
f.write('\n')
|
||||||
""")
|
""")
|
||||||
|
|
||||||
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000):
|
with qtbot.waitSignal(runner.finished, timeout=10000):
|
||||||
with qtbot.waitSignal(runner.got_cmd, raising=True,
|
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||||
timeout=10000) as blocker:
|
|
||||||
runner.run(cmd, *args, env=env)
|
runner.run(cmd, *args, env=env)
|
||||||
|
|
||||||
data = blocker.args[0]
|
data = blocker.args[0]
|
||||||
@ -160,7 +157,7 @@ def test_command_with_error(qtbot, tmpdir, py_proc, runner):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000):
|
with qtbot.waitSignal(runner.finished, timeout=10000):
|
||||||
runner.run(cmd, *args, env=env)
|
runner.run(cmd, *args, env=env)
|
||||||
|
|
||||||
assert not text_file.exists()
|
assert not text_file.exists()
|
||||||
@ -191,14 +188,13 @@ def test_killed_command(qtbot, tmpdir, py_proc, runner):
|
|||||||
""")
|
""")
|
||||||
args.append(str(pidfile))
|
args.append(str(pidfile))
|
||||||
|
|
||||||
with qtbot.waitSignal(watcher.directoryChanged, raising=True,
|
with qtbot.waitSignal(watcher.directoryChanged, timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
runner.run(cmd, *args, env=env)
|
runner.run(cmd, *args, env=env)
|
||||||
|
|
||||||
# Make sure the PID was written to the file, not just the file created
|
# Make sure the PID was written to the file, not just the file created
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
with qtbot.waitSignal(runner.finished, raising=True):
|
with qtbot.waitSignal(runner.finished):
|
||||||
os.kill(int(pidfile.read()), signal.SIGTERM)
|
os.kill(int(pidfile.read()), signal.SIGTERM)
|
||||||
|
|
||||||
assert not text_file.exists()
|
assert not text_file.exists()
|
||||||
@ -216,7 +212,7 @@ def test_temporary_files_failed_cleanup(caplog, qtbot, tmpdir, py_proc,
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with qtbot.waitSignal(runner.finished, raising=True, timeout=10000):
|
with qtbot.waitSignal(runner.finished, timeout=10000):
|
||||||
runner.run(cmd, *args, env={'QUTE_HTML': str(test_file)})
|
runner.run(cmd, *args, env={'QUTE_HTML': str(test_file)})
|
||||||
|
|
||||||
assert len(caplog.records) == 1
|
assert len(caplog.records) == 1
|
||||||
|
@ -305,6 +305,8 @@ class TestString:
|
|||||||
({'minlen': 2}, 'fo'),
|
({'minlen': 2}, 'fo'),
|
||||||
({'minlen': 2, 'maxlen': 3}, 'fo'),
|
({'minlen': 2, 'maxlen': 3}, 'fo'),
|
||||||
({'minlen': 2, 'maxlen': 3}, 'foo'),
|
({'minlen': 2, 'maxlen': 3}, 'foo'),
|
||||||
|
# valid_values
|
||||||
|
({'valid_values': configtypes.ValidValues('fooo')}, 'fooo'),
|
||||||
])
|
])
|
||||||
def test_validate_valid(self, klass, kwargs, val):
|
def test_validate_valid(self, klass, kwargs, val):
|
||||||
klass(**kwargs).validate(val)
|
klass(**kwargs).validate(val)
|
||||||
@ -319,6 +321,8 @@ class TestString:
|
|||||||
({'maxlen': 2}, 'fob'),
|
({'maxlen': 2}, 'fob'),
|
||||||
({'minlen': 2, 'maxlen': 3}, 'f'),
|
({'minlen': 2, 'maxlen': 3}, 'f'),
|
||||||
({'minlen': 2, 'maxlen': 3}, 'fooo'),
|
({'minlen': 2, 'maxlen': 3}, 'fooo'),
|
||||||
|
# valid_values
|
||||||
|
({'valid_values': configtypes.ValidValues('blah')}, 'fooo'),
|
||||||
])
|
])
|
||||||
def test_validate_invalid(self, klass, kwargs, val):
|
def test_validate_invalid(self, klass, kwargs, val):
|
||||||
with pytest.raises(configexc.ValidationError):
|
with pytest.raises(configexc.ValidationError):
|
||||||
@ -335,6 +339,15 @@ class TestString:
|
|||||||
def test_complete(self, klass, value):
|
def test_complete(self, klass, value):
|
||||||
assert klass(completions=value).complete() == value
|
assert klass(completions=value).complete() == value
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('valid_values, expected', [
|
||||||
|
(configtypes.ValidValues('one', 'two'),
|
||||||
|
[('one', ''), ('two', '')]),
|
||||||
|
(configtypes.ValidValues(('1', 'one'), ('2', 'two')),
|
||||||
|
[('1', 'one'), ('2', 'two')]),
|
||||||
|
])
|
||||||
|
def test_complete_valid_values(self, klass, valid_values, expected):
|
||||||
|
assert klass(valid_values=valid_values).complete() == expected
|
||||||
|
|
||||||
|
|
||||||
class TestList:
|
class TestList:
|
||||||
|
|
||||||
|
@ -94,8 +94,7 @@ class JSTester:
|
|||||||
**kwargs: Passed to jinja's template.render().
|
**kwargs: Passed to jinja's template.render().
|
||||||
"""
|
"""
|
||||||
template = self._jinja_env.get_template(path)
|
template = self._jinja_env.get_template(path)
|
||||||
with self._qtbot.waitSignal(self.webview.loadFinished,
|
with self._qtbot.waitSignal(self.webview.loadFinished) as blocker:
|
||||||
raising=True) as blocker:
|
|
||||||
self.webview.setHtml(template.render(**kwargs))
|
self.webview.setHtml(template.render(**kwargs))
|
||||||
assert blocker.args == [True]
|
assert blocker.args == [True]
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ class TestKeyChain:
|
|||||||
assert not keyparser.execute.called
|
assert not keyparser.execute.called
|
||||||
assert keyparser._ambiguous_timer.isActive()
|
assert keyparser._ambiguous_timer.isActive()
|
||||||
# We wait for the timeout to occur.
|
# We wait for the timeout to occur.
|
||||||
with qtbot.waitSignal(keyparser.keystring_updated, raising=True):
|
with qtbot.waitSignal(keyparser.keystring_updated):
|
||||||
pass
|
pass
|
||||||
assert keyparser.execute.called
|
assert keyparser.execute.called
|
||||||
|
|
||||||
|
@ -23,29 +23,11 @@
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
|
|
||||||
from PyQt5.QtCore import QSize, Qt
|
|
||||||
|
|
||||||
from qutebrowser.browser import webview
|
from qutebrowser.browser import webview
|
||||||
from qutebrowser.mainwindow.statusbar.progress import Progress
|
from qutebrowser.mainwindow.statusbar.progress import Progress
|
||||||
|
|
||||||
|
|
||||||
class FakeStatusBar(QWidget):
|
|
||||||
|
|
||||||
"""Fake statusbar to test progressbar sizing."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.hbox = QHBoxLayout(self)
|
|
||||||
self.hbox.addStretch()
|
|
||||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
||||||
self.setStyleSheet('background-color: red;')
|
|
||||||
|
|
||||||
def minimumSizeHint(self):
|
|
||||||
return QSize(1, self.fontMetrics().height())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def progress_widget(qtbot, monkeypatch, config_stub):
|
def progress_widget(qtbot, monkeypatch, config_stub):
|
||||||
"""Create a Progress widget and checks its initial state."""
|
"""Create a Progress widget and checks its initial state."""
|
||||||
@ -62,25 +44,6 @@ def progress_widget(qtbot, monkeypatch, config_stub):
|
|||||||
return widget
|
return widget
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def fake_statusbar(qtbot):
|
|
||||||
"""Fixture providing a statusbar in a container window."""
|
|
||||||
container = QWidget()
|
|
||||||
qtbot.add_widget(container)
|
|
||||||
vbox = QVBoxLayout(container)
|
|
||||||
vbox.addStretch()
|
|
||||||
|
|
||||||
statusbar = FakeStatusBar(container)
|
|
||||||
# to make sure container isn't GCed
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
|
||||||
statusbar.container = container
|
|
||||||
vbox.addWidget(statusbar)
|
|
||||||
|
|
||||||
container.show()
|
|
||||||
qtbot.waitForWindowShown(container)
|
|
||||||
return statusbar
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_started(progress_widget):
|
def test_load_started(progress_widget):
|
||||||
"""Ensure the Progress widget reacts properly when the page starts loading.
|
"""Ensure the Progress widget reacts properly when the page starts loading.
|
||||||
|
|
||||||
|
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"""
|
"""Tests for qutebrowser.misc.autoupdate"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PyQt5.QtTest import QSignalSpy
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
|
|
||||||
from qutebrowser.misc import autoupdate, httpclient
|
from qutebrowser.misc import autoupdate, httpclient
|
||||||
@ -63,13 +62,9 @@ def test_get_version_success(qtbot):
|
|||||||
http_stub = HTTPGetStub(success=True)
|
http_stub = HTTPGetStub(success=True)
|
||||||
client = autoupdate.PyPIVersionClient(client=http_stub)
|
client = autoupdate.PyPIVersionClient(client=http_stub)
|
||||||
|
|
||||||
# Use a spy to inspect the signal
|
with qtbot.assertNotEmitted(client.error):
|
||||||
error_spy = QSignalSpy(client.error)
|
with qtbot.waitSignal(client.success):
|
||||||
|
client.get_version('test')
|
||||||
with qtbot.waitSignal(client.success, raising=True):
|
|
||||||
client.get_version('test')
|
|
||||||
|
|
||||||
assert len(error_spy) == 0
|
|
||||||
|
|
||||||
assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json')
|
assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json')
|
||||||
|
|
||||||
@ -79,13 +74,9 @@ def test_get_version_error(qtbot):
|
|||||||
http_stub = HTTPGetStub(success=False)
|
http_stub = HTTPGetStub(success=False)
|
||||||
client = autoupdate.PyPIVersionClient(client=http_stub)
|
client = autoupdate.PyPIVersionClient(client=http_stub)
|
||||||
|
|
||||||
# Use a spy to inspect the signal
|
with qtbot.assertNotEmitted(client.success):
|
||||||
success_spy = QSignalSpy(client.success)
|
with qtbot.waitSignal(client.error):
|
||||||
|
client.get_version('test')
|
||||||
with qtbot.waitSignal(client.error, raising=True):
|
|
||||||
client.get_version('test')
|
|
||||||
|
|
||||||
assert len(success_spy) == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('json', INVALID_JSON)
|
@pytest.mark.parametrize('json', INVALID_JSON)
|
||||||
@ -95,10 +86,6 @@ def test_invalid_json(qtbot, json):
|
|||||||
client = autoupdate.PyPIVersionClient(client=http_stub)
|
client = autoupdate.PyPIVersionClient(client=http_stub)
|
||||||
client.get_version('test')
|
client.get_version('test')
|
||||||
|
|
||||||
# Use a spy to inspect the signal
|
with qtbot.assertNotEmitted(client.success):
|
||||||
success_spy = QSignalSpy(client.success)
|
with qtbot.waitSignal(client.error):
|
||||||
|
client.get_version('test')
|
||||||
with qtbot.waitSignal(client.error, raising=True):
|
|
||||||
client.get_version('test')
|
|
||||||
|
|
||||||
assert len(success_spy) == 0
|
|
||||||
|
@ -40,7 +40,8 @@ def proc(qtbot):
|
|||||||
p = guiprocess.GUIProcess(0, 'testprocess')
|
p = guiprocess.GUIProcess(0, 'testprocess')
|
||||||
yield p
|
yield p
|
||||||
if p._proc.state() == QProcess.Running:
|
if p._proc.state() == QProcess.Running:
|
||||||
with qtbot.waitSignal(p.finished, timeout=10000) as blocker:
|
with qtbot.waitSignal(p.finished, timeout=10000,
|
||||||
|
raising=False) as blocker:
|
||||||
p._proc.terminate()
|
p._proc.terminate()
|
||||||
if not blocker.signal_triggered:
|
if not blocker.signal_triggered:
|
||||||
p._proc.kill()
|
p._proc.kill()
|
||||||
@ -56,8 +57,7 @@ def fake_proc(monkeypatch, stubs):
|
|||||||
|
|
||||||
def test_start(proc, qtbot, guiprocess_message_mock, py_proc):
|
def test_start(proc, qtbot, guiprocess_message_mock, py_proc):
|
||||||
"""Test simply starting a process."""
|
"""Test simply starting a process."""
|
||||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
||||||
proc.start(*argv)
|
proc.start(*argv)
|
||||||
|
|
||||||
@ -69,8 +69,7 @@ def test_start_verbose(proc, qtbot, guiprocess_message_mock, py_proc):
|
|||||||
"""Test starting a process verbosely."""
|
"""Test starting a process verbosely."""
|
||||||
proc.verbose = True
|
proc.verbose = True
|
||||||
|
|
||||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
||||||
proc.start(*argv)
|
proc.start(*argv)
|
||||||
|
|
||||||
@ -97,8 +96,7 @@ def test_start_env(monkeypatch, qtbot, py_proc):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
proc.start(*argv)
|
proc.start(*argv)
|
||||||
|
|
||||||
data = bytes(proc._proc.readAll()).decode('utf-8')
|
data = bytes(proc._proc.readAll()).decode('utf-8')
|
||||||
@ -110,8 +108,7 @@ def test_start_env(monkeypatch, qtbot, py_proc):
|
|||||||
@pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device', extend=True)
|
@pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device', extend=True)
|
||||||
def test_start_mode(proc, qtbot, py_proc):
|
def test_start_mode(proc, qtbot, py_proc):
|
||||||
"""Test simply starting a process with mode parameter."""
|
"""Test simply starting a process with mode parameter."""
|
||||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
argv = py_proc("import sys; print('test'); sys.exit(0)")
|
||||||
proc.start(*argv, mode=QIODevice.NotOpen)
|
proc.start(*argv, mode=QIODevice.NotOpen)
|
||||||
|
|
||||||
@ -139,7 +136,7 @@ def test_start_detached_error(fake_proc, guiprocess_message_mock):
|
|||||||
|
|
||||||
def test_double_start(qtbot, proc, py_proc):
|
def test_double_start(qtbot, proc, py_proc):
|
||||||
"""Test starting a GUIProcess twice."""
|
"""Test starting a GUIProcess twice."""
|
||||||
with qtbot.waitSignal(proc.started, raising=True, timeout=10000):
|
with qtbot.waitSignal(proc.started, timeout=10000):
|
||||||
argv = py_proc("import time; time.sleep(10)")
|
argv = py_proc("import time; time.sleep(10)")
|
||||||
proc.start(*argv)
|
proc.start(*argv)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -148,12 +145,10 @@ def test_double_start(qtbot, proc, py_proc):
|
|||||||
|
|
||||||
def test_double_start_finished(qtbot, proc, py_proc):
|
def test_double_start_finished(qtbot, proc, py_proc):
|
||||||
"""Test starting a GUIProcess twice (with the first call finished)."""
|
"""Test starting a GUIProcess twice (with the first call finished)."""
|
||||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
argv = py_proc("import sys; sys.exit(0)")
|
argv = py_proc("import sys; sys.exit(0)")
|
||||||
proc.start(*argv)
|
proc.start(*argv)
|
||||||
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
|
with qtbot.waitSignals([proc.started, proc.finished], timeout=10000):
|
||||||
timeout=10000):
|
|
||||||
argv = py_proc("import sys; sys.exit(0)")
|
argv = py_proc("import sys; sys.exit(0)")
|
||||||
proc.start(*argv)
|
proc.start(*argv)
|
||||||
|
|
||||||
@ -180,7 +175,7 @@ def test_start_logging(fake_proc, caplog):
|
|||||||
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
||||||
"""Test the process emitting an error."""
|
"""Test the process emitting an error."""
|
||||||
with caplog.at_level(logging.ERROR, 'message'):
|
with caplog.at_level(logging.ERROR, 'message'):
|
||||||
with qtbot.waitSignal(proc.error, raising=True, timeout=5000):
|
with qtbot.waitSignal(proc.error, timeout=5000):
|
||||||
proc.start('this_does_not_exist_either', [])
|
proc.start('this_does_not_exist_either', [])
|
||||||
|
|
||||||
msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error,
|
msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error,
|
||||||
@ -191,7 +186,7 @@ def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
|||||||
|
|
||||||
|
|
||||||
def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc):
|
def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc):
|
||||||
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000):
|
with qtbot.waitSignal(proc.finished, timeout=10000):
|
||||||
proc.start(*py_proc('import sys; sys.exit(1)'))
|
proc.start(*py_proc('import sys; sys.exit(1)'))
|
||||||
|
|
||||||
msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error)
|
msg = guiprocess_message_mock.getmsg(guiprocess_message_mock.Level.error)
|
||||||
@ -202,7 +197,7 @@ def test_exit_unsuccessful(qtbot, proc, guiprocess_message_mock, py_proc):
|
|||||||
def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream):
|
def test_exit_unsuccessful_output(qtbot, proc, caplog, py_proc, stream):
|
||||||
"""When a process fails, its output should be logged."""
|
"""When a process fails, its output should be logged."""
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000):
|
with qtbot.waitSignal(proc.finished, timeout=10000):
|
||||||
proc.start(*py_proc("""
|
proc.start(*py_proc("""
|
||||||
import sys
|
import sys
|
||||||
print("test", file=sys.{})
|
print("test", file=sys.{})
|
||||||
@ -219,7 +214,7 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream):
|
|||||||
The test doesn't actually check the log as it'd fail because of the error
|
The test doesn't actually check the log as it'd fail because of the error
|
||||||
logging.
|
logging.
|
||||||
"""
|
"""
|
||||||
with qtbot.waitSignal(proc.finished, raising=True, timeout=10000):
|
with qtbot.waitSignal(proc.finished, timeout=10000):
|
||||||
proc.start(*py_proc("""
|
proc.start(*py_proc("""
|
||||||
import sys
|
import sys
|
||||||
print("test", file=sys.{})
|
print("test", file=sys.{})
|
||||||
|
@ -343,13 +343,11 @@ class TestListen:
|
|||||||
ipc_server.listen()
|
ipc_server.listen()
|
||||||
old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
|
old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
|
||||||
|
|
||||||
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000,
|
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000):
|
||||||
raising=True):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Make sure the timer is not singleShot
|
# Make sure the timer is not singleShot
|
||||||
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000,
|
with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000):
|
||||||
raising=True):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
new_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
|
new_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns
|
||||||
@ -412,9 +410,9 @@ class TestHandleConnection:
|
|||||||
def test_double_connection(self, qlocalsocket, ipc_server, caplog):
|
def test_double_connection(self, qlocalsocket, ipc_server, caplog):
|
||||||
ipc_server._socket = qlocalsocket
|
ipc_server._socket = qlocalsocket
|
||||||
ipc_server.handle_connection()
|
ipc_server.handle_connection()
|
||||||
message = ("Got new connection but ignoring it because we're still "
|
msg = ("Got new connection but ignoring it because we're still "
|
||||||
"handling another one.")
|
"handling another one")
|
||||||
assert message in [rec.message for rec in caplog.records]
|
assert any(rec.message.startswith(msg) for rec in caplog.records)
|
||||||
|
|
||||||
def test_disconnected_immediately(self, ipc_server, caplog):
|
def test_disconnected_immediately(self, ipc_server, caplog):
|
||||||
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
|
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
|
||||||
@ -444,7 +442,7 @@ class TestHandleConnection:
|
|||||||
|
|
||||||
ipc_server._server = FakeServer(socket)
|
ipc_server._server = FakeServer(socket)
|
||||||
|
|
||||||
with qtbot.waitSignal(ipc_server.got_args, raising=True) as blocker:
|
with qtbot.waitSignal(ipc_server.got_args) as blocker:
|
||||||
ipc_server.handle_connection()
|
ipc_server.handle_connection()
|
||||||
|
|
||||||
assert blocker.args == [['foo'], 'tab', '']
|
assert blocker.args == [['foo'], 'tab', '']
|
||||||
@ -458,7 +456,7 @@ def connected_socket(qtbot, qlocalsocket, ipc_server):
|
|||||||
pytest.skip("Skipping connected_socket test - "
|
pytest.skip("Skipping connected_socket test - "
|
||||||
"https://github.com/The-Compiler/qutebrowser/issues/1045")
|
"https://github.com/The-Compiler/qutebrowser/issues/1045")
|
||||||
ipc_server.listen()
|
ipc_server.listen()
|
||||||
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
with qtbot.waitSignal(ipc_server._server.newConnection):
|
||||||
qlocalsocket.connectToServer('qute-test')
|
qlocalsocket.connectToServer('qute-test')
|
||||||
yield qlocalsocket
|
yield qlocalsocket
|
||||||
qlocalsocket.disconnectFromServer()
|
qlocalsocket.disconnectFromServer()
|
||||||
@ -496,22 +494,19 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8')
|
|||||||
(b'{"args": [], "target_arg": null}\n', 'invalid version'),
|
(b'{"args": [], "target_arg": null}\n', 'invalid version'),
|
||||||
])
|
])
|
||||||
def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
|
def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
|
||||||
got_args_spy = QSignalSpy(ipc_server.got_args)
|
|
||||||
|
|
||||||
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
|
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with qtbot.waitSignals(signals, raising=True):
|
with qtbot.assertNotEmitted(ipc_server.got_args):
|
||||||
connected_socket.write(data)
|
with qtbot.waitSignals(signals):
|
||||||
|
connected_socket.write(data)
|
||||||
|
|
||||||
messages = [r.message for r in caplog.records]
|
messages = [r.message for r in caplog.records]
|
||||||
assert messages[-1] == 'Ignoring invalid IPC data.'
|
assert messages[-1].startswith('Ignoring invalid IPC data from socket ')
|
||||||
assert messages[-2].startswith(msg)
|
assert messages[-2].startswith(msg)
|
||||||
assert not got_args_spy
|
|
||||||
|
|
||||||
|
|
||||||
def test_multiline(qtbot, ipc_server, connected_socket):
|
def test_multiline(qtbot, ipc_server, connected_socket):
|
||||||
spy = QSignalSpy(ipc_server.got_args)
|
spy = QSignalSpy(ipc_server.got_args)
|
||||||
error_spy = QSignalSpy(ipc_server.got_invalid_data)
|
|
||||||
|
|
||||||
data = ('{{"args": ["one"], "target_arg": "tab",'
|
data = ('{{"args": ["one"], "target_arg": "tab",'
|
||||||
' "protocol_version": {version}}}\n'
|
' "protocol_version": {version}}}\n'
|
||||||
@ -519,11 +514,10 @@ def test_multiline(qtbot, ipc_server, connected_socket):
|
|||||||
' "protocol_version": {version}}}\n'.format(
|
' "protocol_version": {version}}}\n'.format(
|
||||||
version=ipc.PROTOCOL_VERSION))
|
version=ipc.PROTOCOL_VERSION))
|
||||||
|
|
||||||
with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args],
|
with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
|
||||||
raising=True):
|
with qtbot.waitSignals([ipc_server.got_args, ipc_server.got_args]):
|
||||||
connected_socket.write(data.encode('utf-8'))
|
connected_socket.write(data.encode('utf-8'))
|
||||||
|
|
||||||
assert not error_spy
|
|
||||||
assert len(spy) == 2
|
assert len(spy) == 2
|
||||||
assert spy[0] == [['one'], 'tab', '']
|
assert spy[0] == [['one'], 'tab', '']
|
||||||
assert spy[1] == [['two'], '', '']
|
assert spy[1] == [['two'], '', '']
|
||||||
@ -542,19 +536,19 @@ class TestSendToRunningInstance:
|
|||||||
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
|
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
|
||||||
ipc_server.listen()
|
ipc_server.listen()
|
||||||
raw_spy = QSignalSpy(ipc_server.got_raw)
|
raw_spy = QSignalSpy(ipc_server.got_raw)
|
||||||
error_spy = QSignalSpy(ipc_server.got_invalid_data)
|
|
||||||
|
|
||||||
with qtbot.waitSignal(ipc_server.got_args, raising=True,
|
with qtbot.assertNotEmitted(ipc_server.got_invalid_data):
|
||||||
timeout=5000) as blocker:
|
with qtbot.waitSignal(ipc_server.got_args,
|
||||||
with tmpdir.as_cwd():
|
timeout=5000) as blocker:
|
||||||
if not has_cwd:
|
with tmpdir.as_cwd():
|
||||||
m = mocker.patch('qutebrowser.misc.ipc.os')
|
if not has_cwd:
|
||||||
m.getcwd.side_effect = OSError
|
m = mocker.patch('qutebrowser.misc.ipc.os')
|
||||||
sent = ipc.send_to_running_instance('qute-test', ['foo'], None)
|
m.getcwd.side_effect = OSError
|
||||||
|
sent = ipc.send_to_running_instance('qute-test', ['foo'],
|
||||||
|
None)
|
||||||
|
|
||||||
assert sent
|
assert sent
|
||||||
|
|
||||||
assert not error_spy
|
|
||||||
expected_cwd = str(tmpdir) if has_cwd else ''
|
expected_cwd = str(tmpdir) if has_cwd else ''
|
||||||
|
|
||||||
assert blocker.args == [['foo'], '', expected_cwd]
|
assert blocker.args == [['foo'], '', expected_cwd]
|
||||||
@ -598,15 +592,14 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
|
|||||||
ipc_server._timer.setInterval(100)
|
ipc_server._timer.setInterval(100)
|
||||||
ipc_server.listen()
|
ipc_server.listen()
|
||||||
|
|
||||||
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
with qtbot.waitSignal(ipc_server._server.newConnection):
|
||||||
qlocalsocket.connectToServer('qute-test')
|
qlocalsocket.connectToServer('qute-test')
|
||||||
|
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
|
with qtbot.waitSignal(qlocalsocket.disconnected, timeout=5000):
|
||||||
timeout=5000):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert caplog.records[-1].message == "IPC connection timed out."
|
assert caplog.records[-1].message.startswith("IPC connection timed out")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('method, args, is_warning', [
|
@pytest.mark.parametrize('method, args, is_warning', [
|
||||||
@ -679,14 +672,14 @@ class TestSendOrListen:
|
|||||||
objreg_server = objreg.get('ipc-server')
|
objreg_server = objreg.get('ipc-server')
|
||||||
assert objreg_server is ret_server
|
assert objreg_server is ret_server
|
||||||
|
|
||||||
with qtbot.waitSignal(ret_server.got_args, raising=True):
|
with qtbot.waitSignal(ret_server.got_args):
|
||||||
ret_client = ipc.send_or_listen(args)
|
ret_client = ipc.send_or_listen(args)
|
||||||
|
|
||||||
assert ret_client is None
|
assert ret_client is None
|
||||||
|
|
||||||
@pytest.mark.posix(reason="Unneeded on Windows")
|
@pytest.mark.posix(reason="Unneeded on Windows")
|
||||||
def test_legacy_name(self, caplog, qtbot, args, legacy_server):
|
def test_legacy_name(self, caplog, qtbot, args, legacy_server):
|
||||||
with qtbot.waitSignal(legacy_server.got_args, raising=True):
|
with qtbot.waitSignal(legacy_server.got_args):
|
||||||
ret = ipc.send_or_listen(args)
|
ret = ipc.send_or_listen(args)
|
||||||
assert ret is None
|
assert ret is None
|
||||||
msgs = [e.message for e in caplog.records]
|
msgs = [e.message for e in caplog.records]
|
||||||
@ -727,7 +720,7 @@ class TestSendOrListen:
|
|||||||
assert isinstance(ret_server, ipc.IPCServer)
|
assert isinstance(ret_server, ipc.IPCServer)
|
||||||
|
|
||||||
logging.debug('== Connecting ==')
|
logging.debug('== Connecting ==')
|
||||||
with qtbot.waitSignal(ret_server.got_args, raising=True):
|
with qtbot.waitSignal(ret_server.got_args):
|
||||||
ret_client = ipc.send_or_listen(args)
|
ret_client = ipc.send_or_listen(args)
|
||||||
|
|
||||||
assert ret_client is None
|
assert ret_client is None
|
||||||
|
@ -74,7 +74,7 @@ def test_finished_signal(qtbot):
|
|||||||
|
|
||||||
qtbot.add_widget(box)
|
qtbot.add_widget(box)
|
||||||
|
|
||||||
with qtbot.waitSignal(box.finished, raising=True):
|
with qtbot.waitSignal(box.finished):
|
||||||
box.accept()
|
box.accept()
|
||||||
|
|
||||||
assert signal_triggered
|
assert signal_triggered
|
||||||
|
@ -439,9 +439,8 @@ class TestSave:
|
|||||||
|
|
||||||
def test_update_completion_signal(self, sess_man, tmpdir, qtbot):
|
def test_update_completion_signal(self, sess_man, tmpdir, qtbot):
|
||||||
session_path = tmpdir / 'foo.yml'
|
session_path = tmpdir / 'foo.yml'
|
||||||
blocker = qtbot.waitSignal(sess_man.update_completion)
|
with qtbot.waitSignal(sess_man.update_completion):
|
||||||
sess_man.save(str(session_path))
|
sess_man.save(str(session_path))
|
||||||
assert blocker.signal_triggered
|
|
||||||
|
|
||||||
def test_no_state_config(self, sess_man, tmpdir, state_config):
|
def test_no_state_config(self, sess_man, tmpdir, state_config):
|
||||||
session_path = tmpdir / 'foo.yml'
|
session_path = tmpdir / 'foo.yml'
|
||||||
@ -691,9 +690,8 @@ class TestDelete:
|
|||||||
sess = tmpdir / 'foo.yml'
|
sess = tmpdir / 'foo.yml'
|
||||||
sess.ensure()
|
sess.ensure()
|
||||||
|
|
||||||
blocker = qtbot.waitSignal(sess_man.update_completion)
|
with qtbot.waitSignal(sess_man.update_completion):
|
||||||
sess_man.delete(str(sess))
|
sess_man.delete(str(sess))
|
||||||
assert blocker.signal_triggered
|
|
||||||
|
|
||||||
def test_not_existing(self, sess_man, qtbot, tmpdir):
|
def test_not_existing(self, sess_man, qtbot, tmpdir):
|
||||||
sess = tmpdir / 'foo.yml'
|
sess = tmpdir / 'foo.yml'
|
||||||
|
@ -166,13 +166,17 @@ class TestFuzzyUrl:
|
|||||||
assert not os_mock.path.exists.called
|
assert not os_mock.path.exists.called
|
||||||
assert url == QUrl('http://foo')
|
assert url == QUrl('http://foo')
|
||||||
|
|
||||||
def test_file_absolute(self, os_mock):
|
@pytest.mark.parametrize('path, expected', [
|
||||||
|
('/foo', QUrl('file:///foo')),
|
||||||
|
('/bar\n', QUrl('file:///bar')),
|
||||||
|
])
|
||||||
|
def test_file_absolute(self, path, expected, os_mock):
|
||||||
"""Test with an absolute path."""
|
"""Test with an absolute path."""
|
||||||
os_mock.path.exists.return_value = True
|
os_mock.path.exists.return_value = True
|
||||||
os_mock.path.isabs.return_value = True
|
os_mock.path.isabs.return_value = True
|
||||||
|
|
||||||
url = urlutils.fuzzy_url('/foo')
|
url = urlutils.fuzzy_url(path)
|
||||||
assert url == QUrl('file:///foo')
|
assert url == expected
|
||||||
|
|
||||||
@pytest.mark.posix
|
@pytest.mark.posix
|
||||||
def test_file_absolute_expanded(self, os_mock):
|
def test_file_absolute_expanded(self, os_mock):
|
||||||
|
@ -61,23 +61,21 @@ def test_done(mode, answer, signal_names, question, qtbot):
|
|||||||
question.mode = mode
|
question.mode = mode
|
||||||
question.answer = answer
|
question.answer = answer
|
||||||
signals = [getattr(question, name) for name in signal_names]
|
signals = [getattr(question, name) for name in signal_names]
|
||||||
with qtbot.waitSignals(signals, raising=True):
|
with qtbot.waitSignals(signals):
|
||||||
question.done()
|
question.done()
|
||||||
assert not question.is_aborted
|
assert not question.is_aborted
|
||||||
|
|
||||||
|
|
||||||
def test_cancel(question, qtbot):
|
def test_cancel(question, qtbot):
|
||||||
"""Test Question.cancel()."""
|
"""Test Question.cancel()."""
|
||||||
with qtbot.waitSignals([question.cancelled, question.completed],
|
with qtbot.waitSignals([question.cancelled, question.completed]):
|
||||||
raising=True):
|
|
||||||
question.cancel()
|
question.cancel()
|
||||||
assert not question.is_aborted
|
assert not question.is_aborted
|
||||||
|
|
||||||
|
|
||||||
def test_abort(question, qtbot):
|
def test_abort(question, qtbot):
|
||||||
"""Test Question.abort()."""
|
"""Test Question.abort()."""
|
||||||
with qtbot.waitSignals([question.aborted, question.completed],
|
with qtbot.waitSignals([question.aborted, question.completed]):
|
||||||
raising=True):
|
|
||||||
question.abort()
|
question.abort()
|
||||||
assert question.is_aborted
|
assert question.is_aborted
|
||||||
|
|
||||||
|
@ -72,13 +72,13 @@ def test_start_overflow():
|
|||||||
def test_timeout_start(qtbot):
|
def test_timeout_start(qtbot):
|
||||||
"""Make sure the timer works with start()."""
|
"""Make sure the timer works with start()."""
|
||||||
t = usertypes.Timer()
|
t = usertypes.Timer()
|
||||||
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
|
with qtbot.waitSignal(t.timeout, timeout=3000):
|
||||||
t.start(200)
|
t.start(200)
|
||||||
|
|
||||||
|
|
||||||
def test_timeout_set_interval(qtbot):
|
def test_timeout_set_interval(qtbot):
|
||||||
"""Make sure the timer works with setInterval()."""
|
"""Make sure the timer works with setInterval()."""
|
||||||
t = usertypes.Timer()
|
t = usertypes.Timer()
|
||||||
with qtbot.waitSignal(t.timeout, timeout=3000, raising=True):
|
with qtbot.waitSignal(t.timeout, timeout=3000):
|
||||||
t.setInterval(200)
|
t.setInterval(200)
|
||||||
t.start()
|
t.start()
|
||||||
|
20
tox.ini
20
tox.ini
@ -20,7 +20,7 @@ deps =
|
|||||||
Flask==0.10.1
|
Flask==0.10.1
|
||||||
glob2==0.4.1
|
glob2==0.4.1
|
||||||
httpbin==0.4.0
|
httpbin==0.4.0
|
||||||
hypothesis==1.18.1
|
hypothesis==2.0.0
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
Mako==1.0.3
|
Mako==1.0.3
|
||||||
parse==1.6.6
|
parse==1.6.6
|
||||||
@ -33,9 +33,10 @@ deps =
|
|||||||
pytest-faulthandler==1.3.0
|
pytest-faulthandler==1.3.0
|
||||||
pytest-html==1.7
|
pytest-html==1.7
|
||||||
pytest-mock==0.9.0
|
pytest-mock==0.9.0
|
||||||
pytest-qt==1.10.0
|
pytest-qt==1.11.0
|
||||||
pytest-sugar==0.5.1
|
pytest-instafail==0.3.0
|
||||||
pytest-travis-fold==1.2.0
|
pytest-travis-fold==1.2.0
|
||||||
|
pytest-repeat==0.2
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
termcolor==1.1.0
|
termcolor==1.1.0
|
||||||
vulture==0.8.1
|
vulture==0.8.1
|
||||||
@ -45,7 +46,7 @@ deps =
|
|||||||
cherrypy==4.0.0
|
cherrypy==4.0.0
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
{envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
{envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --instafail --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||||
{envpython} scripts/dev/check_coverage.py {posargs}
|
{envpython} scripts/dev/check_coverage.py {posargs}
|
||||||
|
|
||||||
[testenv:mkvenv]
|
[testenv:mkvenv]
|
||||||
@ -73,7 +74,7 @@ passenv = {[testenv]passenv}
|
|||||||
deps = {[testenv]deps}
|
deps = {[testenv]deps}
|
||||||
setenv =
|
setenv =
|
||||||
DISPLAY=
|
DISPLAY=
|
||||||
QUTE_NO_DISPLAY_OK=1
|
QUTE_NO_DISPLAY=1
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
{envpython} -m py.test --strict -rfEw {posargs:tests}
|
{envpython} -m py.test --strict -rfEw {posargs:tests}
|
||||||
@ -105,8 +106,8 @@ passenv =
|
|||||||
deps =
|
deps =
|
||||||
{[testenv]deps}
|
{[testenv]deps}
|
||||||
{[testenv:misc]deps}
|
{[testenv:misc]deps}
|
||||||
astroid==1.4.3
|
astroid==1.4.4
|
||||||
pylint==1.5.2
|
pylint==1.5.4
|
||||||
requests==2.9.1
|
requests==2.9.1
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
@ -156,7 +157,6 @@ deps =
|
|||||||
py==1.4.31
|
py==1.4.31
|
||||||
pyflakes==1.0.0
|
pyflakes==1.0.0
|
||||||
pytest==2.8.5
|
pytest==2.8.5
|
||||||
pytest-cache==1.0
|
|
||||||
pytest-flakes==1.0.1
|
pytest-flakes==1.0.1
|
||||||
commands =
|
commands =
|
||||||
{envpython} -m py.test -q --flakes --ignore=tests --noconftest
|
{envpython} -m py.test -q --flakes --ignore=tests --noconftest
|
||||||
@ -168,10 +168,9 @@ deps =
|
|||||||
-r{toxinidir}/requirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
apipkg==1.4
|
apipkg==1.4
|
||||||
execnet==1.4.1
|
execnet==1.4.1
|
||||||
pep8==1.6.2
|
pep8==1.7
|
||||||
py==1.4.31
|
py==1.4.31
|
||||||
pytest==2.8.5
|
pytest==2.8.5
|
||||||
pytest-cache==1.0
|
|
||||||
pytest-pep8==1.0.6
|
pytest-pep8==1.0.6
|
||||||
commands =
|
commands =
|
||||||
{envpython} -m py.test -q --pep8 --ignore=tests --noconftest
|
{envpython} -m py.test -q --pep8 --ignore=tests --noconftest
|
||||||
@ -187,7 +186,6 @@ deps =
|
|||||||
mccabe==0.3.1
|
mccabe==0.3.1
|
||||||
py==1.4.31
|
py==1.4.31
|
||||||
pytest==2.8.5
|
pytest==2.8.5
|
||||||
pytest-cache==1.0
|
|
||||||
pytest-mccabe==0.1
|
pytest-mccabe==0.1
|
||||||
commands =
|
commands =
|
||||||
{envpython} -m py.test -q --mccabe --ignore=tests --noconftest
|
{envpython} -m py.test -q --mccabe --ignore=tests --noconftest
|
||||||
|
Loading…
Reference in New Issue
Block a user