summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÁrni Dagur <arni@dagur.eu>2020-12-19 20:29:04 +0000
committerÁrni Dagur <arni@dagur.eu>2020-12-19 20:29:04 +0000
commitfd155628e1027fb2836b9df75be0bcbe0c92db8c (patch)
tree831421ab1653ac9c5c0a714cf61986956c21731a
parent17d8d39a4ba24383925e27cfe680b9bf7314d9db (diff)
parent12a4d34758a342449c6cd6734bfe2013012f1e65 (diff)
downloadqutebrowser-fd155628e1027fb2836b9df75be0bcbe0c92db8c.tar.gz
qutebrowser-fd155628e1027fb2836b9df75be0bcbe0c92db8c.zip
Merge branch 'master' into more-sophisticated-adblock
-rw-r--r--.coveragerc5
-rw-r--r--.editorconfig2
-rw-r--r--.github/workflows/ci.yml18
-rw-r--r--.github/workflows/recompile-requirements.yml4
-rw-r--r--.pylintrc2
-rw-r--r--doc/changelog.asciidoc15
-rw-r--r--doc/help/commands.asciidoc8
-rw-r--r--doc/help/configuring.asciidoc3
-rw-r--r--doc/help/settings.asciidoc24
-rw-r--r--doc/quickstart.asciidoc27
-rw-r--r--misc/requirements/requirements-dev.txt6
-rw-r--r--misc/requirements/requirements-flake8.txt4
-rw-r--r--misc/requirements/requirements-mypy.txt2
-rw-r--r--misc/requirements/requirements-pyinstaller.txt4
-rw-r--r--misc/requirements/requirements-pyinstaller.txt-raw5
-rw-r--r--misc/requirements/requirements-pylint.txt4
-rw-r--r--misc/requirements/requirements-sphinx.txt2
-rw-r--r--misc/requirements/requirements-tests.txt21
-rw-r--r--misc/requirements/requirements-tests.txt-raw6
-rw-r--r--misc/requirements/requirements-tox.txt4
-rw-r--r--misc/requirements/requirements-vulture.txt3
-rwxr-xr-xmisc/userscripts/qute-pass2
-rwxr-xr-xmisc/userscripts/readability-js23
-rw-r--r--pytest.ini1
-rw-r--r--qutebrowser/app.py3
-rw-r--r--qutebrowser/browser/browsertab.py6
-rw-r--r--qutebrowser/browser/commands.py63
-rw-r--r--qutebrowser/browser/downloads.py42
-rw-r--r--qutebrowser/browser/eventfilter.py59
-rw-r--r--qutebrowser/browser/greasemonkey.py1
-rw-r--r--qutebrowser/browser/history.py2
-rw-r--r--qutebrowser/browser/inspector.py27
-rw-r--r--qutebrowser/browser/navigate.py10
-rw-r--r--qutebrowser/browser/network/proxy.py6
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py21
-rw-r--r--qutebrowser/browser/webengine/webenginedownloads.py32
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py40
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py14
-rw-r--r--qutebrowser/browser/webkit/webkittab.py18
-rw-r--r--qutebrowser/completion/completiondelegate.py24
-rw-r--r--qutebrowser/completion/completionwidget.py64
-rw-r--r--qutebrowser/config/configdata.yml2
-rw-r--r--qutebrowser/config/qtargs.py3
-rw-r--r--qutebrowser/config/websettings.py23
-rw-r--r--qutebrowser/javascript/whatsapp_web_quirk.user.js4
-rw-r--r--qutebrowser/mainwindow/mainwindow.py45
-rw-r--r--qutebrowser/mainwindow/messageview.py11
-rw-r--r--qutebrowser/mainwindow/windowundo.py7
-rw-r--r--qutebrowser/misc/editor.py4
-rw-r--r--qutebrowser/utils/qtutils.py1
-rw-r--r--qutebrowser/utils/urlutils.py32
-rw-r--r--qutebrowser/utils/version.py52
-rw-r--r--requirements.txt2
-rw-r--r--scripts/dev/build_pyqt_wheel.py4
-rw-r--r--scripts/dev/check_coverage.py164
-rw-r--r--scripts/dev/misc_checks.py2
-rw-r--r--scripts/dev/recompile_requirements.py3
-rwxr-xr-xscripts/dev/src2asciidoc.py2
-rwxr-xr-xsetup.py1
-rw-r--r--tests/end2end/conftest.py10
-rw-r--r--tests/end2end/features/misc.feature4
-rw-r--r--tests/end2end/features/navigate.feature59
-rw-r--r--tests/end2end/features/private.feature19
-rw-r--r--tests/end2end/features/qutescheme.feature1
-rw-r--r--tests/end2end/features/scroll.feature2
-rw-r--r--tests/end2end/fixtures/quteprocess.py6
-rw-r--r--tests/end2end/test_insert_mode.py1
-rw-r--r--tests/helpers/fixtures.py2
-rw-r--r--tests/unit/browser/test_downloads.py (renamed from tests/unit/browser/webkit/test_downloads.py)64
-rw-r--r--tests/unit/browser/test_navigate.py47
-rw-r--r--tests/unit/browser/webkit/network/test_pac.py2
-rw-r--r--tests/unit/browser/webkit/test_webkitelem.py8
-rw-r--r--tests/unit/completion/test_completiondelegate.py18
-rw-r--r--tests/unit/completion/test_completionwidget.py128
-rw-r--r--tests/unit/config/test_configtypes.py30
-rw-r--r--tests/unit/mainwindow/test_messageview.py20
-rw-r--r--tests/unit/misc/test_miscwidgets.py16
-rw-r--r--tests/unit/misc/userscripts/test_qute_lastpass.py3
-rw-r--r--tests/unit/scripts/test_check_coverage.py4
-rw-r--r--tests/unit/utils/test_javascript.py79
-rw-r--r--tests/unit/utils/test_urlutils.py42
-rw-r--r--tests/unit/utils/test_version.py5
-rw-r--r--tox.ini1
83 files changed, 1060 insertions, 500 deletions
diff --git a/.coveragerc b/.coveragerc
index 2ef20dd12..9d43917a3 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,5 +1,8 @@
[run]
-source = qutebrowser
+include =
+ qutebrowser/*
+ tests/*
+ scripts/*
branch = true
omit =
qutebrowser/__main__.py
diff --git a/.editorconfig b/.editorconfig
index 645ced56e..6aab87c94 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,7 +4,7 @@ insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
-max_line_length = 79
+max_line_length = 88
indent_style = space
indent_size = 4
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2bee4391b..36423aab8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,7 +39,7 @@ jobs:
.tox
~/.cache/pip
key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- - uses: actions/setup-python@v2.1.1
+ - uses: actions/setup-python@v2.1.2
with:
python-version: '3.8'
- uses: actions/setup-node@v2.1.1
@@ -129,10 +129,10 @@ jobs:
- testenv: py38-pyqt514
os: ubuntu-20.04
python: 3.8
- ### PyQt 5.15 (Python nightly)
- - testenv: py3-pyqt515
+ ### PyQt 5.15 (Python 3.9)
+ - testenv: py39-pyqt515
os: ubuntu-20.04
- python: 3.10-dev
+ python: 3.9-dev
### PyQt 5.15 (Python 3.8, with coverage)
- testenv: py38-pyqt515-cov
os: ubuntu-20.04
@@ -157,13 +157,7 @@ jobs:
~/.cache/pip
key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}"
- name: Set up Python
- uses: actions/setup-python@v2.1.1
- if: "!endsWith(matrix.python, '-dev')"
- with:
- python-version: "${{ matrix.python }}"
- - name: Set up development Python
- uses: deadsnakes/action@v1.0.0
- if: "endsWith(matrix.python, '-dev')"
+ uses: actions/setup-python@v2.1.2
with:
python-version: "${{ matrix.python }}"
- name: Set up problem matchers
@@ -184,7 +178,7 @@ jobs:
if: "failure()"
- name: Upload coverage
if: "endsWith(matrix.testenv, '-cov')"
- uses: codecov/codecov-action@v1.0.12
+ uses: codecov/codecov-action@v1.0.13
with:
name: "${{ matrix.testenv }}"
diff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml
index 73254d854..045f2ee1e 100644
--- a/.github/workflows/recompile-requirements.yml
+++ b/.github/workflows/recompile-requirements.yml
@@ -20,11 +20,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- uses: actions/setup-python@v2.1.1
+ uses: actions/setup-python@v2.1.2
with:
python-version: '3.7'
- name: Set up Python 3.8
- uses: actions/setup-python@v2.1.1
+ uses: actions/setup-python@v2.1.2
with:
python-version: '3.8'
- name: Recompile requirements
diff --git a/.pylintrc b/.pylintrc
index 1fedefb6d..2d7cbc430 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -59,7 +59,7 @@ docstring-min-length=3
no-docstring-rgx=(^_|^main$)
[FORMAT]
-max-line-length=79
+max-line-length=88
ignore-long-lines=(<?https?://|file://|^# Copyright 201\d|link:)
expected-line-ending-format=LF
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 5977dcbc3..17635fd50 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -45,12 +45,20 @@ Changed
- `:back` and `:forward` now take an optional index which is completed using
the current tab's history.
- The time a website in a tab was visited is now saved/restored in sessions.
+- New argument `strip` for `:navigate` which removes queries and
+ fragments from the current URL.
+- When attempting to download a file to a location for which there's already a
+ still-running download, a confirmation prompt is now displayed.
+- `:completion-item-focus` now understands `next-page` and `prev-page` with
+ corresponding `<PgDown>` / `<PgUp>` default bindings.
+- When the last private window is closed, all private browsing data is now cleared.
Added
~~~~~
- `:undo` now has a new `-w` / `--window` argument, which can be used to
restore closed windows (rather than tabs). This is bound to `U` by default.
+- `:jseval` can now take `javascript:...` URLs via a new `--url` flag.
- New replacement `{aligned_index}` for `tabs.title.format` and `format_pinned`
which behaves like `{index}`, but space-pads the index based on the total
numbers of tabs. This can be used to get aligned tab texts with vertical
@@ -63,6 +71,7 @@ Added
- The `:download-open` command now has a new `--dir` flag, which can be used to
open the directory containing the downloaded file. An entry to do the same
was also added to the context menu.
+- Messages are now wrapped when they are too long to be displayed on a single line.
Fixed
~~~~~
@@ -98,6 +107,12 @@ Fixed
instead of displaying the proper text. This is now fixed.
- When entering different modes too quickly (e.g. pressing `fV`), the statusbar
could end up in a confusing state. This is now fixed.
+- When qutebrowser quits, running downloads are now cancelled properly.
+- The site-specific quirk for `web.whatsapp.com` has been updated to work after recent
+ WhatsApp-changes.
+- Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as
+ emoji) are involved.
+- When a windowed inspector is clicked, insert mode now isn't entered anymore.
v1.13.1 (2020-07-17)
--------------------
diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc
index e9ccf03d7..299b34b5e 100644
--- a/doc/help/commands.asciidoc
+++ b/doc/help/commands.asciidoc
@@ -749,7 +749,7 @@ Insert text at cursor position.
[[jseval]]
=== jseval
-Syntax: +:jseval [*--file*] [*--quiet*] [*--world* 'world'] 'js-code'+
+Syntax: +:jseval [*--file*] [*--url*] [*--quiet*] [*--world* 'world'] 'js-code'+
Evaluate a JavaScript string.
@@ -761,6 +761,7 @@ Evaluate a JavaScript string.
in qutebrowser's data dir, e.g.
`~/.local/share/qutebrowser/js`.
+* +*-u*+, +*--url*+: Interpret js-code as a `javascript:...` URL.
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in.
@@ -864,6 +865,7 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
Uses the
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option.
+ - `strip`: Strip query and fragment from the current URL.
@@ -1662,7 +1664,9 @@ Syntax: +:completion-item-focus [*--history*] 'which'+
Shift the focus of the completion menu to another item.
==== positional arguments
-* +'which'+: 'next', 'prev', 'next-category', or 'prev-category'.
+* +'which'+: 'next', 'prev', 'next-category', 'prev-category',
+ 'next-page', or 'prev-page'.
+
==== optional arguments
* +*-H*+, +*--history*+: Navigate through command history if no text was typed.
diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc
index 575104fc1..90b7ed65b 100644
--- a/doc/help/configuring.asciidoc
+++ b/doc/help/configuring.asciidoc
@@ -394,9 +394,10 @@ Pre-built colorschemes
^^^^^^^^^^^^^^^^^^^^^^
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
+- https://gitlab.com/jjzmajic/qutewal[Pywal integration]
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/dracula/qutebrowser-dracula-theme[Dracula]
-- https://github.com/jjzmajic/qutewal[Pywal theme]
+- https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 79de4fc12..d4de80d06 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -374,6 +374,7 @@ Backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine.
QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit. It's slightly more resource hungry than QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
+
This setting requires a restart.
Type: <<types,String>>
@@ -509,6 +510,8 @@ Default:
* +pass:[&lt;Ctrl-Y&gt;]+: +pass:[rl-yank]+
* +pass:[&lt;Down&gt;]+: +pass:[completion-item-focus --history next]+
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
+* +pass:[&lt;PgDown&gt;]+: +pass:[completion-item-focus next-page]+
+* +pass:[&lt;PgUp&gt;]+: +pass:[completion-item-focus prev-page]+
* +pass:[&lt;Return&gt;]+: +pass:[command-accept]+
* +pass:[&lt;Shift-Delete&gt;]+: +pass:[completion-item-del]+
* +pass:[&lt;Shift-Tab&gt;]+: +pass:[completion-item-focus prev]+
@@ -1569,6 +1572,7 @@ Default: +pass:[white]+
[[colors.webpage.darkmode.algorithm]]
=== colors.webpage.darkmode.algorithm
Which algorithm to use for modifying how colors are rendered with darkmode.
+
This setting requires a restart.
Type: <<types,String>>
@@ -1589,6 +1593,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.contrast
Contrast for dark mode.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
+
This setting requires a restart.
Type: <<types,Float>>
@@ -1616,6 +1621,7 @@ Example configurations from Chromium's `chrome://flags`:
- "With selective inversion of everything": Combines the two variants
above.
+
This setting requires a restart.
Type: <<types,Bool>>
@@ -1630,6 +1636,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.grayscale.all
Render all colors as grayscale.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
+
This setting requires a restart.
Type: <<types,Bool>>
@@ -1644,6 +1651,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.grayscale.images
Desaturation factor for images in dark mode.
If set to 0, images are left as-is. If set to 1, images are completely grayscale. Values between 0 and 1 desaturate the colors accordingly.
+
This setting requires a restart.
Type: <<types,Float>>
@@ -1658,6 +1666,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt].
+
This setting requires a restart.
Type: <<types,String>>
@@ -1677,6 +1686,7 @@ On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
Which pages to apply dark mode to.
+
This setting requires a restart.
Type: <<types,String>>
@@ -1697,6 +1707,7 @@ On QtWebKit, this setting is unavailable.
Threshold for inverting background elements with dark mode.
Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it.
Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
+
This setting requires a restart.
Type: <<types,Int>>
@@ -1711,6 +1722,7 @@ On QtWebKit, this setting is unavailable.
=== colors.webpage.darkmode.threshold.text
Threshold for inverting text with dark mode.
Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color.
+
This setting requires a restart.
Type: <<types,Int>>
@@ -1854,6 +1866,7 @@ Default: +pass:[false]+
A list of patterns which should not be shown in the history.
This only affects the completion. Matching URLs are still saved in the history (and visible on the qute://history page), but hidden in the completion.
Changing this setting will cause the completion history to be regenerated on the next start, which will take a short while.
+
This setting requires a restart.
Type: <<types,List of UrlPattern>>
@@ -2013,6 +2026,7 @@ Default: empty
=== content.canvas_reading
Allow websites to read canvas elements.
Note this is needed for some websites to work properly.
+
This setting requires a restart.
Type: <<types,Bool>>
@@ -2173,6 +2187,7 @@ Default: +pass:[true]+
When to send the Referer header.
The Referer header tells websites from which website you were coming from when visiting them.
No restart is needed with QtWebKit.
+
This setting requires a restart.
Type: <<types,String>>
@@ -2569,6 +2584,7 @@ On QtWebKit, this setting is unavailable.
[[content.site_specific_quirks]]
=== content.site_specific_quirks
Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
+
This setting requires a restart.
Type: <<types,Bool>>
@@ -2633,6 +2649,7 @@ Default: +pass:[true]+
=== content.webrtc_ip_handling_policy
Which interfaces to expose via WebRTC.
On Qt 5.10, this option doesn't work because of a Qt bug.
+
This setting requires a restart.
Type: <<types,String>>
@@ -3460,6 +3477,7 @@ Default: +pass:[8]+
=== qt.args
Additional arguments to pass to Qt, without leading `--`.
With QtWebEngine, some Chromium arguments (see https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work.
+
This setting requires a restart.
Type: <<types,List of String>>
@@ -3470,6 +3488,7 @@ Default: empty
=== qt.force_platform
Force a Qt platform to use.
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force using the XCB plugin when running QtWebEngine on Wayland.
+
This setting requires a restart.
Type: <<types,String>>
@@ -3480,6 +3499,7 @@ Default: empty
=== qt.force_platformtheme
Force a Qt platformtheme to use.
This sets the `QT_QPA_PLATFORMTHEME` environment variable which controls dialogs like the filepicker. By default, Qt determines the platform theme based on the desktop environment.
+
This setting requires a restart.
Type: <<types,String>>
@@ -3490,6 +3510,7 @@ Default: empty
=== qt.force_software_rendering
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers and can be useful in other scenarios related to graphic issues.
+
This setting requires a restart.
Type: <<types,String>>
@@ -3510,6 +3531,7 @@ This setting is only available with the QtWebEngine backend.
Turn on Qt HighDPI scaling.
This is equivalent to setting QT_AUTO_SCREEN_SCALE_FACTOR=1 or QT_ENABLE_HIGHDPI_SCALING=1 (Qt >= 5.14) in the environment.
It's off by default as it can cause issues with some bitmap fonts. As an alternative to this, it's possible to set font sizes and the `zoom.default` setting.
+
This setting requires a restart.
Type: <<types,Bool>>
@@ -3520,6 +3542,7 @@ Default: +pass:[false]+
=== qt.low_end_device_mode
When to use Chromium's low-end device mode.
This improves the RAM usage of renderer processes, at the expense of performance.
+
This setting requires a restart.
Type: <<types,String>>
@@ -3542,6 +3565,7 @@ See the following pages for more details:
- https://www.chromium.org/developers/design-documents/process-models
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
+
This setting requires a restart.
Type: <<types,String>>
diff --git a/doc/quickstart.asciidoc b/doc/quickstart.asciidoc
index 8d8f21aa0..ab9298fa6 100644
--- a/doc/quickstart.asciidoc
+++ b/doc/quickstart.asciidoc
@@ -46,24 +46,25 @@ If you get stuck, you can get help in multiple ways:
* The `:help` command inside qutebrowser shows the built-in documentation.
Additionally, each command can be started with a `--help` flag to show its
help.
-* IRC channel: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
+* Chat via the IRC channel: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
http://freenode.net/[Freenode]
(https://webchat.freenode.net/?channels=#qutebrowser[webchat])
-* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] (
-https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[subscribe])
+* On Reddit: https://www.reddit.com/r/qutebrowser/[/r/qutebrowser]
+* Via https://github.com/qutebrowser/qutebrowser/discussions[GitHub Discussions]
+* Using the mailinglist: mailto:qutebrowser@lists.qutebrowser.org[]
+(https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[subscribe])
Donating
--------
-Working on qutebrowser is a very rewarding hobby, but like (nearly) all hobbies
-it also costs some money. Namely I have to pay for the server and domain, and
-do occasional hardware upgrades footnote:[It turned out a 160 GB SSD is rather
-small - the VMs and custom Qt builds I use for testing/developing qutebrowser
-need about 100 GB of space].
+qutebrowser's primary maintainer, The-Compiler, is currently working part-time on
+qutebrowser, funded by donations.
-If you want to give me a beer or a pizza back, I'm trying to make it as easy as
-possible for you to do so. If some other way would be easier for you, please
-get in touch!
+To sustain this for a long time, your help is needed! Check the
+https://github.com/sponsors/The-Compiler/[GitHub Sponsors page] for more information.
+Depending on your sign-up date and how long you keep a certain level, you can get
+qutebrowser t-shirts, stickers and more!
-* PayPal: me@the-compiler.org
-* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
+Alternatively, there are also various options available for one-time donations, see the
+https://github.com/qutebrowser/qutebrowser/blob/master/README.asciidoc#donating[donation section]
+in the README for details.
diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt
index 6c76979ae..c543088c6 100644
--- a/misc/requirements/requirements-dev.txt
+++ b/misc/requirements/requirements-dev.txt
@@ -2,15 +2,15 @@
bump2version==1.0.0
certifi==2020.6.20
-cffi==1.14.1
+cffi==1.14.2
chardet==3.0.4
colorama==0.4.3
cryptography==3.0
cssutils==1.0.2
github3.py==1.3.0
-hunter==3.1.3
+hunter==3.2.1
idna==2.10
-jwcrypto==0.7
+jwcrypto==0.8
manhole==1.6.0
packaging==20.4
pycparser==2.20
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index 8a62fcdda..00d22b236 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==19.3.0
+attrs==20.1.0
flake8==3.8.3
flake8-bugbear==20.1.4
flake8-builtins==1.5.3
@@ -18,7 +18,7 @@ flake8-tuple==0.4.1
mccabe==0.6.1
pep8-naming==0.11.1
pycodestyle==2.6.0
-pydocstyle==5.0.2
+pydocstyle==5.1.0
pyflakes==2.2.0
six==1.15.0
snowballstemmer==2.0.0
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index b06cf2e8e..09d11ea6c 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -13,4 +13,4 @@ Pygments==2.6.1
-e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs
six==1.15.0
typed-ast==1.4.1
-typing-extensions==3.7.4.2
+typing-extensions==3.7.4.3
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 4394c8044..6de7b6fa8 100644
--- a/misc/requirements/requirements-pyinstaller.txt
+++ b/misc/requirements/requirements-pyinstaller.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.17
--e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=pyinstaller
-pyinstaller-hooks-contrib==2020.6
+pyinstaller==4.0
+pyinstaller-hooks-contrib==2020.7
diff --git a/misc/requirements/requirements-pyinstaller.txt-raw b/misc/requirements/requirements-pyinstaller.txt-raw
index f6cb8ce72..c313980b0 100644
--- a/misc/requirements/requirements-pyinstaller.txt-raw
+++ b/misc/requirements/requirements-pyinstaller.txt-raw
@@ -1,4 +1 @@
--e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
-
-# remove @commit-id for scm installs
-#@ replace: @.*# @develop#
+PyInstaller
diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt
index 8c1055df6..3c7440627 100644
--- a/misc/requirements/requirements-pylint.txt
+++ b/misc/requirements/requirements-pylint.txt
@@ -2,13 +2,13 @@
astroid==2.3.3 # rq.filter: < 2.4
certifi==2020.6.20
-cffi==1.14.1
+cffi==1.14.2
chardet==3.0.4
cryptography==3.0
github3.py==1.3.0
idna==2.10
isort==4.3.21
-jwcrypto==0.7
+jwcrypto==0.8
lazy-object-proxy==1.4.3
mccabe==0.6.1
pycparser==2.20
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index 08c5d57c8..da6447009 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -16,7 +16,7 @@ pytz==2020.1
requests==2.24.0
six==1.15.0
snowballstemmer==2.0.0
-Sphinx==3.1.2
+Sphinx==3.2.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index 1411c984e..a82ba796c 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -1,20 +1,20 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-attrs==19.3.0
+attrs==20.1.0
beautifulsoup4==4.9.1
certifi==2020.6.20
chardet==3.0.4
-cheroot==8.4.2
+cheroot==8.4.5
click==7.1.2
# colorama==0.4.3
coverage==5.2.1
EasyProcess==0.3
Flask==1.1.2
glob2==0.7
-hunter==3.1.3
-hypothesis==5.23.7
+hunter==3.2.1
+hypothesis==5.29.0
idna==2.10
-iniconfig==1.0.0
+iniconfig==1.0.1
itsdangerous==1.1.0
jaraco.functools==3.0.1 ; python_version>="3.6"
# Jinja2==2.11.2
@@ -33,9 +33,9 @@ pyparsing==2.4.7
pytest==6.0.1
pytest-bdd==3.4.0
pytest-benchmark==3.2.3
-pytest-cov==2.10.0
+pytest-cov==2.10.1
pytest-instafail==0.4.2
-pytest-mock==3.2.0
+pytest-mock==3.3.0
pytest-qt==3.3.0
pytest-repeat==0.8.0
pytest-rerunfailures==9.0
@@ -46,9 +46,10 @@ requests-file==1.5.1
six==1.15.0
sortedcontainers==2.2.2
soupsieve==2.0.1
-tldextract==2.2.2
+tldextract==2.2.3
toml==0.10.1
urllib3==1.25.10
-vulture==1.6
+vulture==2.1 ; python_version>="3.6"
Werkzeug==1.0.1
-jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
+jaraco.functools==2.0; python_version<"3.6"
+vulture==1.6; python_version<"3.6"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index 779078021..f063a3512 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -30,5 +30,9 @@ PyVirtualDisplay
tldextract
#@ markers: jaraco.functools python_version>="3.6"
-#@ add: jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0
+#@ add: jaraco.functools==2.0; python_version<"3.6"
+
+#@ markers: vulture python_version>="3.6"
+#@ add: vulture==1.6; python_version<"3.6"
+
#@ ignore: Jinja2, MarkupSafe, colorama
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 21b252930..3fb7595ad 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -9,7 +9,7 @@ py==1.9.0
pyparsing==2.4.7
six==1.15.0
toml==0.10.1
-tox==3.18.1
+tox==3.19.0
tox-pip-version==0.0.7
tox-venv==0.4.0
-virtualenv==20.0.28
+virtualenv==20.0.31
diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt
index 112606543..70848d8ef 100644
--- a/misc/requirements/requirements-vulture.txt
+++ b/misc/requirements/requirements-vulture.txt
@@ -1,3 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-vulture==1.6
+toml==0.10.1
+vulture==2.1
diff --git a/misc/userscripts/qute-pass b/misc/userscripts/qute-pass
index c624503c4..9d078e94f 100755
--- a/misc/userscripts/qute-pass
+++ b/misc/userscripts/qute-pass
@@ -27,7 +27,7 @@ for example: "github.com/cryzed" or "websites/github.com". How the username and
password are determined is freely configurable using the CLI arguments. As an
example, if you instead store the username as part of the secret (and use a
site's name as filename), instead of the default configuration, use
-`--username-target secret` and `--username-regex "username: (.+)"`.
+`--username-target secret` and `--username-pattern "username: (.+)"`.
The login information is inserted by emulating key events using qutebrowser's
fake-key command in this manner: [USERNAME]<Tab>[PASSWORD], which is compatible
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index 6a1a96c84..4b9446ed5 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -1,27 +1,26 @@
#!/usr/bin/env node
-//
+//
// # Description
-//
+//
// Summarize the current page in a new tab, by processing it with the standalone readability
// library used for Firefox Reader View.
-//
+//
// # Prerequisites
-//
-// - NODE_PATH might be required to point to your global node libraries:
+//
+// - Setting NODE_PATH might be required to point qutebrowser to your global node libraries:
// export NODE_PATH=$NODE_PATH:$(npm root -g)
-// - Mozilla's readability library (npm install -g https://github.com/mozilla/readability.git)
-// NOTE: You might have to *login* as root for a system-wide installation to work (e.g. sudo -s)
+// - Mozilla's readability library (npm install -g @mozilla/readability)
// - jsdom (npm install -g jsdom)
// - qutejs (npm install -g qutejs)
-//
+//
// # Usage
-//
+//
// :spawn --userscript readability-js
-//
-// One may wish to define an easy to type command alias in Qutebrowser's configuration file:
+//
+// One may wish to define an easy to type command alias in qutebrowser's configuration file:
// c.aliases = {"readability" : "spawn --userscript readability-js", ...}
-const Readability = require('readability');
+const { Readability } = require('@mozilla/readability');
const qute = require('qutejs');
const JSDOM = require('jsdom').JSDOM;
const fs = require('fs');
diff --git a/pytest.ini b/pytest.ini
index a034a27b3..1235efb4b 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -38,6 +38,7 @@ markers =
unicode_locale: Tests which need an unicode locale to work
qtwebkit6021_xfail: Tests which would fail on WebKit version 602.1
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
+ qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 55131ce7d..20459b890 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -485,8 +485,7 @@ def _init_modules(*, args):
cache.init(q_app)
log.init.debug("Initializing downloads...")
- download_manager = qtnetworkdownloads.DownloadManager(parent=q_app)
- objreg.register('qtnetwork-download-manager', download_manager)
+ qtnetworkdownloads.init()
log.init.debug("Initializing Greasemonkey...")
greasemonkey.init()
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 05553a122..b7b2f3d91 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -72,9 +72,11 @@ def create(win_id: int,
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
tab_class = webenginetab.WebEngineTab # type: typing.Type[AbstractTab]
- else:
+ elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
+ else:
+ raise utils.Unreachable(objects.backend)
return tab_class(win_id=win_id, mode_manager=mode_manager, private=private,
parent=parent)
@@ -84,6 +86,8 @@ def init() -> None:
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
webenginetab.init()
+ return
+ assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
class WebTabError(Exception):
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 3a0468ada..ff18b5408 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -170,7 +170,7 @@ class CommandDispatcher:
elif mode == "stack-next":
tab = tab_deque.next(cur_tab)
else:
- raise NotImplementedError(
+ raise utils.Unreachable(
"Missing implementation for stack mode!")
except IndexError:
if not show_error:
@@ -562,7 +562,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
- 'decrement'])
+ 'decrement', 'strip'])
@cmdutils.argument('count', value=cmdutils.Value.count)
def navigate(self, where: str, tab: bool = False, bg: bool = False,
window: bool = False, count: int = 1) -> None:
@@ -587,6 +587,7 @@ class CommandDispatcher:
Uses the
link:settings{outsuffix}#url.incdec_segments[url.incdec_segments]
config option.
+ - `strip`: Strip query and fragment from the current URL.
tab: Open in a new tab.
bg: Open in a background tab.
@@ -613,9 +614,7 @@ class CommandDispatcher:
handler = handlers[where]
handler(browsertab=widget, win_id=self._win_id, baseurl=url,
tab=tab, background=bg, window=window)
- elif where in ['up', 'increment', 'decrement']:
- if where == 'up':
- url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
+ elif where in ['up', 'increment', 'decrement', 'strip']:
new_url = handlers[where](url, count)
self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover
@@ -1627,9 +1626,31 @@ class CommandDispatcher:
tab.search.prev_result()
tab.search.prev_result(result_cb=cb)
+ def _jseval_cb(self, out):
+ """Show the data returned from JS."""
+ if out is None:
+ # Getting the actual error (if any) seems to be difficult.
+ # The error does end up in
+ # BrowserPage.javaScriptConsoleMessage(), but
+ # distinguishing between :jseval errors and errors from the
+ # webpage is not trivial...
+ message.info('No output or error')
+ else:
+ # The output can be a string, number, dict, array, etc. But
+ # *don't* output too much data, as this will make
+ # qutebrowser hang
+ out = str(out)
+ if len(out) > 5000:
+ out = out[:5000] + ' [...trimmed...]'
+ message.info(out)
+
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True)
- def jseval(self, js_code: str, file: bool = False, quiet: bool = False, *,
+ def jseval(self, js_code: str,
+ file: bool = False,
+ url: bool = False,
+ quiet: bool = False,
+ *,
world: typing.Union[usertypes.JsWorld, int] = None) -> None:
"""Evaluate a JavaScript string.
@@ -1639,33 +1660,16 @@ class CommandDispatcher:
If the path is relative, the file is searched in a js/ subdir
in qutebrowser's data dir, e.g.
`~/.local/share/qutebrowser/js`.
+ url: Interpret js-code as a `javascript:...` URL.
quiet: Don't show resulting JS object.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
run the snippet in.
"""
+ cmdutils.check_exclusive((file, url), 'fu')
+
if world is None:
world = usertypes.JsWorld.jseval
-
- if quiet:
- jseval_cb = None
- else:
- def jseval_cb(out):
- """Show the data returned from JS."""
- if out is None:
- # Getting the actual error (if any) seems to be difficult.
- # The error does end up in
- # BrowserPage.javaScriptConsoleMessage(), but
- # distinguishing between :jseval errors and errors from the
- # webpage is not trivial...
- message.info('No output or error')
- else:
- # The output can be a string, number, dict, array, etc. But
- # *don't* output too much data, as this will make
- # qutebrowser hang
- out = str(out)
- if len(out) > 5000:
- out = out[:5000] + ' [...trimmed...]'
- message.info(out)
+ jseval_cb = None if quiet else self._jseval_cb
if file:
path = os.path.expanduser(js_code)
@@ -1677,6 +1681,11 @@ class CommandDispatcher:
js_code = f.read()
except OSError as e:
raise cmdutils.CommandError(str(e))
+ elif url:
+ try:
+ js_code = urlutils.parse_javascript_url(QUrl(js_code))
+ except urlutils.Error as e:
+ raise cmdutils.CommandError(str(e))
widget = self._current_widget()
try:
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index a02918495..3c3932c5f 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -427,6 +427,7 @@ class AbstractDownloadItem(QObject):
raw_headers: The headers sent by the server.
_filename: The filename of the download.
_dead: Whether the Download has _die()'d.
+ _manager: The DownloadManager which started this download.
Signals:
data_changed: The downloads metadata changed.
@@ -448,8 +449,9 @@ class AbstractDownloadItem(QObject):
remove_requested = pyqtSignal()
pdfjs_requested = pyqtSignal(str, QUrl)
- def __init__(self, parent=None):
+ def __init__(self, manager, parent=None):
super().__init__(parent)
+ self._manager = manager
self.done = False
self.stats = DownloadItemStats(self)
self.index = 0
@@ -651,7 +653,7 @@ class AbstractDownloadItem(QObject):
"""Finish initialization based on self._filename."""
raise NotImplementedError
- def _ask_confirm_question(self, title, msg):
+ def _ask_confirm_question(self, title, msg, *, custom_yes_action=None):
"""Ask a confirmation question for the download."""
raise NotImplementedError
@@ -746,7 +748,13 @@ class AbstractDownloadItem(QObject):
last_used_directory = os.path.dirname(self._filename)
log.downloads.debug("Setting filename to {}".format(self._filename))
- if force_overwrite:
+ if self._get_conflicting_download():
+ txt = ("<b>{}</b> is already downloading. Cancel and "
+ "re-download?".format(html.escape(self._filename)))
+ self._ask_confirm_question(
+ "Cancel other download?", txt,
+ custom_yes_action=self._cancel_conflicting_download)
+ elif force_overwrite:
self._after_set_filename()
elif os.path.isfile(self._filename):
# The file already exists, so ask the user if it should be
@@ -763,6 +771,28 @@ class AbstractDownloadItem(QObject):
else:
self._after_set_filename()
+ def _conflicts_with(self, other: 'AbstractDownloadItem') -> bool:
+ """Check if this download conflicts with the other given one."""
+ return (
+ other is not self and
+ other._filename == self._filename and # pylint: disable=protected-access
+ not other.done
+ )
+
+ def _get_conflicting_download(self):
+ """Return another potential active download with the same name."""
+ for download in self._manager.downloads:
+ if self._conflicts_with(download):
+ return download
+ return None
+
+ def _cancel_conflicting_download(self):
+ """Cancel any conflicting download and call _after_set_filename."""
+ conflicting_download = self._get_conflicting_download()
+ if conflicting_download:
+ conflicting_download.cancel(remove_data=False)
+ self._after_set_filename()
+
def _open_if_successful(self, cmdline):
"""Open the downloaded file, but only if it was successful.
@@ -947,6 +977,12 @@ class AbstractDownloadManager(QObject):
download.cancelled.connect(question.abort)
download.error.connect(question.abort)
+ @pyqtSlot()
+ def shutdown(self):
+ """Cancel all downloads when shutting down."""
+ for download in self.downloads:
+ download.cancel(remove_data=False)
+
class DownloadModel(QAbstractListModel):
diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py
index 002949a2b..78ca67cd8 100644
--- a/qutebrowser/browser/eventfilter.py
+++ b/qutebrowser/browser/eventfilter.py
@@ -27,45 +27,29 @@ from qutebrowser.misc import objects
from qutebrowser.keyinput import modeman
-class ChildEventFilter(QObject):
-
- """An event filter re-adding TabEventFilter on ChildEvent.
-
- This is needed because QtWebEngine likes to randomly change its
- focusProxy...
+class FocusWorkaroundEventFilter(QObject):
- FIXME:qtwebengine Add a test for this happening
+ """An event filter working Qt 5.11 keyboard focus issues.
- Attributes:
- _filter: The event filter to install.
- _widget: The widget expected to send out childEvents.
- _win_id: The window this ChildEventFilter lives in.
- _focus_workaround: Whether to enable a workaround for QTBUG-68076.
+ WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
"""
- def __init__(self, *, eventfilter, win_id, focus_workaround=False,
- widget=None, parent=None):
+ def __init__(self, win_id, widget, parent=None):
super().__init__(parent)
- self._filter = eventfilter
- self._widget = widget
self._win_id = win_id
- self._focus_workaround = focus_workaround
- if focus_workaround:
- assert widget is not None
-
- def _do_focus_workaround(self):
- """WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076."""
- if not self._focus_workaround:
- return
+ self._widget = widget
- assert self._widget is not None
+ def eventFilter(self, _obj, event):
+ """Act on ChildAdded events."""
+ if event.type() != QEvent.ChildAdded:
+ return False
pass_modes = [usertypes.KeyMode.command,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]
if modeman.instance(self._win_id).mode in pass_modes:
- return
+ return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
@@ -77,6 +61,28 @@ class ChildEventFilter(QObject):
if current_index == widget_index:
QTimer.singleShot(0, self._widget.setFocus)
+ return False
+
+
+class ChildEventFilter(QObject):
+
+ """An event filter re-adding TabEventFilter on ChildEvent.
+
+ This is needed because QtWebEngine likes to randomly change its
+ focusProxy...
+
+ FIXME:qtwebengine Add a test for this happening
+
+ Attributes:
+ _filter: The event filter to install.
+ _widget: The widget expected to send out childEvents.
+ """
+
+ def __init__(self, *, eventfilter, widget=None, parent=None):
+ super().__init__(parent)
+ self._filter = eventfilter
+ self._widget = widget
+
def eventFilter(self, obj, event):
"""Act on ChildAdded events."""
if event.type() == QEvent.ChildAdded:
@@ -89,7 +95,6 @@ class ChildEventFilter(QObject):
assert obj is self._widget
child.installEventFilter(self._filter)
- self._do_focus_workaround()
elif event.type() == QEvent.ChildRemoved:
child = event.child()
log.misc.debug("{}: removed child {}".format(obj, child))
diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py
index 6d99a3568..89d720682 100644
--- a/qutebrowser/browser/greasemonkey.py
+++ b/qutebrowser/browser/greasemonkey.py
@@ -136,6 +136,7 @@ class GreasemonkeyScript:
those by forcing them to use document-end instead.
"""
if objects.backend != usertypes.Backend.QtWebEngine:
+ assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
return False
elif not qtutils.version_check('5.12', compiled=False):
return False
diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py
index cf0c1a59a..b7221dc15 100644
--- a/qutebrowser/browser/history.py
+++ b/qutebrowser/browser/history.py
@@ -401,3 +401,5 @@ def init(parent=None):
if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover
from qutebrowser.browser.webkit import webkithistory
webkithistory.init(web_history)
+ return
+ assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py
index c13ebc90c..390762ae0 100644
--- a/qutebrowser/browser/inspector.py
+++ b/qutebrowser/browser/inspector.py
@@ -30,7 +30,7 @@ from PyQt5.QtGui import QCloseEvent
from qutebrowser.browser import eventfilter
from qutebrowser.config import configfiles
-from qutebrowser.utils import log, usertypes
+from qutebrowser.utils import log, usertypes, utils
from qutebrowser.keyinput import modeman
from qutebrowser.misc import miscwidgets, objects
@@ -55,9 +55,10 @@ def create(*, splitter: 'miscwidgets.InspectorSplitter',
else:
return webengineinspector.LegacyWebEngineInspector(
splitter, win_id, parent)
- else:
+ elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitinspector
return webkitinspector.WebKitInspector(splitter, win_id, parent)
+ raise utils.Unreachable(objects.backend)
class Position(enum.Enum):
@@ -91,15 +92,12 @@ class _EventFilter(QObject):
the QWebInspector.
"""
- def __init__(self, win_id: int, parent: QObject) -> None:
- super().__init__(parent)
- self._win_id = win_id
+ clicked = pyqtSignal()
def eventFilter(self, _obj: QObject, event: QEvent) -> bool:
- """Enter insert mode if the inspector is clicked."""
+ """Translate mouse presses to a clicked signal."""
if event.type() == QEvent.MouseButtonPress:
- modeman.enter(self._win_id, usertypes.KeyMode.insert,
- reason='Inspector clicked', only_if_normal=True)
+ self.clicked.emit()
return False
@@ -125,10 +123,12 @@ class AbstractWebInspector(QWidget):
self._layout = miscwidgets.WrapperLayout(self)
self._splitter = splitter
self._position = None # type: typing.Optional[Position]
- self._event_filter = _EventFilter(win_id, parent=self)
+ self._win_id = win_id
+
+ self._event_filter = _EventFilter(parent=self)
+ self._event_filter.clicked.connect(self._on_clicked)
self._child_event_filter = eventfilter.ChildEventFilter(
eventfilter=self._event_filter,
- win_id=win_id,
parent=self)
def _set_widget(self, widget: QWidget) -> None:
@@ -156,6 +156,13 @@ class AbstractWebInspector(QWidget):
"""
return False
+ @pyqtSlot()
+ def _on_clicked(self) -> None:
+ """Enter insert mode if a docked inspector was clicked."""
+ if self._position != Position.window:
+ modeman.enter(self._win_id, usertypes.KeyMode.insert,
+ reason='Inspector clicked', only_if_normal=True)
+
def set_position(self, position: typing.Optional[Position]) -> None:
"""Set the position of the inspector.
diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py
index 11be02c67..194246344 100644
--- a/qutebrowser/browser/navigate.py
+++ b/qutebrowser/browser/navigate.py
@@ -132,6 +132,8 @@ def path_up(url, count):
url: The current url.
count: The number of levels to go up in the url.
"""
+ urlutils.ensure_valid(url)
+ url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
path = url.path()
if not path or path == '/':
raise Error("Can't go up!")
@@ -142,6 +144,14 @@ def path_up(url, count):
return url
+def strip(url, count):
+ """Strip fragment/query from a URL."""
+ if count != 1:
+ raise Error("Count is not supported when stripping URL components")
+ urlutils.ensure_valid(url)
+ return url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
+
+
def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements."""
# First check for <link rel="prev(ious)|next">
diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py
index 18d2f060b..770c26aad 100644
--- a/qutebrowser/browser/network/proxy.py
+++ b/qutebrowser/browser/network/proxy.py
@@ -23,7 +23,7 @@ from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
-from qutebrowser.utils import message, usertypes, urlutils
+from qutebrowser.utils import message, usertypes, urlutils, utils
from qutebrowser.misc import objects
from qutebrowser.browser.network import pac
@@ -105,8 +105,10 @@ class ProxyFactory(QNetworkProxyFactory):
proxy = urlutils.proxy_from_url(QUrl('direct://'))
assert not isinstance(proxy, pac.PACFetcher)
proxies = [proxy]
- else:
+ elif objects.backend == usertypes.Backend.QtWebKit:
proxies = proxy.resolve(query)
+ else:
+ raise utils.Unreachable(objects.backend)
else:
proxies = [proxy]
for proxy in proxies:
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index 0b14ab50a..0bafeeaf9 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -27,10 +27,12 @@ import typing
import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
+from PyQt5.QtWidgets import QApplication
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from qutebrowser.config import config, websettings
-from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug
+from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg
+from qutebrowser.misc import quitter
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
@@ -70,7 +72,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
target file.
_read_timer: A Timer which reads the QNetworkReply into self._buffer
periodically.
- _manager: The DownloadManager which started this download
_reply: The QNetworkReply associated with this download.
_autoclose: Whether to close the associated file when the download is
done.
@@ -90,12 +91,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
Args:
reply: The QNetworkReply to download.
"""
- super().__init__(parent=manager)
+ super().__init__(manager=manager, parent=manager)
self.fileobj = None # type: typing.Optional[typing.IO[bytes]]
self.raw_headers = {} # type: typing.Dict[bytes, bytes]
self._autoclose = True
- self._manager = manager
self._retry_info = None
self._reply = None
self._buffer = io.BytesIO()
@@ -206,11 +206,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _after_set_filename(self):
self._create_fileobj()
- def _ask_confirm_question(self, title, msg):
+ def _ask_confirm_question(self, title, msg, *, custom_yes_action=None):
+ yes_action = custom_yes_action or self._after_set_filename
no_action = functools.partial(self.cancel, remove_data=False)
url = 'file://{}'.format(self._filename)
- message.confirm_async(title=title, text=msg,
- yes_action=self._after_set_filename,
+ message.confirm_async(title=title, text=msg, yes_action=yes_action,
no_action=no_action, cancel_action=no_action,
abort_on=[self.cancelled, self.error], url=url)
@@ -578,3 +578,10 @@ class DownloadManager(downloads.AbstractDownloadManager):
if download._uses_nam(nam): # pylint: disable=protected-access
nam.adopt_download(download)
return nam.adopted_downloads
+
+
+def init():
+ """Initialize the global QtNetwork download manager."""
+ download_manager = DownloadManager(parent=QApplication.instance())
+ objreg.register('qtnetwork-download-manager', download_manager)
+ quitter.instance.shutting_down.connect(download_manager.shutdown)
diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py
index 80266a1c8..16c7f020a 100644
--- a/qutebrowser/browser/webengine/webenginedownloads.py
+++ b/qutebrowser/browser/webengine/webenginedownloads.py
@@ -28,7 +28,7 @@ from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QObject
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
from qutebrowser.browser import downloads, pdfjs
-from qutebrowser.utils import debug, usertypes, message, log, qtutils
+from qutebrowser.utils import debug, usertypes, message, log, qtutils, objreg
class DownloadItem(downloads.AbstractDownloadItem):
@@ -40,8 +40,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
"""
def __init__(self, qt_item: QWebEngineDownloadItem,
+ manager: downloads.AbstractDownloadManager,
parent: QObject = None) -> None:
- super().__init__(parent)
+ super().__init__(manager=manager, parent=manager)
self._qt_item = qt_item
qt_item.downloadProgress.connect( # type: ignore[attr-defined]
self.stats.on_download_progress)
@@ -140,14 +141,15 @@ class DownloadItem(downloads.AbstractDownloadItem):
"state {} (not in requested state)!".format(
filename, self, state_name))
- def _ask_confirm_question(self, title, msg):
+ def _ask_confirm_question(self, title, msg, *, custom_yes_action=None):
+ yes_action = custom_yes_action or self._after_set_filename
no_action = functools.partial(self.cancel, remove_data=False)
question = usertypes.Question()
question.title = title
question.text = msg
question.url = 'file://{}'.format(self._filename)
question.mode = usertypes.PromptMode.yesno
- question.answered_yes.connect(self._after_set_filename)
+ question.answered_yes.connect(yes_action)
question.answered_no.connect(no_action)
question.cancelled.connect(no_action)
self.cancelled.connect(question.abort)
@@ -185,6 +187,26 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._qt_item.accept()
+ def _get_conflicting_download(self):
+ """Return another potential active download with the same name.
+
+ webenginedownloads.DownloadItem needs to look for downloads both in its
+ manager and in qtnetwork-download-manager as both are used
+ simultaneously.
+
+ This method can be safely removed once #2328 is fixed.
+ """
+ conflicting_download = super()._get_conflicting_download()
+ if conflicting_download:
+ return conflicting_download
+
+ qtnetwork_download_manager = objreg.get(
+ 'qtnetwork-download-manager')
+ for download in qtnetwork_download_manager.downloads:
+ if self._conflicts_with(download):
+ return download
+ return None
+
def _get_suggested_filename(path):
"""Convert a path we got from chromium to a suggested filename.
@@ -244,7 +266,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
suggested_filename = _get_suggested_filename(qt_item.path())
use_pdfjs = pdfjs.should_use_pdfjs(qt_item.mimeType(), qt_item.url())
- download = DownloadItem(qt_item)
+ download = DownloadItem(qt_item, manager=self)
self._init_item(download, auto_remove=use_pdfjs,
suggested_filename=suggested_filename)
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index ad22c7d62..f5f4e9c31 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -55,45 +55,45 @@ class _SettingsWrapper:
For read operations, the default profile value is always used.
"""
- def __init__(self):
- self._settings = [default_profile.settings()]
+ def _settings(self):
+ yield default_profile.settings()
if private_profile:
- self._settings.append(private_profile.settings())
+ yield private_profile.settings()
def setAttribute(self, attribute, on):
- for settings in self._settings:
+ for settings in self._settings():
settings.setAttribute(attribute, on)
def setFontFamily(self, which, family):
- for settings in self._settings:
+ for settings in self._settings():
settings.setFontFamily(which, family)
def setFontSize(self, fonttype, size):
- for settings in self._settings:
+ for settings in self._settings():
settings.setFontSize(fonttype, size)
def setDefaultTextEncoding(self, encoding):
- for settings in self._settings:
+ for settings in self._settings():
settings.setDefaultTextEncoding(encoding)
def setUnknownUrlSchemePolicy(self, policy):
- for settings in self._settings:
+ for settings in self._settings():
settings.setUnknownUrlSchemePolicy(policy)
def testAttribute(self, attribute):
- return self._settings[0].testAttribute(attribute)
+ return default_profile.settings().testAttribute(attribute)
def fontSize(self, fonttype):
- return self._settings[0].fontSize(fonttype)
+ return default_profile.settings().fontSize(fonttype)
def fontFamily(self, which):
- return self._settings[0].fontFamily(which)
+ return default_profile.settings().fontFamily(which)
def defaultTextEncoding(self):
- return self._settings[0].defaultTextEncoding()
+ return default_profile.settings().defaultTextEncoding()
def unknownUrlSchemePolicy(self):
- return self._settings[0].unknownUrlSchemePolicy()
+ return default_profile.settings().unknownUrlSchemePolicy()
class WebEngineSettings(websettings.AbstractSettings):
@@ -360,9 +360,9 @@ def init_user_agent():
_init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent())
-def _init_profiles():
- """Init the two used QWebEngineProfiles."""
- global default_profile, private_profile
+def _init_default_profile():
+ """Init the default QWebEngineProfile."""
+ global default_profile
default_profile = QWebEngineProfile.defaultProfile()
init_user_agent()
@@ -376,6 +376,11 @@ def _init_profiles():
default_profile.setter.init_profile()
default_profile.setter.set_persistent_cookie_policy()
+
+def init_private_profile():
+ """Init the private QWebEngineProfile."""
+ global private_profile
+
if not qtutils.is_single_process():
private_profile = QWebEngineProfile()
private_profile.setter = ProfileSetter( # type: ignore[attr-defined]
@@ -450,7 +455,8 @@ def init(args):
webenginequtescheme.init()
spell.init()
- _init_profiles()
+ _init_default_profile()
+ init_private_profile()
config.instance.changed.connect(_update_settings)
global global_settings
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index fe4d37745..a139f3d2f 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -38,7 +38,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
cookies, webenginedownloads,
webenginesettings, certificateerror)
-from qutebrowser.misc import miscwidgets, objects
+from qutebrowser.misc import miscwidgets, objects, quitter
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug)
from qutebrowser.keyinput import modeman
@@ -74,6 +74,7 @@ def init():
if webenginesettings.private_profile:
download_manager.install(webenginesettings.private_profile)
objreg.register('webengine-download-manager', download_manager)
+ quitter.instance.shutting_down.connect(download_manager.shutdown)
log.init.debug("Initializing cookie filter...")
cookies.install_filter(webenginesettings.default_profile)
@@ -1392,15 +1393,20 @@ class WebEngineTab(browsertab.AbstractTab):
fp = self._widget.focusProxy()
if fp is not None:
fp.installEventFilter(self._tab_event_filter)
+
self._child_event_filter = eventfilter.ChildEventFilter(
eventfilter=self._tab_event_filter,
widget=self._widget,
- win_id=self.win_id,
- focus_workaround=qtutils.version_check(
- '5.11', compiled=False, exact=True),
parent=self)
self._widget.installEventFilter(self._child_event_filter)
+ if qtutils.version_check('5.11', compiled=False, exact=True):
+ focus_event_filter = eventfilter.FocusWorkaroundEventFilter(
+ win_id=self.win_id,
+ widget=self._widget,
+ parent=self)
+ self._widget.installEventFilter(focus_event_filter)
+
@pyqtSlot()
def _restore_zoom(self):
if sip.isdeleted(self._widget):
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 7a2addc04..cad9badee 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -55,6 +55,24 @@ class WebKitAction(browsertab.AbstractAction):
def show_source(self, pygments=False):
self._show_source_pygments()
+ def run_string(self, name: str) -> None:
+ """Add special cases for new API.
+
+ Those were added to QtWebKit 5.212 (which we enforce), but we don't get
+ the new API from PyQt. Thus, we'll need to use the raw numbers.
+ """
+ new_actions = {
+ # https://github.com/qtwebkit/qtwebkit/commit/a96d9ef5d24b02d996ad14ff050d0e485c9ddc97
+ 'RequestClose': QWebPage.ToggleVideoFullscreen + 1,
+ # https://github.com/qtwebkit/qtwebkit/commit/96b9ba6269a5be44343635a7aaca4a153ea0366b
+ 'Unselect': QWebPage.ToggleVideoFullscreen + 2,
+ }
+ if name in new_actions:
+ self._widget.triggerPageAction(new_actions[name])
+ return
+
+ super().run_string(name)
+
class WebKitPrinting(browsertab.AbstractPrinting):
diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py
index 11e7c96d8..a8e58b8a2 100644
--- a/qutebrowser/completion/completiondelegate.py
+++ b/qutebrowser/completion/completiondelegate.py
@@ -26,7 +26,7 @@ import re
import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
-from PyQt5.QtCore import QRectF, QSize, Qt
+from PyQt5.QtCore import QRectF, QRegularExpression, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout, QSyntaxHighlighter,
QTextCharFormat)
@@ -41,14 +41,23 @@ class _Highlighter(QSyntaxHighlighter):
super().__init__(doc)
self._format = QTextCharFormat()
self._format.setForeground(color)
- self._pattern = pattern
+ words = pattern.split()
+ words.sort(key=len, reverse=True)
+ pat = "|".join(re.escape(word) for word in words)
+ self._expression = QRegularExpression(
+ pat, QRegularExpression.CaseInsensitiveOption
+ )
def highlightBlock(self, text):
"""Override highlightBlock for custom highlighting."""
- for match in re.finditer(self._pattern, text, re.IGNORECASE):
- start, end = match.span()
- length = end - start
- self.setFormat(start, length, self._format)
+ match_iterator = self._expression.globalMatch(text)
+ while match_iterator.hasNext():
+ match = match_iterator.next()
+ self.setFormat(
+ match.capturedStart(),
+ match.capturedLength(),
+ self._format
+ )
class CompletionItemDelegate(QStyledItemDelegate):
@@ -226,12 +235,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
if index.column() in columns_to_filter and pattern:
- pat = re.escape(pattern).replace(r'\ ', r'|')
if self._opt.state & QStyle.State_Selected:
color = config.val.colors.completion.item.selected.match.fg
else:
color = config.val.colors.completion.match.fg
- _Highlighter(self._doc, pat, color)
+ _Highlighter(self._doc, pattern, color)
self._doc.setPlainText(self._opt.text)
else:
self._doc.setHtml(
diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py
index b4f565d77..50d5bdf62 100644
--- a/qutebrowser/completion/completionwidget.py
+++ b/qutebrowser/completion/completionwidget.py
@@ -205,6 +205,49 @@ class CompletionView(QTreeView):
raise utils.Unreachable
+ def _next_page(self, upwards):
+ """Return the index a page away from the selected index.
+
+ Args:
+ upwards: Get previous item, not next.
+
+ Return:
+ A QModelIndex.
+ """
+ old_idx = self.selectionModel().currentIndex()
+ idx = old_idx
+ model = self.model()
+
+ if not idx.isValid():
+ # No item selected yet
+ return model.last_item() if upwards else model.first_item()
+
+ # Find height of each CompletionView element
+ element_height = self.visualRect(idx).height()
+ page_length = self.height() // element_height
+
+ # Skip one pageful, except leave one old line visible
+ offset = -(page_length - 1) if upwards else page_length - 1
+ idx = model.sibling(old_idx.row() + offset, old_idx.column(), old_idx)
+
+ # Skip category headers
+ while idx.isValid() and not idx.parent().isValid():
+ idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
+
+ if idx.isValid():
+ return idx
+
+ border_item = model.first_item() if upwards else model.last_item()
+
+ # Wrap around if we were already at the beginning/end
+ if old_idx == border_item:
+ return self._next_idx(upwards)
+
+ # Select the first/last item before wrapping around
+ if upwards:
+ self.scrollTo(border_item.parent())
+ return border_item
+
def _next_category_idx(self, upwards):
"""Get the index of the previous/next category.
@@ -238,14 +281,17 @@ class CompletionView(QTreeView):
@cmdutils.register(instance='completion',
modes=[usertypes.KeyMode.command], scope='window')
- @cmdutils.argument('which', choices=['next', 'prev', 'next-category',
- 'prev-category'])
+ @cmdutils.argument('which', choices=['next', 'prev',
+ 'next-category', 'prev-category',
+ 'next-page', 'prev-page'])
@cmdutils.argument('history', flag='H')
def completion_item_focus(self, which, history=False):
"""Shift the focus of the completion menu to another item.
Args:
- which: 'next', 'prev', 'next-category', or 'prev-category'.
+ which: 'next', 'prev',
+ 'next-category', 'prev-category',
+ 'next-page', or 'prev-page'.
history: Navigate through command history if no text was typed.
"""
if history:
@@ -266,12 +312,14 @@ class CompletionView(QTreeView):
selmodel = self.selectionModel()
indices = {
- 'next': self._next_idx(upwards=False),
- 'prev': self._next_idx(upwards=True),
- 'next-category': self._next_category_idx(upwards=False),
- 'prev-category': self._next_category_idx(upwards=True),
+ 'next': lambda: self._next_idx(upwards=False),
+ 'prev': lambda: self._next_idx(upwards=True),
+ 'next-category': lambda: self._next_category_idx(upwards=False),
+ 'prev-category': lambda: self._next_category_idx(upwards=True),
+ 'next-page': lambda: self._next_page(upwards=False),
+ 'prev-page': lambda: self._next_page(upwards=True),
}
- idx = indices[which]
+ idx = indices[which]()
if not idx.isValid():
return
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index b0e060ceb..2d3ac9d42 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -3320,6 +3320,8 @@ bindings.default:
<Tab>: completion-item-focus next
<Ctrl-Tab>: completion-item-focus next-category
<Ctrl-Shift-Tab>: completion-item-focus prev-category
+ <PgDown>: completion-item-focus next-page
+ <PgUp>: completion-item-focus prev-page
<Ctrl-D>: completion-item-del
<Shift-Delete>: completion-item-del
<Ctrl-C>: completion-item-yank
diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py
index 418ae7140..0c517a14c 100644
--- a/qutebrowser/config/qtargs.py
+++ b/qutebrowser/config/qtargs.py
@@ -50,6 +50,7 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
argv += ['--' + arg for arg in config.val.qt.args]
if objects.backend != usertypes.Backend.QtWebEngine:
+ assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
return argv
feature_flags = [flag for flag in argv
@@ -307,6 +308,8 @@ def init_envvars() -> None:
os.environ['QT_QUICK_BACKEND'] = 'software'
elif software_rendering == 'chromium':
os.environ['QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND'] = '1'
+ else:
+ assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index 007758254..cc307dd75 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -30,7 +30,7 @@ from PyQt5.QtGui import QFont
import qutebrowser
from qutebrowser.config import config
-from qutebrowser.utils import log, usertypes, urlmatch, qtutils
+from qutebrowser.utils import log, usertypes, urlmatch, qtutils, utils
from qutebrowser.misc import objects, debugcachestats
UNSET = object()
@@ -269,9 +269,11 @@ def init(args: argparse.Namespace) -> None:
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
webenginesettings.init(args)
- else:
+ elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitsettings
webkitsettings.init(args)
+ else:
+ raise utils.Unreachable(objects.backend)
# Make sure special URLs always get JS support
for pattern in ['chrome://*/*', 'qute://*/*']:
@@ -280,12 +282,27 @@ def init(args: argparse.Namespace) -> None:
hide_userconfig=True)
+def clear_private_data() -> None:
+ """Clear cookies, cache and related data for private browsing sessions."""
+ if objects.backend == usertypes.Backend.QtWebEngine:
+ from qutebrowser.browser.webengine import webenginesettings
+ webenginesettings.init_private_profile()
+ elif objects.backend == usertypes.Backend.QtWebKit:
+ from qutebrowser.browser.webkit import cookies
+ assert cookies.ram_cookie_jar is not None
+ cookies.ram_cookie_jar.setAllCookies([])
+ else:
+ raise utils.Unreachable(objects.backend)
+
+
@pyqtSlot()
def shutdown() -> None:
"""Shut down QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings
webenginesettings.shutdown()
- else:
+ elif objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkitsettings
webkitsettings.shutdown()
+ else:
+ raise utils.Unreachable(objects.backend)
diff --git a/qutebrowser/javascript/whatsapp_web_quirk.user.js b/qutebrowser/javascript/whatsapp_web_quirk.user.js
index b8979d15e..801d300e1 100644
--- a/qutebrowser/javascript/whatsapp_web_quirk.user.js
+++ b/qutebrowser/javascript/whatsapp_web_quirk.user.js
@@ -9,7 +9,9 @@
if (document.querySelector("a[href='https://support.google.com/chrome/answer/95414']")) {
navigator.serviceWorker.getRegistration().then((registration) => {
- registration.unregister();
+ if (registration) {
+ registration.unregister();
+ }
document.location.reload();
});
}
diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py
index 89c0f4417..faccdc73c 100644
--- a/qutebrowser/mainwindow/mainwindow.py
+++ b/qutebrowser/mainwindow/mainwindow.py
@@ -32,7 +32,7 @@ from PyQt5.QtGui import QPalette
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
-from qutebrowser.config import config, configfiles, stylesheet
+from qutebrowser.config import config, configfiles, stylesheet, websettings
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja, debug)
from qutebrowser.mainwindow import messageview, prompt
@@ -231,10 +231,10 @@ class MainWindow(QWidget):
self._downloadview = downloadview.DownloadView(
model=self._download_model)
- self._private = config.val.content.private_browsing or private
+ self.is_private = config.val.content.private_browsing or private
self.tabbed_browser = tabbedbrowser.TabbedBrowser(
- win_id=self.win_id, private=self._private, parent=self
+ win_id=self.win_id, private=self.is_private, parent=self
) # type: tabbedbrowser.TabbedBrowser
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
window=self.win_id)
@@ -243,7 +243,8 @@ class MainWindow(QWidget):
# We need to set an explicit parent for StatusBar because it does some
# show/hide magic immediately which would mean it'd show up as a
# window.
- self.status = bar.StatusBar(win_id=self.win_id, private=self._private,
+ self.status = bar.StatusBar(win_id=self.win_id,
+ private=self.is_private,
parent=self)
self._add_widgets()
@@ -310,12 +311,17 @@ class MainWindow(QWidget):
if not widget.isVisible():
return
- size_hint = widget.sizeHint()
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
width = self.width() - 2 * padding
+ if widget.hasHeightForWidth():
+ height = widget.heightForWidth(width)
+ else:
+ height = widget.sizeHint().height()
left = padding
else:
+ size_hint = widget.sizeHint()
width = min(size_hint.width(), self.width() - 2 * padding)
+ height = size_hint.height()
left = (self.width() - width) // 2 if centered else 0
height_padding = 20
@@ -327,7 +333,7 @@ class MainWindow(QWidget):
else:
status_height = 0
bottom = self.height()
- top = self.height() - status_height - size_hint.height()
+ top = self.height() - status_height - height
top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(left, max(height_padding, top))
bottomright = QPoint(left + width, bottom)
@@ -339,7 +345,7 @@ class MainWindow(QWidget):
status_height = 0
top = 0
topleft = QPoint(left, top)
- bottom = status_height + size_hint.height()
+ bottom = status_height + height
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(left + width,
min(self.height() - height_padding, bottom))
@@ -674,15 +680,28 @@ class MainWindow(QWidget):
e.accept()
- try:
- last_visible = objreg.get('last-visible-main-window')
- if self is last_visible:
- objreg.delete('last-visible-main-window')
- except KeyError:
- pass
+ for key in ['last-visible-main-window', 'last-focused-main-window']:
+ try:
+ win = objreg.get(key)
+ if self is win:
+ objreg.delete(key)
+ except KeyError:
+ pass
sessions.session_manager.save_last_window_session()
self._save_geometry()
+ # Wipe private data if we close the last private window, but there are
+ # still other windows
+ if (
+ self.is_private and
+ len(objreg.window_registry) > 1 and
+ len([window for window in objreg.window_registry.values()
+ if window.is_private]) == 1
+ ):
+ log.destroy.debug("Wiping private data before closing last "
+ "private window")
+ websettings.clear_private_data()
+
log.destroy.debug("Closing window {}".format(self.win_id))
self.tabbed_browser.shutdown()
diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py
index 2a20447ab..1f6295d89 100644
--- a/qutebrowser/mainwindow/messageview.py
+++ b/qutebrowser/mainwindow/messageview.py
@@ -21,7 +21,7 @@
import typing
-from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from qutebrowser.config import config, stylesheet
@@ -36,6 +36,7 @@ class Message(QLabel):
super().__init__(text, parent)
self.replace = replace
self.setAttribute(Qt.WA_StyledBackground, True)
+ self.setWordWrap(True)
qss = """
padding-top: 2px;
padding-bottom: 2px;
@@ -64,8 +65,6 @@ class Message(QLabel):
"""
else: # pragma: no cover
raise ValueError("Invalid level {!r}".format(level))
- # We don't bother with set_register_stylesheet here as it's short-lived
- # anyways.
stylesheet.set_register(self, qss, update=False)
@@ -89,12 +88,6 @@ class MessageView(QWidget):
self._last_text = None
- def sizeHint(self):
- """Get the proposed height for the view."""
- height = sum(label.sizeHint().height() for label in self._messages)
- # The width isn't really relevant as we're expanding anyways.
- return QSize(-1, height)
-
@config.change_filter('messages.timeout')
def _set_clear_timer_interval(self):
"""Configure self._clear_timer according to the config."""
diff --git a/qutebrowser/mainwindow/windowundo.py b/qutebrowser/mainwindow/windowundo.py
index d3939f310..4a4ea5d66 100644
--- a/qutebrowser/mainwindow/windowundo.py
+++ b/qutebrowser/mainwindow/windowundo.py
@@ -38,7 +38,6 @@ class _WindowUndoEntry:
"""Information needed for :undo -w."""
- private = attr.ib()
geometry = attr.ib()
tab_stack = attr.ib()
@@ -60,9 +59,11 @@ class WindowUndoManager(QObject):
self._update_undo_stack_size()
def _on_window_closing(self, window):
+ if window.tabbed_browser.is_private:
+ return
+
self._undos.append(_WindowUndoEntry(
geometry=window.saveGeometry(),
- private=window.tabbed_browser.is_private,
tab_stack=window.tabbed_browser.undo_stack,
))
@@ -79,7 +80,7 @@ class WindowUndoManager(QObject):
"""
entry = self._undos.pop()
window = mainwindow.MainWindow(
- private=entry.private,
+ private=False,
geometry=entry.geometry,
)
window.show()
diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py
index 1d474b380..17fbb9956 100644
--- a/qutebrowser/misc/editor.py
+++ b/qutebrowser/misc/editor.py
@@ -65,7 +65,9 @@ class ExternalEditor(QObject):
def _cleanup(self):
"""Clean up temporary files after the editor closed."""
assert self._remove_file is not None
- if self._watcher is not None and self._watcher.files():
+ if (self._watcher is not None and
+ not sip.isdeleted(self._watcher) and
+ self._watcher.files()):
failed = self._watcher.removePaths(self._watcher.files())
if failed:
log.procs.error("Failed to unwatch paths: {}".format(failed))
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index 63e11ff68..e853b38f8 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -124,6 +124,7 @@ def is_single_process() -> bool:
"""Check whether QtWebEngine is running in single-process mode."""
if objects.backend == usertypes.Backend.QtWebKit:
return False
+ assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
args = QApplication.instance().arguments()
return '--single-process' in args
diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py
index 761853a18..a691b2cbc 100644
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -55,7 +55,12 @@ WEBENGINE_SCHEMES = [
]
-class InvalidUrlError(Exception):
+class Error(Exception):
+
+ """Base class for errors in this module."""
+
+
+class InvalidUrlError(Error):
"""Error raised if a function got an invalid URL."""
@@ -624,3 +629,28 @@ def proxy_from_url(url: QUrl) -> typing.Union[QNetworkProxy, pac.PACFetcher]:
if url.password():
proxy.setPassword(url.password())
return proxy
+
+
+def parse_javascript_url(url: QUrl) -> str:
+ """Get JavaScript source from the given URL.
+
+ See https://wiki.whatwg.org/wiki/URL_schemes#javascript:_URLs
+ and https://github.com/whatwg/url/issues/385
+ """
+ ensure_valid(url)
+ if url.scheme() != 'javascript':
+ raise Error("Expected a javascript:... URL")
+ if url.authority():
+ raise Error("URL contains unexpected components: {}"
+ .format(url.authority()))
+
+ code = url.path(QUrl.FullyDecoded)
+ if url.hasQuery():
+ code += '?' + url.query(QUrl.FullyDecoded)
+ if url.hasFragment():
+ code += '#' + url.fragment(QUrl.FullyDecoded)
+
+ if not code:
+ raise Error("Resulted in empty JavaScript code")
+
+ return code
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 0be1c1a29..476926d34 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -337,16 +337,14 @@ def _pdfjs_version() -> str:
else:
pdfjs_file = pdfjs_file.decode('utf-8')
version_re = re.compile(
- r"^ *(PDFJS\.version|var pdfjsVersion) = '([^']+)';$",
+ r"^ *(PDFJS\.version|(var|const) pdfjsVersion) = '(?P<version>[^']+)';$",
re.MULTILINE)
match = version_re.search(pdfjs_file)
- if not match:
- pdfjs_version = 'unknown'
- else:
- pdfjs_version = match.group(2)
+ pdfjs_version = 'unknown' if not match else match.group('version')
if file_path is None:
file_path = 'bundled'
+
return '{} ({})'.format(pdfjs_version, file_path)
@@ -360,6 +358,7 @@ def _chromium_version() -> str:
Qt 5.7: Chromium 49
49.0.2623.111 (2016-03-31)
+ 5.7.0: Security fixes from Chromium 50 and 51
5.7.1: Security fixes up to 54.0.2840.87 (2016-11-01)
Qt 5.8: Chromium 53
@@ -368,34 +367,64 @@ def _chromium_version() -> str:
Qt 5.9: Chromium 56
(LTS) 56.0.2924.122 (2017-01-25)
+ 5.9.0: Security fixes up to 56.0.2924.122 (?)
+ 5.9.1: Security fixes up to 59.0.3071.104 (2017-06-15)
+ 5.9.2: Security fixes up to 61.0.3163.79 (2017-09-05)
+ 5.9.3: Security fixes up to 62.0.3202.89 (2017-11-06)
+ 5.9.4: Security fixes up to 63.0.3239.132 (~2017-12-14)
+ 5.9.5: Security fixes up to 65.0.3325.146 (~2018-03-13)
+ 5.9.6: Security fixes up to 66.0.3359.170 (2018-05-10)
+ 5.9.7: Security fixes up to 69.0.3497.113 (~2018-09-11)
+ 5.9.8: Security fixes up to 72.0.3626.121 (2019-03-01)
5.9.9: Security fixes up to 78.0.3904.108 (2019-11-18)
Qt 5.10: Chromium 61
61.0.3163.140 (2017-09-05)
+ 5.10.0: Security fixes up to 62.0.3202.94 (2017-11-13)
5.10.1: Security fixes up to 64.0.3282.140 (2018-02-01)
Qt 5.11: Chromium 65
- 65.0.3325.151 (.1: .230) (2018-03-06)
+ 65.0.3325.151 (2018-03-06)
+ 5.11.0: Security fixes up to 66.0.3359.139 (2018-04-26)
+ 5.11.1: Updated to 65.0.3325.15.230
+ Security fixes up to 67.0.3396.87 (2018-06-12)
+ 5.11.2: Security fixes up to 68.0.3440.75 (~2018-07-31)
5.11.3: Security fixes up to 70.0.3538.102 (2018-11-09)
Qt 5.12: Chromium 69
- (LTS) 69.0.3497.113 (2018-09-27)
- 5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
+ (LTS) 69.0.3497.128 (~2018-09-11)
+ 5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24)
+ 5.12.1: Security fixes up to 71.0.3578.94 (2018-12-12)
+ 5.12.2: Security fixes up to 72.0.3626.121 (2019-03-01)
+ 5.12.3: Security fixes up to 73.0.3683.75 (2019-03-12)
+ 5.12.4: Security fixes up to 74.0.3729.157 (2019-05-14)
+ 5.12.5: Security fixes up to 76.0.3809.87 (2019-07-30)
+ 5.12.6: Security fixes up to 77.0.3865.120 (~2019-09-10)
+ 5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16)
+ 5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)
+ 5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
Qt 5.13: Chromium 73
73.0.3683.105 (~2019-02-28)
+ 5.13.0: Security fixes up to 74.0.3729.157 (2019-05-14)
+ 5.13.1: Security fixes up to 76.0.3809.87 (2019-07-30)
5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10)
Qt 5.14: Chromium 77
77.0.3865.129 (~2019-10-10)
+ 5.14.0: Security fixes up to 77.0.3865.129 (~2019-09-10)
+ 5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07)
5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03)
Qt 5.15: Chromium 80
80.0.3987.163 (2020-04-02)
5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05)
- Also see https://www.chromium.org/developers/calendar
- and https://chromereleases.googleblog.com/
+ Also see:
+
+ - https://chromiumdash.appspot.com/schedule
+ - https://www.chromium.org/developers/calendar
+ - https://chromereleases.googleblog.com/
"""
if webenginesettings is None:
return 'unavailable' # type: ignore[unreachable]
@@ -411,10 +440,11 @@ def _backend() -> str:
"""Get the backend line with relevant information."""
if objects.backend == usertypes.Backend.QtWebKit:
return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
- else:
+ elif objects.backend == usertypes.Backend.QtWebEngine:
webengine = usertypes.Backend.QtWebEngine
assert objects.backend == webengine, objects.backend
return 'QtWebEngine (Chromium {})'.format(_chromium_version())
+ raise utils.Unreachable(objects.backend)
def _uptime() -> datetime.timedelta:
diff --git a/requirements.txt b/requirements.txt
index d00782fb6..741b8903d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
adblock==0.3.0
-attrs==19.3.0
+attrs==20.1.0
colorama==0.4.3
cssutils==1.0.2
Jinja2==2.11.2
diff --git a/scripts/dev/build_pyqt_wheel.py b/scripts/dev/build_pyqt_wheel.py
index 9a36c8129..aa3fe9322 100644
--- a/scripts/dev/build_pyqt_wheel.py
+++ b/scripts/dev/build_pyqt_wheel.py
@@ -86,7 +86,9 @@ def main():
for wheel in input_files:
utils.print_subtitle(wheel.stem.split('-')[0])
subprocess.run([str(pyqt_bundle),
- '--qt-dir', args.qt_location, str(wheel)],
+ '--qt-dir', args.qt_location,
+ '--ignore-missing',
+ str(wheel)],
check=True)
wheel.unlink()
diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py
index 12963de38..313aa13e3 100644
--- a/scripts/dev/check_coverage.py
+++ b/scripts/dev/check_coverage.py
@@ -59,170 +59,170 @@ MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file')
# A list of (test_file, tested_file) tuples. test_file can be None.
PERFECT_FILES = [
(None,
- 'commands/cmdexc.py'),
+ 'qutebrowser/commands/cmdexc.py'),
('tests/unit/commands/test_argparser.py',
- 'commands/argparser.py'),
+ 'qutebrowser/commands/argparser.py'),
('tests/unit/api/test_cmdutils.py',
- 'api/cmdutils.py'),
+ 'qutebrowser/api/cmdutils.py'),
(None,
- 'api/apitypes.py'),
+ 'qutebrowser/api/apitypes.py'),
(None,
- 'api/config.py'),
+ 'qutebrowser/api/config.py'),
(None,
- 'api/message.py'),
+ 'qutebrowser/api/message.py'),
(None,
- 'api/qtutils.py'),
+ 'qutebrowser/api/qtutils.py'),
('tests/unit/browser/webkit/test_cache.py',
- 'browser/webkit/cache.py'),
+ 'qutebrowser/browser/webkit/cache.py'),
('tests/unit/browser/webkit/test_cookies.py',
- 'browser/webkit/cookies.py'),
+ 'qutebrowser/browser/webkit/cookies.py'),
('tests/unit/browser/test_history.py',
- 'browser/history.py'),
+ 'qutebrowser/browser/history.py'),
('tests/unit/browser/test_pdfjs.py',
- 'browser/pdfjs.py'),
+ 'qutebrowser/browser/pdfjs.py'),
('tests/unit/browser/webkit/http/test_http.py',
- 'browser/webkit/http.py'),
+ 'qutebrowser/browser/webkit/http.py'),
('tests/unit/browser/webkit/http/test_content_disposition.py',
- 'browser/webkit/rfc6266.py'),
+ 'qutebrowser/browser/webkit/rfc6266.py'),
# ('tests/unit/browser/webkit/test_webkitelem.py',
- # 'browser/webkit/webkitelem.py'),
+ # 'qutebrowser/browser/webkit/webkitelem.py'),
# ('tests/unit/browser/webkit/test_webkitelem.py',
- # 'browser/webelem.py'),
+ # 'qutebrowser/browser/webelem.py'),
('tests/unit/browser/webkit/network/test_filescheme.py',
- 'browser/webkit/network/filescheme.py'),
+ 'qutebrowser/browser/webkit/network/filescheme.py'),
('tests/unit/browser/webkit/network/test_networkreply.py',
- 'browser/webkit/network/networkreply.py'),
+ 'qutebrowser/browser/webkit/network/networkreply.py'),
('tests/unit/browser/test_signalfilter.py',
- 'browser/signalfilter.py'),
+ 'qutebrowser/browser/signalfilter.py'),
(None,
- 'browser/webengine/certificateerror.py'),
+ 'qutebrowser/browser/webengine/certificateerror.py'),
# ('tests/unit/browser/test_tab.py',
- # 'browser/tab.py'),
+ # 'qutebrowser/browser/tab.py'),
('tests/unit/keyinput/test_basekeyparser.py',
- 'keyinput/basekeyparser.py'),
+ 'qutebrowser/keyinput/basekeyparser.py'),
('tests/unit/keyinput/test_keyutils.py',
- 'keyinput/keyutils.py'),
+ 'qutebrowser/keyinput/keyutils.py'),
('tests/unit/components/test_readlinecommands.py',
- 'components/readlinecommands.py'),
+ 'qutebrowser/components/readlinecommands.py'),
('tests/unit/misc/test_autoupdate.py',
- 'misc/autoupdate.py'),
+ 'qutebrowser/misc/autoupdate.py'),
('tests/unit/misc/test_split.py',
- 'misc/split.py'),
+ 'qutebrowser/misc/split.py'),
('tests/unit/misc/test_msgbox.py',
- 'misc/msgbox.py'),
+ 'qutebrowser/misc/msgbox.py'),
('tests/unit/misc/test_checkpyver.py',
- 'misc/checkpyver.py'),
+ 'qutebrowser/misc/checkpyver.py'),
('tests/unit/misc/test_guiprocess.py',
- 'misc/guiprocess.py'),
+ 'qutebrowser/misc/guiprocess.py'),
('tests/unit/misc/test_editor.py',
- 'misc/editor.py'),
+ 'qutebrowser/misc/editor.py'),
('tests/unit/misc/test_cmdhistory.py',
- 'misc/cmdhistory.py'),
+ 'qutebrowser/misc/cmdhistory.py'),
('tests/unit/misc/test_ipc.py',
- 'misc/ipc.py'),
+ 'qutebrowser/misc/ipc.py'),
('tests/unit/misc/test_keyhints.py',
- 'misc/keyhintwidget.py'),
+ 'qutebrowser/misc/keyhintwidget.py'),
('tests/unit/misc/test_pastebin.py',
- 'misc/pastebin.py'),
+ 'qutebrowser/misc/pastebin.py'),
('tests/unit/misc/test_objects.py',
- 'misc/objects.py'),
+ 'qutebrowser/misc/objects.py'),
('tests/unit/misc/test_throttle.py',
- 'misc/throttle.py'),
+ 'qutebrowser/misc/throttle.py'),
(None,
- 'mainwindow/statusbar/keystring.py'),
+ 'qutebrowser/mainwindow/statusbar/keystring.py'),
('tests/unit/mainwindow/statusbar/test_percentage.py',
- 'mainwindow/statusbar/percentage.py'),
+ 'qutebrowser/mainwindow/statusbar/percentage.py'),
('tests/unit/mainwindow/statusbar/test_progress.py',
- 'mainwindow/statusbar/progress.py'),
+ 'qutebrowser/mainwindow/statusbar/progress.py'),
('tests/unit/mainwindow/statusbar/test_tabindex.py',
- 'mainwindow/statusbar/tabindex.py'),
+ 'qutebrowser/mainwindow/statusbar/tabindex.py'),
('tests/unit/mainwindow/statusbar/test_textbase.py',
- 'mainwindow/statusbar/textbase.py'),
+ 'qutebrowser/mainwindow/statusbar/textbase.py'),
('tests/unit/mainwindow/statusbar/test_url.py',
- 'mainwindow/statusbar/url.py'),
+ 'qutebrowser/mainwindow/statusbar/url.py'),
('tests/unit/mainwindow/statusbar/test_backforward.py',
- 'mainwindow/statusbar/backforward.py'),
+ 'qutebrowser/mainwindow/statusbar/backforward.py'),
('tests/unit/mainwindow/test_messageview.py',
- 'mainwindow/messageview.py'),
+ 'qutebrowser/mainwindow/messageview.py'),
('tests/unit/config/test_config.py',
- 'config/config.py'),
+ 'qutebrowser/config/config.py'),
('tests/unit/config/test_stylesheet.py',
- 'config/stylesheet.py'),
+ 'qutebrowser/config/stylesheet.py'),
('tests/unit/config/test_configdata.py',
- 'config/configdata.py'),
+ 'qutebrowser/config/configdata.py'),
('tests/unit/config/test_configexc.py',
- 'config/configexc.py'),
+ 'qutebrowser/config/configexc.py'),
('tests/unit/config/test_configfiles.py',
- 'config/configfiles.py'),
+ 'qutebrowser/config/configfiles.py'),
('tests/unit/config/test_configtypes.py',
- 'config/configtypes.py'),
+ 'qutebrowser/config/configtypes.py'),
('tests/unit/config/test_configinit.py',
- 'config/configinit.py'),
+ 'qutebrowser/config/configinit.py'),
('tests/unit/config/test_qtargs.py',
- 'config/qtargs.py'),
+ 'qutebrowser/config/qtargs.py'),
('tests/unit/config/test_configcommands.py',
- 'config/configcommands.py'),
+ 'qutebrowser/config/configcommands.py'),
('tests/unit/config/test_configutils.py',
- 'config/configutils.py'),
+ 'qutebrowser/config/configutils.py'),
('tests/unit/config/test_configcache.py',
- 'config/configcache.py'),
+ 'qutebrowser/config/configcache.py'),
('tests/unit/utils/test_qtutils.py',
- 'utils/qtutils.py'),
+ 'qutebrowser/utils/qtutils.py'),
('tests/unit/utils/test_standarddir.py',
- 'utils/standarddir.py'),
+ 'qutebrowser/utils/standarddir.py'),
('tests/unit/utils/test_urlutils.py',
- 'utils/urlutils.py'),
+ 'qutebrowser/utils/urlutils.py'),
('tests/unit/utils/usertypes',
- 'utils/usertypes.py'),
+ 'qutebrowser/utils/usertypes.py'),
('tests/unit/utils/test_utils.py',
- 'utils/utils.py'),
+ 'qutebrowser/utils/utils.py'),
('tests/unit/utils/test_version.py',
- 'utils/version.py'),
+ 'qutebrowser/utils/version.py'),
('tests/unit/utils/test_debug.py',
- 'utils/debug.py'),
+ 'qutebrowser/utils/debug.py'),
('tests/unit/utils/test_jinja.py',
- 'utils/jinja.py'),
+ 'qutebrowser/utils/jinja.py'),
('tests/unit/utils/test_error.py',
- 'utils/error.py'),
+ 'qutebrowser/utils/error.py'),
('tests/unit/utils/test_javascript.py',
- 'utils/javascript.py'),
+ 'qutebrowser/utils/javascript.py'),
('tests/unit/utils/test_urlmatch.py',
- 'utils/urlmatch.py'),
+ 'qutebrowser/utils/urlmatch.py'),
(None,
- 'completion/models/util.py'),
+ 'qutebrowser/completion/models/util.py'),
('tests/unit/completion/test_models.py',
- 'completion/models/urlmodel.py'),
+ 'qutebrowser/completion/models/urlmodel.py'),
('tests/unit/completion/test_models.py',
- 'completion/models/configmodel.py'),
+ 'qutebrowser/completion/models/configmodel.py'),
('tests/unit/completion/test_histcategory.py',
- 'completion/models/histcategory.py'),
+ 'qutebrowser/completion/models/histcategory.py'),
('tests/unit/completion/test_listcategory.py',
- 'completion/models/listcategory.py'),
+ 'qutebrowser/completion/models/listcategory.py'),
('tests/unit/browser/webengine/test_spell.py',
- 'browser/webengine/spell.py'),
+ 'qutebrowser/browser/webengine/spell.py'),
('tests/unit/browser/webengine/test_webengine_cookies.py',
- 'browser/webengine/cookies.py'),
+ 'qutebrowser/browser/webengine/cookies.py'),
]
# 100% coverage because of end2end tests, but no perfect unit tests yet.
WHITELISTED_FILES = [
- 'browser/webkit/webkitinspector.py',
- 'misc/debugcachestats.py',
- 'keyinput/macros.py',
- 'browser/webkit/webkitelem.py',
- 'api/interceptor.py',
+ 'qutebrowser/browser/webkit/webkitinspector.py',
+ 'qutebrowser/misc/debugcachestats.py',
+ 'qutebrowser/keyinput/macros.py',
+ 'qutebrowser/browser/webkit/webkitelem.py',
+ 'qutebrowser/api/interceptor.py',
]
@@ -243,8 +243,6 @@ def _get_filename(filename):
common_path = os.path.commonprefix([basedir, filename])
if common_path:
filename = filename[len(common_path):].lstrip('/')
- if filename.startswith('qutebrowser/'):
- filename = filename.split('/', maxsplit=1)[1]
return filename
@@ -295,8 +293,10 @@ def check(fileobj, perfect_files):
filename, line_cov, branch_cov)
messages.append(Message(MsgType.insufficient_coverage, filename,
text))
- elif (filename not in perfect_src_files and not is_bad and
- filename not in WHITELISTED_FILES):
+ elif (filename not in perfect_src_files and
+ not is_bad and
+ filename not in WHITELISTED_FILES and
+ not filename.startswith('tests/')):
text = ("{} has 100% coverage but is not in "
"perfect_files!".format(filename))
messages.append(Message(MsgType.perfect_file, filename, text))
@@ -320,7 +320,7 @@ def main_check():
for msg in messages:
msg.show()
print()
- filters = ','.join('qutebrowser/' + msg.filename for msg in messages)
+ filters = ','.join(msg.filename for msg in messages)
subprocess.run([sys.executable, '-m', 'coverage', 'report',
'--show-missing', '--include', filters], check=True)
print()
diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py
index 366abc9ca..6bb3eb1ca 100644
--- a/scripts/dev/misc_checks.py
+++ b/scripts/dev/misc_checks.py
@@ -162,7 +162,7 @@ def check_userscripts_descriptions():
described.add(match.group(1))
present = {path.name for path in folder.iterdir()}
- present.remove('README.md')
+ present -= {'README.md', '.mypy_cache', '__pycache__'}
missing = present - described
additional = described - present
diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py
index 7474c56c9..e36d7ee1d 100644
--- a/scripts/dev/recompile_requirements.py
+++ b/scripts/dev/recompile_requirements.py
@@ -42,7 +42,7 @@ CHANGELOG_URLS = {
'cherrypy': 'https://github.com/cherrypy/cherrypy/blob/master/CHANGES.rst',
'pylint': 'http://pylint.pycqa.org/en/latest/whatsnew/changelog.html',
'setuptools': 'https://github.com/pypa/setuptools/blob/master/CHANGES.rst',
- 'pytest-cov': 'https://github.com/pytest-dev/pytest-cov',
+ 'pytest-cov': 'https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst',
'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md',
'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst',
'werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst',
@@ -110,6 +110,7 @@ CHANGELOG_URLS = {
'chardet': 'https://github.com/chardet/chardet/releases',
'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst',
'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md',
+ 'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions',
}
# PyQt versions which need SIP v4
diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py
index 70df0ebe0..aefbff7f8 100755
--- a/scripts/dev/src2asciidoc.py
+++ b/scripts/dev/src2asciidoc.py
@@ -429,7 +429,7 @@ def _generate_setting_option(f, opt):
f.write("=== {}".format(opt.name) + "\n")
f.write(opt.description + "\n")
if opt.restart:
- f.write("This setting requires a restart.\n")
+ f.write("\nThis setting requires a restart.\n")
if opt.supports_pattern:
f.write("\nThis setting supports URL patterns.\n")
if opt.no_autoconfig:
diff --git a/setup.py b/setup.py
index 69a382e15..0c0bf73b4 100755
--- a/setup.py
+++ b/setup.py
@@ -98,6 +98,7 @@ try:
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',
diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py
index f87b84a56..43105d6cb 100644
--- a/tests/end2end/conftest.py
+++ b/tests/end2end/conftest.py
@@ -28,9 +28,10 @@ import sys
import shutil
import pstats
import operator
+import pathlib
import pytest
-from PyQt5.QtCore import PYQT_VERSION
+from PyQt5.QtCore import PYQT_VERSION, QCoreApplication
pytest.register_assert_rewrite('end2end.fixtures')
@@ -142,6 +143,9 @@ def pytest_collection_modifyitems(config, items):
header_bug_fixed = (not qtutils.version_check('5.12', compiled=False) or
qtutils.version_check('5.15', compiled=False))
+ lib_path = pathlib.Path(QCoreApplication.libraryPaths()[0])
+ qpdf_image_plugin = lib_path / 'imageformats' / 'libqpdf.so'
+
markers = [
('qtwebengine_todo', 'QtWebEngine TODO', pytest.mark.xfail,
config.webengine),
@@ -160,6 +164,10 @@ def pytest_collection_modifyitems(config, items):
('js_headers', 'Sets headers dynamically via JS',
pytest.mark.skipif,
config.webengine and not header_bug_fixed),
+ ('qtwebkit_pdf_imageformat_skip',
+ 'Skipped with QtWebKit if PDF image plugin is available',
+ pytest.mark.skipif,
+ not config.webengine and qpdf_image_plugin.exists()),
]
for item in items:
diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature
index 33a6cb5aa..93a15cd62 100644
--- a/tests/end2end/features/misc.feature
+++ b/tests/end2end/features/misc.feature
@@ -89,6 +89,10 @@ Feature: Various utility commands.
When I run :jseval Array(5002).join("x")
Then the message "x* [...trimmed...]" should be shown
+ Scenario: :jseval --url
+ When I run :jseval --url javascript:console.log("hello world?")
+ Then the javascript message "hello world?" should be logged
+
@qtwebengine_skip
Scenario: :jseval with --world on QtWebKit
When I run :jseval --world=1 console.log("Hello from JS!");
diff --git a/tests/end2end/features/navigate.feature b/tests/end2end/features/navigate.feature
index 2596f3ef1..ec295c4d1 100644
--- a/tests/end2end/features/navigate.feature
+++ b/tests/end2end/features/navigate.feature
@@ -4,25 +4,10 @@ Feature: Using :navigate
Scenario: :navigate with invalid argument
When I run :navigate foo
- Then the error "where: Invalid value foo - expected one of: prev, next, up, increment, decrement" should be shown
+ Then the error "where: Invalid value foo - expected one of: prev, next, up, increment, decrement, strip" should be shown
# up
- Scenario: Navigating up
- When I open data/navigate/sub
- And I run :navigate up
- Then data/navigate should be loaded
-
- Scenario: Navigating up with a query
- When I open data/navigate/sub?foo=bar
- And I run :navigate up
- Then data/navigate should be loaded
-
- Scenario: Navigating up by count
- When I open data/navigate/sub/index.html
- And I run :navigate up with count 2
- Then data/navigate should be loaded
-
Scenario: Navigating up in qute://help/
When the documentation is up to date
And I open qute://help/commands.html
@@ -90,48 +75,6 @@ Feature: Using :navigate
# increment/decrement
- Scenario: Incrementing number in URL
- When I open data/numbers/1.txt
- And I run :navigate increment
- Then data/numbers/2.txt should be loaded
-
- Scenario: Decrementing number in URL
- When I open data/numbers/4.txt
- And I run :navigate decrement
- Then data/numbers/3.txt should be loaded
-
- Scenario: Decrementing with no number in URL
- When I open data/navigate
- And I run :navigate decrement
- Then the error "No number found in URL!" should be shown
-
- Scenario: Incrementing with no number in URL
- When I open data/navigate
- And I run :navigate increment
- Then the error "No number found in URL!" should be shown
-
- Scenario: Incrementing number in URL by count
- When I open data/numbers/3.txt
- And I run :navigate increment with count 3
- Then data/numbers/6.txt should be loaded
-
- Scenario: Decrementing number in URL by count
- When I open data/numbers/8.txt
- And I run :navigate decrement with count 5
- Then data/numbers/3.txt should be loaded
-
- Scenario: Setting url.incdec_segments
- When I set url.incdec_segments to [anchor]
- And I open data/numbers/1.txt
- And I run :navigate increment
- Then the error "No number found in URL!" should be shown
-
- Scenario: Incrementing query
- When I set url.incdec_segments to ["query"]
- And I open data/numbers/1.txt?value=2
- And I run :navigate increment
- Then data/numbers/1.txt?value=3 should be loaded
-
@qtwebengine_todo: Doesn't find any elements
Scenario: Navigating multiline links
When I open data/navigate/multilinelinks.html
diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature
index 6ea9e7b33..07ff225a3 100644
--- a/tests/end2end/features/private.feature
+++ b/tests/end2end/features/private.feature
@@ -42,6 +42,25 @@ Feature: Using private browsing
## https://github.com/qutebrowser/qutebrowser/issues/1219
+ Scenario: Make sure private data is cleared when closing last private window
+ When I open about:blank in a private window
+ And I open cookies/set?cookie-to-delete=1 without waiting in a new tab
+ And I wait until cookies is loaded
+ And I run :close
+ And I open about:blank in a private window
+ And I open cookies
+ Then the cookie cookie-to-delete should not be set
+
+ Scenario: Make sure private data is not cleared when closing a private window but another remains
+ When I open about:blank in a private window
+ And I open about:blank in a private window
+ And I open cookies/set?cookie-to-preserve=1 without waiting in a new tab
+ And I wait until cookies is loaded
+ And I run :close
+ And I open about:blank in a private window
+ And I open cookies
+ Then the cookie cookie-to-preserve should be set to 1
+
Scenario: Sharing cookies with private browsing
When I open cookies/set?qute-test=42 without waiting in a private window
And I wait until cookies is loaded
diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature
index 2325912c5..55e366b4f 100644
--- a/tests/end2end/features/qutescheme.feature
+++ b/tests/end2end/features/qutescheme.feature
@@ -177,6 +177,7 @@ Feature: Special qute:// pages
And I open data/misc/test.pdf without waiting
Then the javascript message "PDF * [*] (PDF.js: *)" should be logged
+ @qtwebkit_pdf_imageformat_skip
Scenario: pdfjs is not used when disabled
When I set content.pdfjs to false
And I set downloads.location.prompt to false
diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature
index 85223aee2..3aa3f0df4 100644
--- a/tests/end2end/features/scroll.feature
+++ b/tests/end2end/features/scroll.feature
@@ -106,6 +106,7 @@ Feature: Scrolling
When I run :scroll bottom
Then the page should be scrolled vertically
+ @flaky
Scenario: Scrolling to bottom and to top
When I run :scroll bottom
And I wait until the scroll position changed
@@ -219,6 +220,7 @@ Feature: Scrolling
When I run :scroll-to-perc --horizontal
Then the page should be scrolled horizontally
+ @flaky
Scenario: :scroll-to-perc with count
When I run :scroll-to-perc with count 50
Then the page should be scrolled vertically
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
index 5fb095583..78fd0e48a 100644
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -114,6 +114,12 @@ def is_ignored_lowlevel_message(message):
'*/QtWebEngineProcess: /lib/x86_64-linux-gnu/libdbus-1.so.3: no '
'version information available (required by '
'*/libQt5WebEngineCore.so.5)',
+
+ # hunter and Python 3.9
+ # https://github.com/ionelmc/python-hunter/issues/87
+ '<frozen importlib._bootstrap>:*: RuntimeWarning: builtins.type size changed, '
+ 'may indicate binary incompatibility. Expected 872 from C header, got 880 from '
+ 'PyObject',
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)
diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py
index a4508441a..eb27b27a6 100644
--- a/tests/end2end/test_insert_mode.py
+++ b/tests/end2end/test_insert_mode.py
@@ -58,6 +58,7 @@ def test_insert_mode(file_name, elem_id, source, input_text, zoom,
(True, False, True), # enabled and foreground tab
(True, True, False), # background tab
])
+@pytest.mark.flaky
def test_auto_load(quteproc, auto_load, background, insert_mode):
quteproc.set_setting('input.insert_mode.auto_load', str(auto_load))
url_path = 'data/insert_mode_settings/html/autofocus.html'
diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py
index b62a488ce..015238d1b 100644
--- a/tests/helpers/fixtures.py
+++ b/tests/helpers/fixtures.py
@@ -215,6 +215,7 @@ def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager,
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
private=False)
+ tab.backend = usertypes.Backend.QtWebKit
widget_container.set_widget(tab)
yield tab
@@ -238,6 +239,7 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
private=False)
+ tab.backend = usertypes.Backend.QtWebEngine
widget_container.set_widget(tab)
yield tab
diff --git a/tests/unit/browser/webkit/test_downloads.py b/tests/unit/browser/test_downloads.py
index b3629b39d..4cd7b3727 100644
--- a/tests/unit/browser/webkit/test_downloads.py
+++ b/tests/unit/browser/test_downloads.py
@@ -22,10 +22,14 @@ import pytest
from qutebrowser.browser import downloads, qtnetworkdownloads
-def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache,
- fake_args):
+@pytest.fixture
+def manager(config_stub, cookiejar_and_cache):
+ """A QtNetwork download manager."""
+ return qtnetworkdownloads.DownloadManager()
+
+
+def test_download_model(qapp, qtmodeltester, manager):
"""Simple check for download model internals."""
- manager = qtnetworkdownloads.DownloadManager()
model = downloads.DownloadModel(manager)
qtmodeltester.check(model)
@@ -107,7 +111,7 @@ def test_sanitized_filenames(raw, expected,
config_stub, download_tmpdir, monkeypatch):
manager = downloads.AbstractDownloadManager()
target = downloads.FileDownloadTarget(str(download_tmpdir))
- item = downloads.AbstractDownloadItem()
+ item = downloads.AbstractDownloadItem(manager=manager)
# Don't try to start a timer outside of a QThread
manager._update_timer.isActive = lambda: True
@@ -116,6 +120,58 @@ def test_sanitized_filenames(raw, expected,
item._ensure_can_set_filename = lambda *args: True
item._after_set_filename = lambda *args: True
+ # Don't try to get current window
+ monkeypatch.setattr(item, '_get_conflicting_download', list)
+
manager._init_item(item, True, raw)
item.set_target(target)
assert item._filename.endswith(expected)
+
+
+class TestConflictingDownloads:
+
+ @pytest.fixture
+ def item1(self, manager):
+ return downloads.AbstractDownloadItem(manager=manager)
+
+ @pytest.fixture
+ def item2(self, manager):
+ return downloads.AbstractDownloadItem(manager=manager)
+
+ def test_no_downloads(self, item1):
+ item1._filename = 'download.txt'
+ assert item1._get_conflicting_download() is None
+
+ @pytest.mark.parametrize('filename1, filename2, done, conflict', [
+ # Different name
+ ('download.txt', 'download2.txt', False, False),
+ # Finished
+ ('download.txt', 'download.txt', True, False),
+ # Conflict
+ ('download.txt', 'download.txt', False, True),
+ ])
+ def test_conflicts(self, manager, item1, item2,
+ filename1, filename2, done, conflict):
+ item1._filename = filename1
+ item2._filename = filename2
+ item2.done = done
+ manager.downloads.append(item1)
+ manager.downloads.append(item2)
+ expected = item2 if conflict else None
+ assert item1._get_conflicting_download() is expected
+
+ def test_cancel_conflicting_downloads(self, manager, item1, item2, monkeypatch):
+ item1._filename = 'download.txt'
+ item2._filename = 'download.txt'
+ item2.done = False
+ manager.downloads.append(item1)
+ manager.downloads.append(item2)
+
+ def patched_cancel(remove_data=True):
+ assert not remove_data
+ item2.done = True
+
+ monkeypatch.setattr(item2, 'cancel', patched_cancel)
+ monkeypatch.setattr(item1, '_after_set_filename', lambda: None)
+ item1._cancel_conflicting_download()
+ assert item2.done
diff --git a/tests/unit/browser/test_navigate.py b/tests/unit/browser/test_navigate.py
index efabc3040..5fe0acbf6 100644
--- a/tests/unit/browser/test_navigate.py
+++ b/tests/unit/browser/test_navigate.py
@@ -172,10 +172,55 @@ class TestIncDec:
def test_invalid_url(self):
with pytest.raises(urlutils.InvalidUrlError):
- navigate.incdec(QUrl(""), 1, "increment")
+ navigate.incdec(QUrl(), 1, "increment")
def test_wrong_mode(self):
"""Test if incdec rejects a wrong parameter for inc_or_dec."""
valid_url = QUrl("http://example.com/0")
with pytest.raises(ValueError):
navigate.incdec(valid_url, 1, "foobar")
+
+
+class TestUp:
+
+ @pytest.mark.parametrize('url_suffix, count, expected_suffix', [
+ ('/one/two/three', 1, '/one/two'),
+ ('/one/two/three?foo=bar', 1, '/one/two'),
+ ('/one/two/three', 2, '/one'),
+ ])
+ def test_up(self, url_suffix, count, expected_suffix):
+ url_base = 'https://example.com'
+ url = QUrl(url_base + url_suffix)
+ assert url.isValid()
+
+ new = navigate.path_up(url, count)
+ assert new == QUrl(url_base + expected_suffix)
+
+ def test_invalid_url(self):
+ with pytest.raises(urlutils.InvalidUrlError):
+ navigate.path_up(QUrl(), count=1)
+
+
+class TestStrip:
+
+ @pytest.mark.parametrize('url_suffix', [
+ '?foo=bar',
+ '#label',
+ '?foo=bar#label',
+ ])
+ def test_strip(self, url_suffix):
+ url_base = 'https://example.com/test'
+ url = QUrl(url_base + url_suffix)
+ assert url.isValid()
+
+ stripped = navigate.strip(url, count=1)
+ assert stripped.isValid()
+ assert stripped == QUrl(url_base)
+
+ def test_count(self):
+ with pytest.raises(navigate.Error, match='Count is not supported'):
+ navigate.strip(QUrl('https://example.com/'), count=2)
+
+ def test_invalid_url(self):
+ with pytest.raises(urlutils.InvalidUrlError):
+ navigate.strip(QUrl(), count=1)
diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py
index f33642ae3..1ad10cc3c 100644
--- a/tests/unit/browser/webkit/network/test_pac.py
+++ b/tests/unit/browser/webkit/network/test_pac.py
@@ -87,7 +87,7 @@ def _pac_noexcept_test(call):
_pac_common_test(test_str_f.format(call))
-# pylint: disable=line-too-long, invalid-name
+# pylint: disable=invalid-name
@pytest.mark.parametrize("domain, expected", [
diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py
index cd8c088eb..37262a7b3 100644
--- a/tests/unit/browser/webkit/test_webkitelem.py
+++ b/tests/unit/browser/webkit/test_webkitelem.py
@@ -743,8 +743,8 @@ class TestGetChildFrames:
def test_one_level(self, stubs):
r"""Test get_child_frames with one level of children.
- o parent
- / \
+ o parent
+ / \ ------
child1 o o child2
"""
child1 = stubs.FakeChildrenFrame()
@@ -763,9 +763,9 @@ class TestGetChildFrames:
r"""Test get_child_frames with multiple levels of children.
o root
- / \
+ / \ ------
o o first
- /\ /\
+ /\ /\ ------
o o o o second
"""
second = [stubs.FakeChildrenFrame() for _ in range(4)]
diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py
index ac07e80dc..739b8b773 100644
--- a/tests/unit/completion/test_completiondelegate.py
+++ b/tests/unit/completion/test_completiondelegate.py
@@ -34,7 +34,7 @@ from qutebrowser.completion import completiondelegate
('foo', 'barfoobaz', [(3, 3)]),
('foo', 'barfoobazfoo', [(3, 3), (9, 3)]),
('foo', 'foofoo', [(0, 3), (3, 3)]),
- ('a|b', 'cadb', [(1, 1), (3, 1)]),
+ ('a b', 'cadb', [(1, 1), (3, 1)]),
('foo', '<foo>', [(1, 3)]),
('<a>', "<a>bc", [(0, 3)]),
@@ -42,6 +42,10 @@ from qutebrowser.completion import completiondelegate
('foo', "'foo'", [(1, 3)]),
('x', "'x'", [(1, 1)]),
('lt', "<lt", [(1, 2)]),
+
+ # See https://github.com/qutebrowser/qutebrowser/pull/5111
+ ('bar', '\U0001d65b\U0001d664\U0001d664bar', [(6, 3)]),
+ ('an anomaly', 'an anomaly', [(0, 2), (3, 7)]),
])
def test_highlight(pat, txt, segments):
doc = QTextDocument(txt)
@@ -53,6 +57,18 @@ def test_highlight(pat, txt, segments):
])
+def test_benchmark_highlight(benchmark):
+ txt = 'boofoobar'
+ pat = 'foo bar'
+ doc = QTextDocument(txt)
+
+ def bench():
+ highlighter = completiondelegate._Highlighter(doc, pat, Qt.red)
+ highlighter.highlightBlock(txt)
+
+ benchmark(bench)
+
+
def test_highlighted(qtbot):
"""Make sure highlighting works.
diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py
index bdeda54b7..356b854c5 100644
--- a/tests/unit/completion/test_completionwidget.py
+++ b/tests/unit/completion/test_completionwidget.py
@@ -22,6 +22,7 @@
from unittest import mock
import pytest
+from PyQt5.QtCore import QRect
from qutebrowser.completion import completionwidget
from qutebrowser.completion.models import completionmodel, listcategory
@@ -42,9 +43,13 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
return view
-def test_set_model(completionview):
+@pytest.fixture()
+def model():
+ return completionmodel.CompletionModel()
+
+
+def test_set_model(completionview, model):
"""Ensure set_model actually sets the model and expands all categories."""
- model = completionmodel.CompletionModel()
for _i in range(3):
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
@@ -53,8 +58,7 @@ def test_set_model(completionview):
assert completionview.isExpanded(model.index(i, 0))
-def test_set_pattern(completionview):
- model = completionmodel.CompletionModel()
+def test_set_pattern(completionview, model):
model.set_pattern = mock.Mock(spec=[])
completionview.set_model(model)
completionview.set_pattern('foo')
@@ -116,7 +120,7 @@ def test_maybe_update_geometry(completionview, config_stub, qtbot):
('next-category', [[]], [None, None]),
('prev-category', [[]], [None, None]),
])
-def test_completion_item_focus(which, tree, expected, completionview, qtbot):
+def test_completion_item_focus(which, tree, expected, completionview, model, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
@@ -127,7 +131,6 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
successive movement. None implies no signal should be
emitted.
"""
- model = completionmodel.CompletionModel()
for catdata in tree:
cat = listcategory.ListCategory('', ((x,) for x in catdata))
model.add_category(cat)
@@ -142,23 +145,23 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
assert sig.args == [entry]
-@pytest.mark.parametrize('which', ['next', 'prev', 'next-category',
- 'prev-category'])
-def test_completion_item_focus_no_model(which, completionview, qtbot):
+@pytest.mark.parametrize('which', ['next', 'prev',
+ 'next-category', 'prev-category',
+ 'next-page', 'prev-page'])
+def test_completion_item_focus_no_model(which, completionview, model, qtbot):
"""Test that selectionChanged is not fired when the model is None.
Validates #1812: help completion repeatedly completes
"""
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
- model = completionmodel.CompletionModel()
completionview.set_model(model)
completionview.set_model(None)
with qtbot.assertNotEmitted(completionview.selection_changed):
completionview.completion_item_focus(which)
-def test_completion_item_focus_fetch(completionview, qtbot):
+def test_completion_item_focus_fetch(completionview, model, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
@@ -169,7 +172,6 @@ def test_completion_item_focus_fetch(completionview, qtbot):
successive movement. None implies no signal should be
emitted.
"""
- model = completionmodel.CompletionModel()
cat = mock.Mock(spec=[
'layoutChanged', 'layoutAboutToBeChanged', 'canFetchMore',
'fetchMore', 'rowCount', 'index', 'data'])
@@ -190,10 +192,95 @@ def test_completion_item_focus_fetch(completionview, qtbot):
assert cat.fetchMore.called
+class TestCompletionItemFocusPage:
+
+ """Test :completion-item-focus with prev-page/next-page."""
+
+ @pytest.fixture(autouse=True)
+ def patch_heights(self, monkeypatch, completionview):
+ """Patch the item/widget heights so that 10 items are always visible."""
+ monkeypatch.setattr(completionview, 'visualRect',
+ lambda _idx: QRect(0, 0, 100, 20))
+ monkeypatch.setattr(completionview, 'height', lambda: 200)
+
+ @pytest.mark.parametrize('which, expected', [
+ ('prev-page', 'Last Item'),
+ ('next-page', 'First Item'),
+ ])
+ def test_no_selection(self, qtbot, completionview, model, which, expected):
+ """With no selection, the first/last item should be selected."""
+ items = [("First Item",), ("Middle Item",), ("Last Item",)]
+ cat = listcategory.ListCategory('Test', items)
+ model.add_category(cat)
+ completionview.set_model(model)
+ with qtbot.waitSignal(completionview.selection_changed) as blocker:
+ completionview.completion_item_focus(which)
+ assert blocker.args == [expected]
+
+ @pytest.mark.parametrize('steps', [
+ # Select first item and go down
+ [('next', 'Item 1'), ('next-page', 'Item 10')],
+ # Go down twice
+ [('next', 'Item 1'), ('next-page', 'Item 10'), ('next-page', 'Item 19')],
+ # Last item via Page Down
+ [('next', 'Item 1'),
+ ('next-page', 'Item 10'),
+ ('next-page', 'Item 19'),
+ ('next-page', 'Item 24')],
+ # Wrapping around via Page Down
+ [('next', 'Item 1'),
+ ('next-page', 'Item 10'),
+ ('next-page', 'Item 19'),
+ ('next-page', 'Item 24'),
+ ('next-page', 'Item 1')],
+
+ # Select last item and go up
+ [('prev', 'Item 24'), ('prev-page', 'Item 15')],
+ # Go up twice
+ [('prev', 'Item 24'), ('prev-page', 'Item 15'), ('prev-page', 'Item 6')],
+ # Last item via Page Up
+ [('prev', 'Item 24'),
+ ('prev-page', 'Item 15'),
+ ('prev-page', 'Item 6'),
+ ('prev-page', 'Item 1')],
+ # Wrapping around via Page Up
+ [('prev', 'Item 24'),
+ ('prev-page', 'Item 15'),
+ ('prev-page', 'Item 6'),
+ ('prev-page', 'Item 1'),
+ ('prev-page', 'Item 24')],
+ ])
+ def test_steps(self, completionview, qtbot, model, steps):
+ items = [("Item {}".format(i),) for i in range(1, 25)]
+ cat = listcategory.ListCategory('Test', items)
+ model.add_category(cat)
+ completionview.set_model(model)
+
+ for move, item in steps:
+ print('{:9} -> expecting {}'.format(move, item))
+ with qtbot.waitSignal(completionview.selection_changed) as blocker:
+ completionview.completion_item_focus(move)
+ assert blocker.args == [item]
+
+ def test_category_headers(self, completionview, qtbot, model):
+ for name, items in [
+ ("First", [("Item {}".format(i),) for i in range(1, 9)]),
+ ("Second", []),
+ ("Third", [("Target item",)])]:
+ cat = listcategory.ListCategory(name, items)
+ model.add_category(cat)
+ completionview.set_model(model)
+
+ for move, item in [('next', 'Item 1'), ('next-page', 'Target item')]:
+ with qtbot.waitSignal(completionview.selection_changed) as blocker:
+ completionview.completion_item_focus(move)
+ assert blocker.args == [item]
+
+
@pytest.mark.parametrize('show', ['always', 'auto', 'never'])
@pytest.mark.parametrize('rows', [[], ['Aa'], ['Aa', 'Bb']])
@pytest.mark.parametrize('quick_complete', [True, False])
-def test_completion_show(show, rows, quick_complete, completionview,
+def test_completion_show(show, rows, quick_complete, completionview, model,
config_stub):
"""Test that the completion widget is shown at appropriate times.
@@ -205,7 +292,6 @@ def test_completion_show(show, rows, quick_complete, completionview,
config_stub.val.completion.show = show
config_stub.val.completion.quick = quick_complete
- model = completionmodel.CompletionModel()
for name in rows:
cat = listcategory.ListCategory('', [(name,)])
model.add_category(cat)
@@ -222,10 +308,9 @@ def test_completion_show(show, rows, quick_complete, completionview,
assert not completionview.isVisible()
-def test_completion_item_del(completionview):
+def test_completion_item_del(completionview, model):
"""Test that completion_item_del invokes delete_cur_item in the model."""
func = mock.Mock(spec=[])
- model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')], delete_func=func)
model.add_category(cat)
completionview.set_model(model)
@@ -234,10 +319,9 @@ def test_completion_item_del(completionview):
func.assert_called_once_with(['foo', 'bar'])
-def test_completion_item_del_no_selection(completionview):
+def test_completion_item_del_no_selection(completionview, model):
"""Test that completion_item_del with an invalid index."""
func = mock.Mock(spec=[])
- model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo',)], delete_func=func)
model.add_category(cat)
completionview.set_model(model)
@@ -247,12 +331,11 @@ def test_completion_item_del_no_selection(completionview):
@pytest.mark.parametrize('sel', [True, False])
-def test_completion_item_yank(completionview, mocker, sel):
+def test_completion_item_yank(completionview, model, mocker, sel):
"""Test that completion_item_yank invokes delete_cur_item in the model."""
m = mocker.patch(
'qutebrowser.completion.completionwidget.utils',
autospec=True)
- model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')])
model.add_category(cat)
@@ -264,13 +347,12 @@ def test_completion_item_yank(completionview, mocker, sel):
@pytest.mark.parametrize('sel', [True, False])
-def test_completion_item_yank_selected(completionview, status_command_stub,
- mocker, sel):
+def test_completion_item_yank_selected(completionview, model,
+ status_command_stub, mocker, sel):
"""Test that completion_item_yank yanks selected text."""
m = mocker.patch(
'qutebrowser.completion.completionwidget.utils',
autospec=True)
- model = completionmodel.CompletionModel()
cat = listcategory.ListCategory('', [('foo', 'bar')])
model.add_category(cat)
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
index 97d8707f4..a98584164 100644
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -63,28 +63,6 @@ class Font(QFont):
return utils.get_repr(self, **kwargs)
- @classmethod
- def fromdesc(cls, desc):
- """Get a Font based on a font description."""
- f = cls()
-
- f.setStyle(desc.style)
- f.setWeight(desc.weight)
-
- if desc.pt is not None and desc.pt != -1:
- f.setPointSize(desc.pt)
- if desc.px is not None and desc.pt != -1:
- f.setPixelSize(desc.px)
-
- f.setFamily(desc.family)
- try:
- f.setFamilies([desc.family])
- except AttributeError:
- # Added in Qt 5.13
- pass
-
- return f
-
class RegexEq:
@@ -1434,10 +1412,6 @@ class TestFont:
def klass(self):
return configtypes.Font
- @pytest.fixture
- def font_class(self):
- return configtypes.Font
-
@pytest.mark.parametrize('val, desc', sorted(TESTS.items()))
def test_to_py_valid(self, klass, val, desc):
assert klass().to_py(val) == val
@@ -1743,10 +1717,6 @@ class TestFile:
def klass(self, request):
return request.param
- @pytest.fixture
- def file_class(self):
- return configtypes.File
-
def test_to_py_does_not_exist_file(self, os_mock):
"""Test to_py with a file which does not exist (File)."""
os_mock.path.isfile.return_value = False
diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py
index 77cc072b6..050788a9e 100644
--- a/tests/unit/mainwindow/test_messageview.py
+++ b/tests/unit/mainwindow/test_messageview.py
@@ -59,6 +59,26 @@ def test_size_hint(view):
assert height2 == height1 * 2
+def test_word_wrap(view, qtbot):
+ """A long message should be wrapped."""
+ with qtbot.waitSignal(view._clear_timer.timeout):
+ view.show_message(usertypes.MessageLevel.info, 'short')
+ height1 = view.sizeHint().height()
+ assert height1 > 0
+
+ text = ("Athene, the bright-eyed goddess, answered him at once: Father of "
+ "us all, Son of Cronos, Highest King, clearly that man deserved to be "
+ "destroyed: so let all be destroyed who act as he did. But my heart aches "
+ "for Odysseus, wise but ill fated, who suffers far from his friends on an "
+ "island deep in the sea.")
+
+ view.show_message(usertypes.MessageLevel.info, text)
+ height2 = view.sizeHint().height()
+
+ assert height2 > height1
+ assert view._messages[0].wordWrap()
+
+
def test_show_message_twice(view):
"""Show the same message twice -> only one should be shown."""
view.show_message(usertypes.MessageLevel.info, 'test')
diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py
index 7568e56c0..cd9fe93b8 100644
--- a/tests/unit/misc/test_miscwidgets.py
+++ b/tests/unit/misc/test_miscwidgets.py
@@ -18,10 +18,9 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import logging
-from unittest import mock
from PyQt5.QtCore import Qt, QSize
-from PyQt5.QtWidgets import QApplication, QWidget
+from PyQt5.QtWidgets import QWidget
import pytest
from qutebrowser.misc import miscwidgets
@@ -41,19 +40,6 @@ class TestCommandLineEdit:
assert cmd_edit.text() == ''
yield cmd_edit
- @pytest.fixture
- def mock_clipboard(self, mocker):
- """Fixture to mock QApplication.clipboard.
-
- Return:
- The mocked QClipboard object.
- """
- mocker.patch.object(QApplication, 'clipboard')
- clipboard = mock.MagicMock()
- clipboard.supportsSelection.return_value = True
- QApplication.clipboard.return_value = clipboard
- return clipboard
-
def test_position(self, qtbot, cmd_edit):
"""Test cursor position based on the prompt."""
qtbot.keyClicks(cmd_edit, ':hello')
diff --git a/tests/unit/misc/userscripts/test_qute_lastpass.py b/tests/unit/misc/userscripts/test_qute_lastpass.py
index 84672e6dc..20646edd0 100644
--- a/tests/unit/misc/userscripts/test_qute_lastpass.py
+++ b/tests/unit/misc/userscripts/test_qute_lastpass.py
@@ -84,7 +84,6 @@ class TestQuteLastPassComponents:
"""Test if fake_key_raw properly escapes characters."""
qute_lastpass.fake_key_raw('john.doe@example.com ')
- # pylint: disable=line-too-long
qutecommand_mock.assert_called_once_with(
'fake-key \\j\\o\\h\\n\\.\\d\\o\\e\\@\\e\\x\\a\\m\\p\\l\\e\\.\\c\\o\\m" "'
)
@@ -258,7 +257,6 @@ class TestQuteLastPassMain:
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
- # pylint: disable=line-too-long
subprocess_mock.assert_has_calls([
call(['lpass', 'show', '-x', '-j', '-G', '\\bwww\\.example\\.com'],
stdout=ANY, stderr=ANY),
@@ -325,7 +323,6 @@ class TestQuteLastPassMain:
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
- # pylint: disable=line-too-long
subprocess_mock.assert_has_calls([
call(['lpass', 'show', '-x', '-j', '-G', '\\bwww\\.example\\.com'],
stdout=ANY, stderr=ANY),
diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py
index 9aa3e172e..ec9d9666b 100644
--- a/tests/unit/scripts/test_check_coverage.py
+++ b/tests/unit/scripts/test_check_coverage.py
@@ -227,11 +227,11 @@ def test_skipped_non_linux(covtest):
def _generate_files():
"""Get filenames from WHITELISTED_/PERFECT_FILES."""
for src_file in check_coverage.WHITELISTED_FILES:
- yield pathlib.Path('qutebrowser') / src_file
+ yield pathlib.Path(src_file)
for test_file, src_file in check_coverage.PERFECT_FILES:
if test_file is not None:
yield pathlib.Path(test_file)
- yield pathlib.Path('qutebrowser') / src_file
+ yield pathlib.Path(src_file)
@pytest.mark.parametrize('filename', list(_generate_files()))
diff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py
index 005b8f86c..fc8267435 100644
--- a/tests/unit/utils/test_javascript.py
+++ b/tests/unit/utils/test_javascript.py
@@ -22,57 +22,76 @@
import pytest
import hypothesis
import hypothesis.strategies
+import attr
-from qutebrowser.utils import javascript
+from qutebrowser.utils import javascript, usertypes
+
+
+@attr.s
+class Case:
+
+ original = attr.ib()
+ replacement = attr.ib()
+ webkit_only = attr.ib(False)
+
+ def __str__(self):
+ return self.original
class TestStringEscape:
- TESTS = {
- 'foo\\bar': r'foo\\bar',
- 'foo\nbar': r'foo\nbar',
- 'foo\rbar': r'foo\rbar',
- "foo'bar": r"foo\'bar",
- 'foo"bar': r'foo\"bar',
- 'one\\two\rthree\nfour\'five"six': r'one\\two\rthree\nfour\'five\"six',
- '\x00': r'\x00',
- 'hellö': 'hellö',
- '☃': '☃',
- '\x80Ā': '\x80Ā',
- '𐀀\x00𐀀\x00': r'𐀀\x00𐀀\x00',
- '𐀀\ufeff': r'𐀀\ufeff',
- '\ufeff': r'\ufeff',
+ TESTS = [
+ Case('foo\\bar', r'foo\\bar'),
+ Case('foo\nbar', r'foo\nbar'),
+ Case('foo\rbar', r'foo\rbar'),
+ Case("foo'bar", r"foo\'bar"),
+ Case('foo"bar', r'foo\"bar'),
+ Case('one\\two\rthree\nfour\'five"six', r'one\\two\rthree\nfour\'five\"six'),
+ Case('\x00', r'\x00', webkit_only=True),
+ Case('hellö', 'hellö'),
+ Case('☃', '☃'),
+ Case('\x80Ā', '\x80Ā'),
+ Case('𐀀\x00𐀀\x00', r'𐀀\x00𐀀\x00', webkit_only=True),
+ Case('𐀀\ufeff', r'𐀀\ufeff'),
+ Case('\ufeff', r'\ufeff', webkit_only=True),
# http://stackoverflow.com/questions/2965293/
- '\u2028': r'\u2028',
- '\u2029': r'\u2029',
- }
+ Case('\u2028', r'\u2028'),
+ Case('\u2029', r'\u2029'),
+ ]
# Once there was this warning here:
# load glyph failed err=6 face=0x2680ba0, glyph=1912
# http://qutebrowser.org:8010/builders/debian-jessie/builds/765/steps/unittests/
# Should that be ignored?
- @pytest.mark.parametrize('before, after', sorted(TESTS.items()), ids=repr)
- def test_fake_escape(self, before, after):
+ @pytest.mark.parametrize('case', TESTS, ids=str)
+ def test_fake_escape(self, case):
"""Test javascript escaping with some expected outcomes."""
- assert javascript.string_escape(before) == after
+ assert javascript.string_escape(case.original) == case.replacement
- def _test_escape(self, text, webframe):
- """Test conversion by using evaluateJavaScript."""
+ def _test_escape(self, text, web_tab, qtbot):
+ """Test conversion by running JS in a tab."""
escaped = javascript.string_escape(text)
- result = webframe.evaluateJavaScript('"{}";'.format(escaped))
- assert result == text
- @pytest.mark.parametrize('text', sorted(TESTS), ids=repr)
- def test_real_escape(self, webframe, text):
+ with qtbot.waitCallback() as cb:
+ web_tab.run_js_async('"{}";'.format(escaped), cb)
+
+ cb.assert_called_with(text)
+
+ @pytest.mark.parametrize('case', TESTS, ids=str)
+ def test_real_escape(self, web_tab, qtbot, case):
"""Test javascript escaping with a real QWebPage."""
- self._test_escape(text, webframe)
+ if web_tab.backend == usertypes.Backend.QtWebEngine and case.webkit_only:
+ pytest.xfail("Not supported with QtWebEngine")
+ self._test_escape(case.original, web_tab, qtbot)
@pytest.mark.qt_log_ignore('^OpenType support missing for script')
@hypothesis.given(hypothesis.strategies.text())
- def test_real_escape_hypothesis(self, webframe, text):
+ def test_real_escape_hypothesis(self, web_tab, qtbot, text):
"""Test javascript escaping with a real QWebPage and hypothesis."""
- self._test_escape(text, webframe)
+ if web_tab.backend == usertypes.Backend.QtWebEngine:
+ hypothesis.assume('\x00' not in text)
+ self._test_escape(text, web_tab, qtbot)
@pytest.mark.parametrize('arg, expected', [
diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py
index 72fe631ca..04f7f04e6 100644
--- a/tests/unit/utils/test_urlutils.py
+++ b/tests/unit/utils/test_urlutils.py
@@ -21,11 +21,14 @@
import os.path
import logging
+import urllib.parse
import attr
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkProxy
import pytest
+import hypothesis
+import hypothesis.strategies
from qutebrowser.api import cmdutils
from qutebrowser.browser.network import pac
@@ -760,3 +763,42 @@ class TestProxyFromUrl:
def test_invalid(self, url, exception):
with pytest.raises(exception):
urlutils.proxy_from_url(QUrl(url))
+
+
+class TestParseJavascriptUrl:
+
+ @pytest.mark.parametrize('url, message', [
+ (QUrl(), ""),
+ (QUrl('https://example.com'), "Expected a javascript:... URL"),
+ (QUrl('javascript://example.com'),
+ "URL contains unexpected components: example.com"),
+ (QUrl('javascript://foo:bar@example.com:1234'),
+ "URL contains unexpected components: foo:bar@example.com:1234"),
+ ])
+ def test_invalid(self, url, message):
+ with pytest.raises(urlutils.Error, match=message):
+ urlutils.parse_javascript_url(url)
+
+ @pytest.mark.parametrize('url, source', [
+ (QUrl('javascript:"hello" %0a "world"'), '"hello" \n "world"'),
+ # https://github.com/web-platform-tests/wpt/blob/master/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-query-fragment-components.html
+ (QUrl('javascript:"nope" ? "yep" : "what";'), '"nope" ? "yep" : "what";'),
+ (QUrl('javascript:"wrong"; // # %0a "ok";'), '"wrong"; // # \n "ok";'),
+ (QUrl('javascript:"%252525 ? %252525 # %252525"'),
+ '"%2525 ? %2525 # %2525"'),
+ ])
+ def test_valid(self, url, source):
+ assert urlutils.parse_javascript_url(url) == source
+
+ @hypothesis.given(source=hypothesis.strategies.text())
+ def test_hypothesis(self, source):
+ scheme = 'javascript:'
+ url = QUrl(scheme + urllib.parse.quote(source))
+ hypothesis.assume(url.isValid())
+
+ try:
+ parsed = urlutils.parse_javascript_url(url)
+ except urlutils.Error:
+ pass
+ else:
+ assert parsed == source
diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py
index 4b7d4f5fa..868c4920f 100644
--- a/tests/unit/utils/test_version.py
+++ b/tests/unit/utils/test_version.py
@@ -809,8 +809,9 @@ class TestPDFJSVersion:
assert version._pdfjs_version() == 'unknown (bundled)'
@pytest.mark.parametrize('varname', [
- 'PDFJS.version', # older versions
- 'var pdfjsVersion', # newer versions
+ 'PDFJS.version', # v1.10.100 and older
+ 'var pdfjsVersion', # v2.0.943
+ 'const pdfjsVersion', # v2.5.207
])
def test_known(self, monkeypatch, varname):
pdfjs_code = textwrap.dedent("""
diff --git a/tox.ini b/tox.ini
index 309584355..369d4afe6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -22,6 +22,7 @@ basepython =
py36: {env:PYTHON:python3.6}
py37: {env:PYTHON:python3.7}
py38: {env:PYTHON:python3.8}
+ py39: {env:PYTHON:python3.9}
pip_version = pip
deps =
-r{toxinidir}/requirements.txt