diff --git a/.appveyor.yml b/.appveyor.yml index f2424fc94..92a20c0bd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,15 +5,15 @@ cache: build: off environment: PYTHONUNBUFFERED: 1 - PYTHON: C:\Python36\python.exe + PYTHON: C:\Python36-x64\python.exe matrix: - - TESTENV: py36-pyqt510 + - TESTENV: py36-pyqt511 - TESTENV: pylint install: - '%PYTHON% -m pip install -U pip' - '%PYTHON% -m pip install -r misc\requirements\requirements-tox.txt' - - 'set PATH=%PATH%;C:\Python36' + - 'set PATH=C:\Python36-x64;%PATH' test_script: - '%PYTHON% -m tox -e %TESTENV%' diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..9556c6763 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +doc/changelog.asciidoc merge=union diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d7da20300..1bc570a7e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ qutebrowser/browser/history.py @rcorre -qutebrowser/completion/* @rcorre +qutebrowser/completion/** @rcorre qutebrowser/misc/sql.py @rcorre tests/end2end/features/completion.feature @rcorre tests/end2end/features/test_completion_bdd.py @rcorre diff --git a/.github/CONTRIBUTING.asciidoc b/.github/CONTRIBUTING.asciidoc index 6449c6323..d0fa243e9 100644 --- a/.github/CONTRIBUTING.asciidoc +++ b/.github/CONTRIBUTING.asciidoc @@ -1,3 +1,11 @@ +IMPORTANT: *Currently, bigger changes are going on in qutebrowser, as +part of a +https://lists.schokokeks.org/pipermail/qutebrowser-announce/2018-September/000051.html[student research project] +about adding a plugin API to qutebrowser and moving a lot of code from the code +into plugins.* Due to that, bandwidth for pull request review is currently +very limited, and contributions might lead to merge conflicts due to +ongoing refactorings. + - Before you start to work on something, please leave a comment on the relevant issue (or open one). This makes sure there is no duplicate work done. @@ -7,6 +15,5 @@ - If you are stuck somewhere or have questions, https://github.com/qutebrowser/qutebrowser#getting-help[please ask]! -See the full contribution documentation for details and other useful hints: - -include::../doc/contributing.asciidoc[] +See the link:../doc/contributing.asciidoc[full contribution documentation] for +details and other useful hints. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b9bf8d399..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md new file mode 100644 index 000000000..56566849d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -0,0 +1,15 @@ +--- +name: 🐛 Bug Report +about: Report errors and problems + +--- + +**Version info (see `:version`)**: + +**Does the bug happen if you start with `--temp-basedir`?** (if applicable): + +**Description** + +**How to reproduce** + diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md new file mode 100644 index 000000000..54912b052 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -0,0 +1,5 @@ +--- +name: 🚀 Feature Request +about: Ideas for new features and improvements + +--- diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md new file mode 100644 index 000000000..76d0cffca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_Support_question.md @@ -0,0 +1,12 @@ +--- +name: ❓ Support Question +about: It's okay to ask questions via GitHub, but IRC/Reddit/Mailinglist might be better. + +--- + + diff --git a/.github/ISSUE_TEMPLATE/4_Security_issue.md b/.github/ISSUE_TEMPLATE/4_Security_issue.md new file mode 100644 index 000000000..b8f7d25e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4_Security_issue.md @@ -0,0 +1,11 @@ +--- +name: ⛔ Security Issue +about: Contact mail@qutebrowser.org for security issues. + +--- + +⚠ PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW. + +If you have found a security issue in qutebrowser, please send the details to +mail [at] qutebrowser.org and don't disclose it publicly until we can provide a +fix for it diff --git a/.github/img/macstadium.png b/.github/img/macstadium.png new file mode 100644 index 000000000..3655b2f3c Binary files /dev/null and b/.github/img/macstadium.png differ diff --git a/.gitignore b/.gitignore index b41285fd3..9efceef63 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ __pycache__ /doc/*.html /README.html /qutebrowser/html/doc/ -/qutebrowser/html/*.html /.venv* /.coverage /htmlcov diff --git a/.pylintrc b/.pylintrc index b654355c2..445d2adcc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -54,7 +54,7 @@ no-docstring-rgx=(^_|^main$) [FORMAT] max-line-length=79 -ignore-long-lines=(` in the download prompt. +- New settings: + * `content.mouse_lock` to handle HTML5 pointer locking. + * `completion.web_history.exclude` which hides a list of URL patterns from + the completion. + * `qt.process_model` which can be used to change Chromium's process model. + * `qt.low_end_device_mode` which turns on Chromium's low-end device mode. + This mode uses less RAM, but the expense of performance. + * `content.webrtc_ip_handling_policy`, which allows more + fine-grained/restrictive control about which IPs are exposed via WebRTC. + * `tabs.max_width` which allows to have a more "normal" look for tabs. + * `content.mute` which allows to mute pages (or all tabs) by default. +- Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only + once), as support for those is going to be removed in a future release. +- New t[iI][hHu] default bindings (similar to `tsh` etc.) to toggle images. +- The qute-pass userscript now has optional OTP support. +- When `:spawn --userscript` is called with a count, that count is now + passed to userscripts as `$QUTE_COUNT`. + +Changed +~~~~~~~ + +- Windows and macOS releases now bundle Python 3.7, PyQt 5.11.3 and Qt 5.11.2. + QtWebEngine includes security fixes up to Chromium 68.0.3440.75 and + http://code.qt.io/cgit/qt/qtwebengine.git/tree/dist/changes-5.11.2/?h=v5.11.2[various other fixes]. +- Various performance improvements when many tabs are opened. +- The `content.headers.referer` setting now works on QtWebEngine. +- The `:repeat` command now takes a count which is multiplied with the given + "times" argument. +- The default keybinding to leave passthrough mode was changed from `` + to ``, which makes pasting from the clipboard easier in + passthrough mode and is also unlikely to conflict with webpage bindings. +- The `app_id` is now set to `qutebrowser` for Wayland. +- `Command` or `Cmd` can now be used (instead of `Meta`) to map the Command key + on macOS. +- Using `:set option` now shows the value of the setting (like `:set option?` + already did). +- The `completion.web_history_max_items` setting got renamed to + `completion.web_history.max_items`. +- The Makefile shipped with qutebrowser now supports overriding variables + `DATADIR` and `MANDIR`. +- Regenerating completion history now shows a progress dialog. +- The `content.autoplay` setting now supports URL patterns on Qt >= 5.11. +- The `content.host_blocking.whitelist` setting now takes a list of URL + patterns instead of globs. +- In passthrough mode, Ctrl + Mousewheel now also gets passed through to the + page instead of zooming. +- Editing text in an external editor now simulates a JS "input" event, which + improves compatibility with websites reacting via JS to input. +- The `qute://settings` page is now properly sorted on Python 3.5. +- `:zoom`, `:zoom-in` and `:zoom-out` now have a `--quiet` switch which causes + them to not display a message. +- The `scrolling.bar` setting now takes three values instead of being a + boolean: `always`, `never`, and `when-searching` (which only displays it + while a search is active). +- '@@' now repeats the last run macro. +- The `content.host_blocking.lists` setting now accepts a `file://` URL to a + directory, and reads all files in that directory. +- The `:tab-give` and `:tab-take` command now have a new flag `--keep` which + causes them to keep the old tab around. +- `:navigate` now clears the URL query. + +Fixed +~~~~~ + +- `qute://` pages now work properly on Qt 5.11.2 +- Error when passing a substring with spaces to `:tab-take`. +- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly. +- When no documentation has been generated, the plaintext documentation now can + be shown for more files such as `qute://help/userscripts.html`. +- Crash when doing initial run on Wayland without XWayland. +- Crash when trying to load an empty session file. +- `:hint` with an invalid `--mode=` value now shows a proper error. +- Rare crash on Qt 5.11.2 when clicking on ` + With padding: +
+ With existing text (logs to JS):: +
+ + diff --git a/tests/end2end/data/issue4011.html b/tests/end2end/data/issue4011.html new file mode 100644 index 000000000..488193736 --- /dev/null +++ b/tests/end2end/data/issue4011.html @@ -0,0 +1,10 @@ + + + + + <img src="x" onerror="console.log('XSS')">foo + + + foo + + diff --git a/tests/end2end/data/misc/qutescheme_csrf.html b/tests/end2end/data/misc/qutescheme_csrf.html new file mode 100644 index 000000000..66c8fe240 --- /dev/null +++ b/tests/end2end/data/misc/qutescheme_csrf.html @@ -0,0 +1,20 @@ + + + + + CSRF issues with qute://settings + + + +
+ + Via link + Via redirect + + diff --git a/tests/end2end/data/search_select.js b/tests/end2end/data/search_select.js index 874e9e9fe..79d27c30c 100644 --- a/tests/end2end/data/search_select.js +++ b/tests/end2end/data/search_select.js @@ -7,6 +7,8 @@ if(s.rangeCount > 0) s.removeAllRanges(); for(var i = 0; i < toSelect.length; i++) { var range = document.createRange(); - range.selectNode(toSelect[i]); - s.addRange(range); + if (toSelect[i].childNodes.length > 0) { + range.selectNodeContents(toSelect[i].childNodes[0]); + s.addRange(range); + } } diff --git a/tests/end2end/data/userscripts/hello_if_count b/tests/end2end/data/userscripts/hello_if_count new file mode 100755 index 000000000..b9f07b86f --- /dev/null +++ b/tests/end2end/data/userscripts/hello_if_count @@ -0,0 +1,11 @@ +#!/bin/bash + +if [ "$QUTE_COUNT" -eq 5 ]; then + + echo "message-info 'Count is five!'" >> "$QUTE_FIFO" + +elif [ -z "$QUTE_COUNT" ]; then + + echo "message-info 'No count!'" >> "$QUTE_FIFO" + +fi diff --git a/tests/end2end/features/backforward.feature b/tests/end2end/features/backforward.feature index 413ee9d95..4de785517 100644 --- a/tests/end2end/features/backforward.feature +++ b/tests/end2end/features/backforward.feature @@ -3,6 +3,7 @@ Feature: Going back and forward. Testing the :back/:forward commands. + @skip # Too flaky Scenario: Going back/forward Given I open data/backforward/1.txt When I open data/backforward/2.txt @@ -74,6 +75,7 @@ Feature: Going back and forward. url: http://localhost:*/data/backforward/1.txt - url: http://localhost:*/data/backforward/2.txt + @flaky Scenario: Going back with count. Given I open data/backforward/1.txt When I open data/backforward/2.txt diff --git a/tests/end2end/features/caret.feature b/tests/end2end/features/caret.feature index 268828edd..e540bafcb 100644 --- a/tests/end2end/features/caret.feature +++ b/tests/end2end/features/caret.feature @@ -7,224 +7,6 @@ Feature: Caret mode Given I open data/caret.html And I run :tab-only ;; enter-mode caret - # document - - Scenario: Selecting the entire document - When I run :toggle-selection - And I run :move-to-end-of-document - And I run :yank selection - Then the clipboard should contain: - one two three - eins zwei drei - - four five six - vier fünf sechs - - Scenario: Moving to end and to start of document - When I run :move-to-end-of-document - And I run :move-to-start-of-document - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "one" - - Scenario: Moving to end and to start of document (with selection) - When I run :move-to-end-of-document - And I run :toggle-selection - And I run :move-to-start-of-document - And I run :yank selection - Then the clipboard should contain: - one two three - eins zwei drei - - four five six - vier fünf sechs - - # block - - Scenario: Selecting a block - When I run :toggle-selection - And I run :move-to-end-of-next-block - And I run :yank selection - Then the clipboard should contain: - one two three - eins zwei drei - - Scenario: Moving back to the end of previous block (with selection) - When I run :move-to-end-of-next-block with count 2 - And I run :toggle-selection - And I run :move-to-end-of-prev-block - And I run :move-to-prev-word - And I run :yank selection - Then the clipboard should contain: - drei - - four five six - - Scenario: Moving back to the end of previous block - When I run :move-to-end-of-next-block with count 2 - And I run :move-to-end-of-prev-block - And I run :toggle-selection - And I run :move-to-prev-word - And I run :yank selection - Then the clipboard should contain "drei" - - Scenario: Moving back to the start of previous block (with selection) - When I run :move-to-end-of-next-block with count 2 - And I run :toggle-selection - And I run :move-to-start-of-prev-block - And I run :yank selection - Then the clipboard should contain: - eins zwei drei - - four five six - - Scenario: Moving back to the start of previous block - When I run :move-to-end-of-next-block with count 2 - And I run :move-to-start-of-prev-block - And I run :toggle-selection - And I run :move-to-next-word - And I run :yank selection - Then the clipboard should contain "eins " - - Scenario: Moving to the start of next block (with selection) - When I run :toggle-selection - And I run :move-to-start-of-next-block - And I run :yank selection - Then the clipboard should contain "one two three\n" - - Scenario: Moving to the start of next block - When I run :move-to-start-of-next-block - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "eins" - - # line - - Scenario: Selecting a line - When I run :toggle-selection - And I run :move-to-end-of-line - And I run :yank selection - Then the clipboard should contain "one two three" - - Scenario: Moving and selecting a line - When I run :move-to-next-line - And I run :toggle-selection - And I run :move-to-end-of-line - And I run :yank selection - Then the clipboard should contain "eins zwei drei" - - Scenario: Selecting next line - When I run :toggle-selection - And I run :move-to-next-line - And I run :yank selection - Then the clipboard should contain "one two three\n" - - Scenario: Moving to end and to start of line - When I run :move-to-end-of-line - And I run :move-to-start-of-line - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "one" - - Scenario: Selecting a line (backwards) - When I run :move-to-end-of-line - And I run :toggle-selection - When I run :move-to-start-of-line - And I run :yank selection - Then the clipboard should contain "one two three" - - Scenario: Selecting previous line - When I run :move-to-next-line - And I run :toggle-selection - When I run :move-to-prev-line - And I run :yank selection - Then the clipboard should contain "one two three\n" - - Scenario: Moving to previous line - When I run :move-to-next-line - When I run :move-to-prev-line - And I run :toggle-selection - When I run :move-to-next-line - And I run :yank selection - Then the clipboard should contain "one two three\n" - - # word - - Scenario: Selecting a word - When I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "one" - - Scenario: Moving to end and selecting a word - When I run :move-to-end-of-word - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain " two" - - Scenario: Moving to next word and selecting a word - When I run :move-to-next-word - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "two" - - Scenario: Moving to next word and selecting until next word - When I run :move-to-next-word - And I run :toggle-selection - And I run :move-to-next-word - And I run :yank selection - Then the clipboard should contain "two " - - Scenario: Moving to previous word and selecting a word - When I run :move-to-end-of-word - And I run :toggle-selection - And I run :move-to-prev-word - And I run :yank selection - Then the clipboard should contain "one" - - Scenario: Moving to previous word - When I run :move-to-end-of-word - And I run :move-to-prev-word - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "one" - - # char - - Scenario: Selecting a char - When I run :toggle-selection - And I run :move-to-next-char - And I run :yank selection - Then the clipboard should contain "o" - - Scenario: Moving and selecting a char - When I run :move-to-next-char - And I run :toggle-selection - And I run :move-to-next-char - And I run :yank selection - Then the clipboard should contain "n" - - Scenario: Selecting previous char - When I run :move-to-end-of-word - And I run :toggle-selection - And I run :move-to-prev-char - And I run :yank selection - Then the clipboard should contain "e" - - Scenario: Moving to previous char - When I run :move-to-end-of-word - And I run :move-to-prev-char - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :yank selection - Then the clipboard should contain "e" - # :yank selection Scenario: :yank selection without selection @@ -261,42 +43,8 @@ Feature: Caret mode And the message "7 chars yanked to clipboard" should be shown. And the clipboard should contain "one two" - # :drop-selection - - Scenario: :drop-selection - When I run :toggle-selection - And I run :move-to-end-of-word - And I run :drop-selection - And I run :yank selection - Then the message "Nothing to yank" should be shown. - # :follow-selected - Scenario: :follow-selected without a selection - When I run :follow-selected - Then no crash should happen - - Scenario: :follow-selected with text - When I run :move-to-next-word - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :follow-selected - Then no crash should happen - - Scenario: :follow-selected with link (with JS) - When I set content.javascript.enabled to true - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :follow-selected - Then data/hello.txt should be loaded - - Scenario: :follow-selected with link (without JS) - When I set content.javascript.enabled to false - And I run :toggle-selection - And I run :move-to-end-of-word - And I run :follow-selected - Then data/hello.txt should be loaded - Scenario: :follow-selected with --tab (with JS) When I set content.javascript.enabled to true And I run :tab-only @@ -321,6 +69,7 @@ Feature: Caret mode - data/caret.html - data/hello.txt (active) + @flaky Scenario: :follow-selected with link tabbing (without JS) When I set content.javascript.enabled to false And I run :leave-mode @@ -329,6 +78,7 @@ Feature: Caret mode And I run :follow-selected Then data/hello.txt should be loaded + @flaky Scenario: :follow-selected with link tabbing (with JS) When I set content.javascript.enabled to true And I run :leave-mode @@ -337,6 +87,7 @@ Feature: Caret mode And I run :follow-selected Then data/hello.txt should be loaded + @flaky Scenario: :follow-selected with link tabbing in a tab (without JS) When I set content.javascript.enabled to false And I run :leave-mode @@ -345,6 +96,7 @@ Feature: Caret mode And I run :follow-selected --tab Then data/hello.txt should be loaded + @flaky Scenario: :follow-selected with link tabbing in a tab (with JS) When I set content.javascript.enabled to true And I run :leave-mode @@ -352,28 +104,3 @@ Feature: Caret mode And I run :fake-key And I run :follow-selected --tab Then data/hello.txt should be loaded - - # Search + caret mode - - # https://bugreports.qt.io/browse/QTBUG-60673 - @qtbug60673 - Scenario: yanking a searched line - When I run :leave-mode - And I run :search fiv - And I wait for "search found fiv" in the log - And I run :enter-mode caret - And I run :move-to-end-of-line - And I run :yank selection - Then the clipboard should contain "five six" - - @qtbug60673 - Scenario: yanking a searched line with multiple matches - When I run :leave-mode - And I run :search w - And I wait for "search found w" in the log - And I run :search-next - And I wait for "next_result found w" in the log - And I run :enter-mode caret - And I run :move-to-end-of-line - And I run :yank selection - Then the clipboard should contain "wei drei" diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 8040d84cc..ca14768ef 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -166,7 +166,7 @@ def clean_open_tabs(quteproc): @bdd.given('pdfjs is available') -def pdfjs_available(): +def pdfjs_available(data_tmpdir): if not pdfjs.is_available(): pytest.skip("No pdfjs installation found.") diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 7aa92abe0..7bde58b4f 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -80,7 +80,8 @@ Feature: Downloading things from a website. And I open data/downloads/issue1243.html And I hint with args "links download" and follow a And I wait for "Asking question text=* title='Save file to:'>, *" in the log - Then the error "Download error: No handler found for qute://!" should be shown + Then the error "Download error: No handler found for qute://" should be shown + And "NotFoundError while handling qute://* URL" should be logged Scenario: Downloading a data: link (issue 1214) When I set downloads.location.suggestion to filename @@ -91,6 +92,8 @@ Feature: Downloading things from a website. And I run :leave-mode Then no crash should happen + # https://github.com/qutebrowser/qutebrowser/issues/4240 + @qt!=5.11.2 Scenario: Downloading with SSL errors (issue 1413) When SSL is supported And I clear SSL errors @@ -211,6 +214,7 @@ Feature: Downloading things from a website. # works e.g. on a connection loss, which we can't test automatically. Then "Retrying downloads is unsupported *" should not be logged + @flaky Scenario: Retrying with count When I run :download http://localhost:(port)/data/downloads/download.bin And I run :download http://localhost:(port)/does-not-exist @@ -633,16 +637,16 @@ Feature: Downloading things from a website. And I run :download foo! Then the error "Invalid URL" should be shown - @qtwebengine_todo: pdfjs is not implemented yet Scenario: Downloading via pdfjs Given pdfjs is available When I set downloads.location.prompt to false And I set content.pdfjs to true - And I open data/misc/test.pdf + And I open data/misc/test.pdf without waiting And I wait for the javascript message "PDF * [*] (PDF.js: *)" And I run :click-element id download And I wait until the download is finished - Then the downloaded file test.pdf should exist + # We get viewer.html as name on QtWebKit... + # Then the downloaded file test.pdf should exist Scenario: Answering a question for a cancelled download (#415) When I set downloads.location.prompt to true diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature index 33535856c..ada688903 100644 --- a/tests/end2end/features/editor.feature +++ b/tests/end2end/features/editor.feature @@ -86,6 +86,7 @@ Feature: Opening external editors When I run :edit-url -t -b Then the error "Only one of -t/-b/-w can be given!" should be shown + @flaky Scenario: Editing a URL with invalid URL When I set url.auto_search to never And I open data/hello.txt diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index e33b16f68..cb146e1ae 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -15,6 +15,10 @@ Feature: Using hints And I hint with args "links normal" and follow xyz Then the error "No hint xyz!" should be shown + Scenario: Using :hint with invalid mode. + When I run :hint --mode=foobar + Then the error "Invalid mode: Invalid value 'foobar' - valid values: number, letter, word" should be shown + ### Opening in current or new tab Scenario: Following a hint and force to open in current tab. @@ -165,13 +169,13 @@ Feature: Using hints And I hint with args "all run message-info {hint-url}" and follow a Then the message "http://localhost:(port)/data/hello.txt" should be shown - @qt!=5.11.0 + @qt<5.11 Scenario: Clicking an invalid link When I open data/invalid_link.html And I hint with args "all" and follow a Then the error "Invalid link clicked - *" should be shown - @qt!=5.11.0 + @qt<5.11 Scenario: Clicking an invalid link opening in a new tab When I open data/invalid_link.html And I hint with args "all tab" and follow a @@ -185,6 +189,37 @@ Feature: Using hints # The actual check is already done above Then no crash should happen + Scenario: Error with invalid hint group + When I open data/hints/buttons.html + And I run :hint INVALID_GROUP + Then the error "Undefined hinting group 'INVALID_GROUP'" should be shown + + Scenario: Custom hint group + When I open data/hints/custom_group.html + And I set hints.selectors to {"custom":[".clickable"]} + And I hint with args "custom" and follow a + Then the javascript message "beep!" should be logged + + Scenario: Custom hint group with URL pattern + When I open data/hints/custom_group.html + And I run :set -tu *://*/data/hints/custom_group.html hints.selectors '{"custom": [".clickable"]}' + And I hint with args "custom" and follow a + Then the javascript message "beep!" should be logged + + Scenario: Fallback to global value with URL pattern set + When I open data/hints/custom_group.html + And I set hints.selectors to {"custom":[".clickable"]} + And I run :set -tu *://*/data/hints/custom_group.html hints.selectors '{"other": [".other"]}' + And I hint with args "custom" and follow a + Then the javascript message "beep!" should be logged + + @qtwebkit_skip + Scenario: Invalid custom selector + When I open data/hints/custom_group.html + And I set hints.selectors to {"custom":["@"]} + And I run :hint custom + Then the error "SyntaxError: Failed to execute 'querySelectorAll' on 'Document': '@' is not a valid selector." should be shown + # https://github.com/qutebrowser/qutebrowser/issues/1613 Scenario: Hinting inputs with padding When I open data/hints/input.html diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index 2e2e1712a..0706fde17 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -96,19 +96,36 @@ Feature: Page history http://localhost:(port)/data/hints/html/simple.html Simple link http://localhost:(port)/data/hello.txt + @flaky Scenario: Listing history When I open data/numbers/3.txt And I open data/numbers/4.txt And I open qute://history + And I wait 2s Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" # Hangs a lot on AppVeyor - @posix + @posix @flaky Scenario: Listing history with qute:history redirect When I open data/numbers/3.txt And I open data/numbers/4.txt And I open qute:history without waiting And I wait until qute://history is loaded + And I wait 2s Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" + + Scenario: XSS in :history + When I open data/issue4011.html + And I open qute://history + Then the javascript message "XSS" should not be logged + + @skip # Too flaky + Scenario: Escaping of URLs in :history + When I open query?one=1&two=2 + And I open qute://history + And I wait 2s # JS loads the history async + And I hint with args "links normal" and follow a + And I wait until query?one=1&two=2 is loaded + Then the query parameter two should be set to 2 diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 8c4348e5f..9f0d2f14b 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -8,7 +8,7 @@ Feature: Javascript stuff When I open data/javascript/consolelog.html Then the javascript message "console.log works!" should be logged - @flaky + @skip # Too flaky Scenario: Opening/Closing a window via JS When I open data/javascript/window_open.html And I run :tab-only @@ -21,7 +21,7 @@ Feature: Javascript stuff And the following tabs should be open: - data/javascript/window_open.html (active) - @qtwebkit_skip @flaky + @skip # Too flaky Scenario: Opening/closing a modal window via JS When I open data/javascript/window_open.html And I run :tab-only @@ -124,6 +124,9 @@ Feature: Javascript stuff # https://github.com/qutebrowser/qutebrowser/issues/1190 # https://github.com/qutebrowser/qutebrowser/issues/2495 + # Currently broken on Windows: + # https://github.com/qutebrowser/qutebrowser/issues/4230 + @posix Scenario: Checking visible/invisible window size When I run :tab-only And I open data/javascript/windowsize.html in a new background tab @@ -131,6 +134,7 @@ Feature: Javascript stuff And I run :tab-next Then the window sizes should be the same + @flaky Scenario: Checking visible/invisible window size with vertical tabbar When I run :tab-only And I set tabs.position to left @@ -164,7 +168,7 @@ Feature: Javascript stuff Scenario: Per-URL localstorage setting When I set content.local_storage to false - And I run :set -u http://localhost:*/data2/* content.local_storage true + And I run :set -tu http://localhost:*/data2/* content.local_storage true And I open data/javascript/localstorage.html And I wait for "[*] local storage is not working" in the log And I open data2/javascript/localstorage.html @@ -172,7 +176,7 @@ Feature: Javascript stuff Scenario: Per-URL JavaScript setting When I set content.javascript.enabled to false - And I run :set -u http://localhost:*/data2/* content.javascript.enabled true + And I run :set -tu http://localhost:*/data2/* content.javascript.enabled true And I open data2/javascript/enabled.html And I wait for "[*] JavaScript is enabled" in the log And I open data/javascript/enabled.html diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index b5ab7fcc1..5456b6739 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -18,6 +18,7 @@ Feature: Keyboard input # input.forward_unbound_keys + @qt<5.11.1 Scenario: Forwarding all keys When I open data/keyinput/log.html And I set input.forward_unbound_keys to all @@ -30,6 +31,7 @@ Feature: Keyboard input And the javascript message "key press: 112" should be logged And the javascript message "key release: 112" should be logged + @qt<5.11.1 Scenario: Forwarding special keys When I open data/keyinput/log.html And I set input.forward_unbound_keys to auto @@ -160,3 +162,46 @@ Feature: Keyboard input And I press the key "c" Then the message "foo 3" should be shown And the message "foo 3" should be shown + + # test all tabs.mode_on_change modes + + Scenario: mode on change normal + Given I set tabs.mode_on_change to normal + And I clean up open tabs + When I open data/hello.txt + And I run :enter-mode insert + And I open data/hello2.txt in a new background tab + And I run :tab-focus 2 + Then "Entering mode KeyMode.insert (reason: command)" should be logged + And "Leaving mode KeyMode.insert (reason: tab changed)" should be logged + And "Mode before tab change: insert (mode_on_change = normal)" should be logged + And "Mode after tab change: normal (mode_on_change = normal)" should be logged + + Scenario: mode on change persist + Given I set tabs.mode_on_change to persist + And I clean up open tabs + When I open data/hello.txt + And I run :enter-mode insert + And I open data/hello2.txt in a new background tab + And I run :tab-focus 2 + Then "Entering mode KeyMode.insert (reason: command)" should be logged + And "Leaving mode KeyMode.insert (reason: tab changed)" should not be logged + And "Mode before tab change: insert (mode_on_change = persist)" should be logged + And "Mode after tab change: insert (mode_on_change = persist)" should be logged + + Scenario: mode on change restore + Given I set tabs.mode_on_change to restore + And I clean up open tabs + When I open data/hello.txt + And I run :enter-mode insert + And I open data/hello2.txt in a new background tab + And I run :tab-focus 2 + And I run :enter-mode passthrough + And I run :tab-focus 1 + Then "Entering mode KeyMode.insert (reason: command)" should be logged + And "Mode before tab change: insert (mode_on_change = restore)" should be logged + And "Mode after tab change: normal (mode_on_change = restore)" should be logged + And "Entering mode KeyMode.passthrough (reason: command)" should be logged + And "Mode before tab change: passthrough (mode_on_change = restore)" should be logged + And "Entering mode KeyMode.insert (reason: restore)" should be logged + And "Mode after tab change: insert (mode_on_change = restore)" should be logged diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index f3b5ac47a..a8e81b7b5 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -118,6 +118,16 @@ Feature: Various utility commands. Then the javascript message "Hello from the page!" should be logged And "No output or error" should be logged + @qtwebkit_skip + Scenario: :jseval using too high of a world + When I run :jseval --world=257 console.log("Hello from JS!"); + Then the error "World ID should be between 0 and *" should be shown + + @qtwebkit_skip + Scenario: :jseval using a negative world id + When I run :jseval --world=-1 console.log("Hello from JS!"); + Then the error "World ID should be between 0 and *" should be shown + Scenario: :jseval --file using a file that exists as js-code When I run :jseval --file (testdata)/misc/jseval_file.js Then the javascript message "Hello from JS!" should be logged @@ -533,14 +543,14 @@ Feature: Various utility commands. Then "Renderer process crashed" should be logged And "* 'Error loading chrome://crash/'" should be logged - @qtwebkit_skip @no_invalid_lines @qt>=5.9 + @qtwebkit_skip @no_invalid_lines @qt>=5.9 @flaky Scenario: Renderer kill (5.9) When I run :open -t chrome://kill Then "Renderer process was killed" should be logged And "* 'Error loading chrome://kill/'" should be logged # https://github.com/qutebrowser/qutebrowser/issues/2290 - @qtwebkit_skip @no_invalid_lines + @qtwebkit_skip @no_invalid_lines @flaky Scenario: Navigating to URL after renderer process is gone When I run :tab-only And I open data/numbers/1.txt diff --git a/tests/end2end/features/navigate.feature b/tests/end2end/features/navigate.feature index b40b4d9cc..b5ad6ff54 100644 --- a/tests/end2end/features/navigate.feature +++ b/tests/end2end/features/navigate.feature @@ -13,6 +13,11 @@ Feature: Using :navigate 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 @@ -70,6 +75,16 @@ Feature: Using :navigate And I run :navigate next Then data/navigate/next.html should be loaded + Scenario: Navigating with invalid selector + When I set hints.selectors to {"links": ["@"]} + And I run :navigate next + Then the error "SyntaxError: Failed to execute 'querySelectorAll' on 'Document': '@' is not a valid selector." should be shown + + Scenario: Navigating with no next selector + When I set hints.selectors to {'all': ['a']} + And I run :navigate next + Then the error "Undefined hinting group 'links'" should be shown + # increment/decrement Scenario: Incrementing number in URL diff --git a/tests/end2end/features/open.feature b/tests/end2end/features/open.feature index ebae3cefb..3aec41786 100644 --- a/tests/end2end/features/open.feature +++ b/tests/end2end/features/open.feature @@ -41,6 +41,7 @@ Feature: Opening pages And I run :open 3 Then data/numbers/3.txt should be loaded + @flaky Scenario: Opening in a new tab Given I open about:blank When I run :tab-only diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index 1abaadd87..0f5954e19 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -73,7 +73,16 @@ Feature: Special qute:// pages And I open qute://help/index.html/../ without waiting Then qute://help/ should be loaded - Scenario: Opening a link with qute://help/img/ + @qtwebengine_skip + Scenario: Opening a link with qute://help/img/ (QtWebKit) + When the documentation is up to date + And I open qute://help/img/ without waiting + Then "*Error while * qute://*" should be logged + And "*Is a directory*" should be logged + And "* url='qute://help/img'* LoadStatus.error" should be logged + + @qtwebkit_skip + Scenario: Opening a link with qute://help/img/ (QtWebEngine) When the documentation is up to date And I open qute://help/img/ without waiting Then "*Error while * qute://*" should be logged @@ -98,8 +107,8 @@ Feature: Special qute:// pages # qute://settings - # Sometimes, an unrelated value gets set - @flaky + # Sometimes, an unrelated value gets set, which also breaks other tests + @skip Scenario: Focusing input fields in qute://settings and entering valid value When I set search.ignore_case to never And I open qute://settings @@ -116,7 +125,8 @@ Feature: Special qute:// pages Then the option search.ignore_case should be set to always # Sometimes, an unrelated value gets set - @flaky + # Too flaky... + @skip Scenario: Focusing input fields in qute://settings and entering invalid value When I open qute://settings # scroll to the right - the table does not fit in the default screen @@ -130,36 +140,86 @@ Feature: Special qute:// pages And I press the key "" Then "Invalid value 'foo' *" should be logged + @qtwebkit_skip + Scenario: qute://settings CSRF via img (webengine) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-img + Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged + + @qtwebkit_skip + Scenario: qute://settings CSRF via link (webengine) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-link + Then "Blocking malicious request from qute://settings/set?* to qute://settings/set?*" should be logged + + @qtwebkit_skip + Scenario: qute://settings CSRF via redirect (webengine) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-redirect + Then "Blocking malicious request from qute://settings/set?* to qute://settings/set?*" should be logged + + @qtwebkit_skip + Scenario: qute://settings CSRF via form (webengine) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-form + Then "Blocking malicious request from qute://settings/set?* to qute://settings/set?*" should be logged + + @qtwebkit_skip + Scenario: qute://settings CSRF token (webengine) + When I open qute://settings + And I run :jseval const xhr = new XMLHttpRequest(); xhr.open("GET", "qute://settings/set"); xhr.send() + Then "RequestDeniedError while handling qute://* URL" should be logged + And the error "Invalid CSRF token for qute://settings!" should be shown + + @qtwebengine_skip + Scenario: qute://settings CSRF via img (webkit) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-img + Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged + + @qtwebengine_skip + Scenario: qute://settings CSRF via link (webkit) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-link + Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged + And "Error while loading qute://settings/set?*: Invalid qute://settings request" should be logged + + @qtwebengine_skip + Scenario: qute://settings CSRF via redirect (webkit) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-redirect + Then "Blocking malicious request from http://localhost:*/data/misc/qutescheme_csrf.html to qute://settings/set?*" should be logged + And "Error while loading qute://settings/set?*: Invalid qute://settings request" should be logged + + @qtwebengine_skip + Scenario: qute://settings CSRF via form (webkit) + When I open data/misc/qutescheme_csrf.html + And I run :click-element id via-form + Then "Error while loading qute://settings/set?*: Unsupported request type" should be logged + # pdfjs support - @qtwebengine_skip: pdfjs is not implemented yet Scenario: pdfjs is used for pdf files Given pdfjs is available When I set content.pdfjs to true - And I open data/misc/test.pdf + And I open data/misc/test.pdf without waiting Then the javascript message "PDF * [*] (PDF.js: *)" should be logged - @qtwebengine_todo: pdfjs is not implemented yet Scenario: pdfjs is not used when disabled When I set content.pdfjs to false And I set downloads.location.prompt to false - And I open data/misc/test.pdf + And I open data/misc/test.pdf without waiting Then "Download test.pdf finished" should be logged - @qtwebengine_skip: pdfjs is not implemented yet + @qtwebengine_skip: Might work with Qt 5.12 Scenario: Downloading a pdf via pdf.js button (issue 1214) Given pdfjs is available - # WORKAROUND to prevent the "Painter ended with 2 saved states" warning - # Might be related to https://bugreports.qt.io/browse/QTBUG-13524 and - # a weird interaction with the previous test. - And I have a fresh instance When I set content.pdfjs to true - And I set downloads.location.suggestion to filename And I set downloads.location.prompt to true - And I open data/misc/test.pdf + And I open data/misc/test.pdf without waiting And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log And I run :jseval document.getElementById("download").click() - And I wait for "Asking question text=* title='Save file to:'>, *" in the log + And I wait for "Asking question text=* title='Save file to:'>, *" in the log And I run :leave-mode Then no crash should happen diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature index 83eef16ab..a77998e2d 100644 --- a/tests/end2end/features/scroll.feature +++ b/tests/end2end/features/scroll.feature @@ -145,6 +145,7 @@ Feature: Scrolling And I wait until the scroll position changed to 0/0 Then the page should not be scrolled + @skip # Too flaky Scenario: Scrolling down with a very big count When I run :scroll down with count 99999999999 # Make sure it doesn't hang @@ -162,6 +163,7 @@ Feature: Scrolling When I run :scroll-to-perc 100 Then the page should be scrolled vertically + @flaky Scenario: Scrolling to bottom and to top with :scroll-to-perc When I run :scroll-to-perc 100 And I wait until the scroll position changed @@ -173,10 +175,12 @@ Feature: Scrolling When I run :scroll-to-perc 50 Then the page should be scrolled vertically + @flaky Scenario: Scrolling to middle with :scroll-to-perc (float) When I run :scroll-to-perc 50.5 Then the page should be scrolled vertically + @flaky Scenario: Scrolling to middle and to top with :scroll-to-perc When I run :scroll-to-perc 50 And I wait until the scroll position changed @@ -231,7 +235,7 @@ Feature: Scrolling Scenario: :scroll-to-perc with count and argument When I run :scroll-to-perc 0 with count 50 Then the page should be scrolled vertically - + # https://github.com/qutebrowser/qutebrowser/issues/1821 @issue3572 Scenario: :scroll-to-perc without doctype @@ -249,6 +253,7 @@ Feature: Scrolling When I run :scroll-page 0 1.5 Then the page should be scrolled vertically + @flaky Scenario: Scrolling down and up with :scroll-page When I run :scroll-page 0 1 And I wait until the scroll position changed @@ -289,6 +294,7 @@ Feature: Scrolling @issue3572 Scenario: :scroll-page with --bottom-navigate and zoom When I run :zoom 200 + And I wait 0.5s And I run :scroll-to-perc 100 And I wait until the scroll position changed And I run :scroll-page --bottom-navigate next 0 1 diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 568831c0d..8d9d28e78 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -214,12 +214,14 @@ Feature: Searching on a page # TODO: wrapping message without scrolling ## follow searched links + @flaky Scenario: Follow a searched link When I run :search follow And I wait for "search found follow" in the log And I run :follow-selected Then data/hello.txt should be loaded + @flaky Scenario: Follow a searched link in a new tab When I run :window-only And I run :search follow @@ -260,7 +262,8 @@ Feature: Searching on a page - data/search.html - data/hello.txt (active) - @qtwebkit_skip: Not supported in qtwebkit + # Too flaky + @qtwebkit_skip: Not supported in qtwebkit @skip Scenario: Follow a searched link in an iframe When I open data/iframe_search.html And I run :tab-only @@ -269,7 +272,7 @@ Feature: Searching on a page And I run :follow-selected Then "navigation request: url http://localhost:*/data/hello.txt, type Type.link_clicked, is_main_frame False" should be logged - @qtwebkit_skip: Not supported in qtwebkit + @qtwebkit_skip: Not supported in qtwebkit @flaky Scenario: Follow a tabbed searched link in an iframe When I open data/iframe_search.html And I run :tab-only @@ -280,3 +283,9 @@ Feature: Searching on a page Then the following tabs should be open: - data/iframe_search.html - data/hello.txt (active) + + Scenario: Closing a tab during a search + When I run :open -b about:blank + And I run :search a + And I run :tab-close + Then no crash should happen diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index cb54e34cf..626a88ba8 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -228,7 +228,7 @@ Feature: Saving and loading sessions url: http://localhost:*/data/hello.txt # Seems like that bug is fixed upstream in QtWebEngine - @qtwebkit_skip + @qtwebkit_skip @flaky Scenario: Saving a session with a page using history.replaceState() and navigating away When I open data/sessions/history_replace_state.html without waiting And I wait for "* Called history.replaceState" in the log @@ -363,7 +363,7 @@ Feature: Saving and loading sessions And I replace "about:blank" by "http://localhost:(port)/data/numbers/2.txt" in the "loaded_session" session file And I run :session-load loaded_session Then data/numbers/2.txt should be loaded - + @qtwebengine_flaky Scenario: Loading and deleting a session When I open about:blank diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature index 2a1ea0039..87ffb53e0 100644 --- a/tests/end2end/features/spawn.feature +++ b/tests/end2end/features/spawn.feature @@ -47,6 +47,17 @@ Feature: :spawn - data/hello.txt - data/hello.txt (active) + @posix + Scenario: Running :spawn with userscript and count + When I run :spawn -u (testdata)/userscripts/hello_if_count with count 5 + Then the message "Count is five!" should be shown + + @posix + Scenario: Running :spawn with userscript and no count + When I run :spawn -u (testdata)/userscripts/hello_if_count + Then the message "No count!" should be shown + + @windows Scenario: Running :spawn with userscript on Windows When I open data/hello.txt diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 1841ef9c9..7f4d4635c 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -894,6 +894,71 @@ Feature: Tab management - about:blank - data/hello.txt (active) + # stacking tabs + Scenario: stacking tabs opening tab with tabs.new_position.related next + When I set tabs.new_position.related to next + And I set tabs.new_position.stacking to true + And I set tabs.background to true + And I open about:blank + And I open data/navigate/index.html in a new tab + And I hint with args "all tab-bg" and follow a + And I hint with args "all tab-bg" and follow s + And I wait until data/navigate/prev.html is loaded + And I wait until data/navigate/next.html is loaded + Then the following tabs should be open: + - about:blank + - data/navigate/index.html (active) + - data/navigate/prev.html + - data/navigate/next.html + + Scenario: stacking tabs opening tab with tabs.new_position.related prev + When I set tabs.new_position.related to prev + And I set tabs.new_position.stacking to true + And I set tabs.background to true + And I open about:blank + And I open data/navigate/index.html in a new tab + And I hint with args "all tab-bg" and follow a + And I hint with args "all tab-bg" and follow s + And I wait until data/navigate/prev.html is loaded + And I wait until data/navigate/next.html is loaded + Then the following tabs should be open: + - about:blank + - data/navigate/next.html + - data/navigate/prev.html + - data/navigate/index.html (active) + + Scenario: no stacking tabs opening tab with tabs.new_position.related next + When I set tabs.new_position.related to next + And I set tabs.new_position.stacking to false + And I set tabs.background to true + And I open about:blank + And I open data/navigate/index.html in a new tab + And I hint with args "all tab-bg" and follow a + And I hint with args "all tab-bg" and follow s + And I wait until data/navigate/prev.html is loaded + And I wait until data/navigate/next.html is loaded + Then the following tabs should be open: + - about:blank + - data/navigate/index.html (active) + - data/navigate/next.html + - data/navigate/prev.html + + Scenario: no stacking tabs opening tab with tabs.new_position.related prev + When I set tabs.new_position.related to prev + And I set tabs.new_position.stacking to false + And I set tabs.background to true + And I open about:blank + And I open data/navigate/index.html in a new tab + And I hint with args "all tab-bg" and follow a + And I hint with args "all tab-bg" and follow s + And I wait until data/navigate/prev.html is loaded + And I wait until data/navigate/next.html is loaded + Then the following tabs should be open: + - about:blank + - data/navigate/prev.html + - data/navigate/next.html + - data/navigate/index.html (active) + # :buffer Scenario: :buffer without args or count @@ -915,6 +980,7 @@ Feature: Tab management When I run :buffer invalid title Then the error "No matching tab for: invalid title" should be shown + @flaky Scenario: :buffer with matching title and two windows When I open data/title.html And I open data/search.html in a new tab @@ -1250,3 +1316,41 @@ Feature: Tab management Then the following tabs should be open: - data/numbers/1.txt - data/numbers/2.txt (pinned) (active) + + + Scenario: Focused webview after clicking link in bg + When I open data/hints/link_input.html + And I run :click-element id qute-input-existing + And I wait for "Entering mode KeyMode.insert *" in the log + And I run :leave-mode + And I hint with args "all tab-bg" and follow a + And I wait until data/hello.txt is loaded + And I run :enter-mode insert + And I run :fake-key -g new + Then the javascript message "contents: existingnew" should be logged + + Scenario: Focused webview after opening link in bg + When I open data/hints/link_input.html + And I run :click-element id qute-input-existing + And I wait for "Entering mode KeyMode.insert *" in the log + And I run :leave-mode + And I open data/hello.txt in a new background tab + And I run :enter-mode insert + And I run :fake-key -g new + Then the javascript message "contents: existingnew" should be logged + + Scenario: Focused prompt after opening link in bg + When I open data/hints/link_input.html + When I run :set-cmd-text -s :message-info + And I open data/hello.txt in a new background tab + And I run :fake-key -g hello-world + And I run :command-accept + Then the message "hello-world" should be shown + + Scenario: Focused prompt after opening link in fg + When I open data/hints/link_input.html + When I run :set-cmd-text -s :message-info + And I open data/hello.txt in a new tab + And I run :fake-key -g hello-world + And I run :command-accept + Then the message "hello-world" should be shown diff --git a/tests/end2end/features/test_history_bdd.py b/tests/end2end/features/test_history_bdd.py index 6efa08330..4d477d832 100644 --- a/tests/end2end/features/test_history_bdd.py +++ b/tests/end2end/features/test_history_bdd.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import json import logging import re @@ -34,6 +35,19 @@ def turn_on_sql_history(quteproc): quteproc.wait_for_load_finished_url('qute://pyeval') +@bdd.then(bdd.parsers.parse("the query parameter {name} should be set to " + "{value}")) +def check_query(quteproc, name, value): + """Check if a given query is set correctly. + + This assumes we're on the server query page. + """ + content = quteproc.get_content() + data = json.loads(content) + print(data) + assert data[name] == value + + @bdd.then(bdd.parsers.parse("the history should contain:\n{expected}")) def check_history(quteproc, server, tmpdir, expected): path = tmpdir / 'history' diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py index 12d9cbeec..0d74700b4 100644 --- a/tests/end2end/features/test_prompts_bdd.py +++ b/tests/end2end/features/test_prompts_bdd.py @@ -17,9 +17,13 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import time + import pytest_bdd as bdd bdd.scenarios('prompts.feature') +from qutebrowser.utils import qtutils + @bdd.when("I load an SSL page") def load_ssl_page(quteproc, ssl_server): @@ -46,14 +50,21 @@ def no_prompt_shown(quteproc): @bdd.then("a SSL error page should be shown") def ssl_error_page(request, quteproc): - if not request.config.webengine: - line = quteproc.wait_for(message='Error while loading *: SSL ' - 'handshake failed') - line.expected = True - quteproc.wait_for(message="Changing title for idx * to 'Error " - "loading page: *'") - content = quteproc.get_content().strip() - assert "Unable to load page" in content + if request.config.webengine and qtutils.version_check('5.9'): + quteproc.wait_for(message="Certificate error: *") + time.sleep(0.5) # Wait for error page to appear + content = quteproc.get_content().strip() + assert ("ERR_INSECURE_RESPONSE" in content or # Qt <= 5.10 + "ERR_CERT_AUTHORITY_INVALID" in content) # Qt 5.11 + else: + if not request.config.webengine: + line = quteproc.wait_for(message='Error while loading *: SSL ' + 'handshake failed') + line.expected = True + quteproc.wait_for(message="Changing title for idx * to 'Error " + "loading page: *'") + content = quteproc.get_content().strip() + assert "Unable to load page" in content class AbstractCertificateErrorWrapper: diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index 836af5c4f..ec38116c3 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -116,6 +116,7 @@ Feature: quickmarks and bookmarks When I run :quickmark-add http://localhost:(port)/data/numbers/9.txt nine Then the quickmark file should contain "nine http://localhost:*/data/numbers/9.txt" + @flaky Scenario: Saving a quickmark (:quickmark-save) When I open data/numbers/10.txt And I run :quickmark-save diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index 5ccbac9b3..fac335813 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -41,6 +41,15 @@ Feature: Miscellaneous utility commands exposed to the user. # If we have an error, the test will fail Then no crash should happen + Scenario: :repeat with count + When I run :repeat 3 message-info "repeat-test 3" with count 2 + Then the message "repeat-test 3" should be shown + And the message "repeat-test 3" should be shown + And the message "repeat-test 3" should be shown + And the message "repeat-test 3" should be shown + And the message "repeat-test 3" should be shown + And the message "repeat-test 3" should be shown + ## :run-with-count Scenario: :run-with-count diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature index dc3024eb4..c6f9b572b 100644 --- a/tests/end2end/features/yankpaste.feature +++ b/tests/end2end/features/yankpaste.feature @@ -77,6 +77,11 @@ Feature: Yanking and pasting. Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title.html?a;b&c=d" should be shown And the clipboard should contain "http://localhost:(port)/data/title.html?a;b&c=d" + Scenario: Yanking with --quiet + When I open data/title.html + And I run :yank --quiet + Then "Yanked URL to clipboard: *" should not be logged + #### {clipboard} and {primary} Scenario: Pasting a URL diff --git a/tests/end2end/features/zoom.feature b/tests/end2end/features/zoom.feature index c962ecb9f..5de89b65b 100644 --- a/tests/end2end/features/zoom.feature +++ b/tests/end2end/features/zoom.feature @@ -111,3 +111,15 @@ Feature: Zooming in and out And I open data/hello.txt in a new tab And I run :tab-only Then the zoom should be 200% + + Scenario: Zooming in with --quiet + When I run :zoom-in --quiet + Then "Zoom level: *" should not be logged + + Scenario: Zooming out with --quiet + When I run :zoom-out --quiet + Then "Zoom level: *" should not be logged + + Scenario: Zooming with --quiet + When I run :zoom --quiet + Then "Zoom level: *" should not be logged diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 4101e6142..3cb3ee89a 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -65,6 +65,7 @@ def is_ignored_lowlevel_message(message): "GL(dl_tls_generation)' failed!*"), # ??? 'getrlimit(RLIMIT_NOFILE) failed', + 'libpng warning: Skipped (ignored) a chunk between APNG chunks', # Travis CI containers don't have a /etc/machine-id ('*D-Bus library appears to be incorrectly set up; failed to read ' 'machine uuid: Failed to open "/etc/machine-id": No such file or ' @@ -104,6 +105,15 @@ def is_ignored_lowlevel_message(message): # Qt 5.11 # DevTools listening on ws://127.0.0.1:37945/devtools/browser/... 'DevTools listening on *', + # /home/travis/build/qutebrowser/qutebrowser/.tox/py36-pyqt511-cov/lib/ + # python3.6/site-packages/PyQt5/Qt/libexec/QtWebEngineProcess: + # /lib/x86_64-linux-gnu/libdbus-1.so.3: no version information + # available (required by /home/travis/build/qutebrowser/qutebrowser/ + # .tox/py36-pyqt511-cov/lib/python3.6/site-packages/PyQt5/Qt/libexec/ + # ../lib/libQt5WebEngineCore.so.5) + '*/QtWebEngineProcess: /lib/x86_64-linux-gnu/libdbus-1.so.3: no ' + 'version information available (required by ' + '*/libQt5WebEngineCore.so.5)', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) @@ -204,6 +214,22 @@ def is_ignored_chromium_message(line): # [30412:30412:0323/074933.387250:ERROR:node_channel.cc(899)] Dropping # message on closed channel. 'Dropping message on closed channel.', + # [2204:1408:0703/113804.788:ERROR: + # gpu_process_transport_factory.cc(1019)] Lost UI shared context. + 'Lost UI shared context.', + + # Qt 5.12 + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70702 + # [32123:32123:0923/224739.457307:ERROR:in_progress_cache_impl.cc(192)] + # Cache is not initialized, cannot RetrieveEntry. + 'Cache is not initialized, cannot RetrieveEntry.', + 'Cache is not initialized, cannot AddOrReplaceEntry.', + # [10518:10518:0924/121250.186121:WARNING: + # render_frame_host_impl.cc(431)] + # InterfaceRequest was dropped, the document is no longer active: + # content.mojom.RendererAudioOutputStreamFactory + 'InterfaceRequest was dropped, the document is no longer active: ' + 'content.mojom.RendererAudioOutputStreamFactory', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) @@ -641,7 +667,7 @@ class QuteProc(testprocess.Process): # \ and " in a value should be treated literally, so escape them value = value.replace('\\', r'\\') value = value.replace('"', '\\"') - self.send_cmd(':set "{}" "{}"'.format(option, value), escape=False) + self.send_cmd(':set -t "{}" "{}"'.format(option, value), escape=False) self.wait_for(category='config', message='Config option changed: *') @contextlib.contextmanager diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index 7d9af2ee3..15cd0becc 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -261,6 +261,11 @@ def response_headers(): return response +@app.route('/query') +def query(): + return flask.jsonify(flask.request.args) + + @app.route('/user-agent') def view_user_agent(): """Return User-Agent.""" diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index abc94d70d..9dee23c53 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -117,6 +117,7 @@ def test_hints(test_name, zoom_text_only, zoom_level, find_implementation, quteproc.set_setting('hints.find_implementation', 'javascript') +@pytest.mark.skip # Too flaky def test_word_hints_issue1393(quteproc, tmpdir): dict_file = tmpdir / 'dict' dict_file.write(textwrap.dedent(""" diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index fbab681a4..0375554b8 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -65,9 +65,16 @@ def temp_basedir_env(tmpdir, short_tmpdir): runtime_dir.ensure(dir=True) runtime_dir.chmod(0o700) - (data_dir / 'qutebrowser' / 'state').write_text( - '[general]\nquickstart-done = 1\nbackend-warning-shown=1', - encoding='utf-8', ensure=True) + lines = [ + '[general]', + 'quickstart-done = 1', + 'backend-warning-shown = 1', + 'old-qt-warning-shown = 1', + 'webkit-warning-shown = 1', + ] + + state_file = data_dir / 'qutebrowser' / 'state' + state_file.write_text('\n'.join(lines), encoding='utf-8', ensure=True) env = { 'XDG_DATA_HOME': str(data_dir), @@ -368,8 +375,11 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): """Make sure settings from qute://settings are persistent.""" args = _base_args(request.config) + ['--basedir', str(short_tmpdir)] quteproc_new.start(args) - quteproc_new.open_path( - 'qute://settings/set?option=search.ignore_case&value=always') + quteproc_new.open_path('qute://settings/') + quteproc_new.send_cmd(':jseval --world main ' + 'cset("search.ignore_case", "always")') + quteproc_new.wait_for(message='No output or error') + assert quteproc_new.get_setting('search.ignore_case') == 'always' quteproc_new.send_cmd(':quit') diff --git a/tests/end2end/test_mhtml_e2e.py b/tests/end2end/test_mhtml_e2e.py index feddc963d..953824b8c 100644 --- a/tests/end2end/test_mhtml_e2e.py +++ b/tests/end2end/test_mhtml_e2e.py @@ -61,6 +61,12 @@ def normalize_line(line): return line +def normalize_whole(s): + if qtutils.version_check('5.12', compiled=False): + s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID') + return s + + class DownloadDir: """Abstraction over a download directory.""" @@ -81,10 +87,13 @@ class DownloadDir: def compare_mhtml(self, filename): with open(filename, 'r', encoding='utf-8') as f: - expected_data = [normalize_line(line) for line in f - if normalize_line(line) is not None] - actual_data = self.read_file() - actual_data = [normalize_line(line) for line in actual_data] + expected_data = '\n'.join(normalize_line(line) + for line in f + if normalize_line(line) is not None) + actual_data = '\n'.join(normalize_line(line) + for line in self.read_file()) + actual_data = normalize_whole(actual_data) + assert actual_data == expected_data diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index b183933d2..fac23ae1c 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -31,6 +31,8 @@ import itertools import textwrap import unittest.mock import types +import mimetypes +import os.path import attr import pytest @@ -40,16 +42,42 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout from PyQt5.QtNetwork import QNetworkCookieJar import helpers.stubs as stubsmod -import helpers.utils from qutebrowser.config import (config, configdata, configtypes, configexc, - configfiles) + configfiles, configcache) from qutebrowser.utils import objreg, standarddir, utils, usertypes -from qutebrowser.browser import greasemonkey +from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser.webkit import cookies from qutebrowser.misc import savemanager, sql, objects from qutebrowser.keyinput import modeman +_qute_scheme_handler = None + + +class WidgetContainer(QWidget): + + """Container for another widget.""" + + def __init__(self, qtbot, parent=None): + super().__init__(parent) + self._qtbot = qtbot + self.vbox = QVBoxLayout(self) + qtbot.add_widget(self) + + def set_widget(self, widget): + self.vbox.addWidget(widget) + widget.container = self + + def expose(self): + with self._qtbot.waitExposed(self): + self.show() + + +@pytest.fixture +def widget_container(qtbot): + return WidgetContainer(qtbot) + + class WinRegistryHelper: """Helper class for win_registry.""" @@ -79,11 +107,6 @@ class WinRegistryHelper: del objreg.window_registry[win_id] -@pytest.fixture -def callback_checker(qtbot): - return helpers.utils.CallbackChecker(qtbot) - - class FakeStatusBar(QWidget): """Fake statusbar to test progressbar sizing.""" @@ -101,22 +124,11 @@ class FakeStatusBar(QWidget): @pytest.fixture -def fake_statusbar(qtbot): +def fake_statusbar(widget_container): """Fixture providing a statusbar in a container window.""" - container = QWidget() - qtbot.add_widget(container) - vbox = QVBoxLayout(container) - vbox.addStretch() - - statusbar = FakeStatusBar(container) - # to make sure container isn't GCed - # pylint: disable=attribute-defined-outside-init - statusbar.container = container - vbox.addWidget(statusbar) - # pylint: enable=attribute-defined-outside-init - - with qtbot.waitExposed(container): - container.show() + widget_container.vbox.addStretch() + statusbar = FakeStatusBar(widget_container) + widget_container.set_widget(statusbar) return statusbar @@ -152,26 +164,72 @@ def greasemonkey_manager(data_tmpdir): objreg.delete('greasemonkey') +@pytest.fixture(scope='session') +def testdata_scheme(qapp): + try: + global _qute_scheme_handler + from qutebrowser.browser.webengine import webenginequtescheme + from PyQt5.QtWebEngineWidgets import QWebEngineProfile + _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler( + parent=qapp) + _qute_scheme_handler.install(QWebEngineProfile.defaultProfile()) + except ImportError: + pass + + @qutescheme.add_handler('testdata') + def handler(url): # pylint: disable=unused-variable + file_abs = os.path.abspath(os.path.dirname(__file__)) + filename = os.path.join(file_abs, os.pardir, 'end2end', + url.path().lstrip('/')) + with open(filename, 'rb') as f: + data = f.read() + + mimetype, _encoding = mimetypes.guess_type(filename) + return mimetype, data + + @pytest.fixture -def webkit_tab(qtbot, tab_registry, cookiejar_and_cache, mode_manager, - session_manager_stub, greasemonkey_manager, fake_args): +def web_tab_setup(qtbot, tab_registry, session_manager_stub, + greasemonkey_manager, fake_args, host_blocker_stub, + config_stub, testdata_scheme): + """Shared setup for webkit_tab/webengine_tab.""" + # Make sure error logging via JS fails tests + config_stub.val.content.javascript.log = { + 'info': 'info', + 'error': 'error', + 'unknown': 'error', + 'warning': 'error', + } + + +@pytest.fixture +def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager, + widget_container): webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab') + tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager, private=False) - qtbot.add_widget(tab) + widget_container.set_widget(tab) return tab @pytest.fixture -def webengine_tab(qtbot, tab_registry, fake_args, mode_manager, - session_manager_stub, greasemonkey_manager, - redirect_webengine_data): +def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data, + tabbed_browser_stubs, mode_manager, widget_container): + tabwidget = tabbed_browser_stubs[0].widget + tabwidget.current_index = 0 + tabwidget.index_of = 0 + webenginetab = pytest.importorskip( 'qutebrowser.browser.webengine.webenginetab') + tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager, private=False) - qtbot.add_widget(tab) - return tab + widget_container.set_widget(tab) + yield tab + # If a page is still loading here, _on_load_finished could get called + # during teardown when session_manager_stub is already deleted. + tab.stop() @pytest.fixture(params=['webkit', 'webengine']) @@ -249,6 +307,9 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub): container = config.ConfigContainer(conf) monkeypatch.setattr(config, 'val', container) + cache = configcache.ConfigCache() + monkeypatch.setattr(config, 'cache', cache) + try: configtypes.Font.monospace_fonts = container.fonts.monospace except configexc.NoOptionError: @@ -448,23 +509,41 @@ def fake_args(request): @pytest.fixture -def mode_manager(win_registry, config_stub, qapp): - mm = modeman.ModeManager(0) - objreg.register('mode-manager', mm, scope='window', window=0) +def mode_manager(win_registry, config_stub, key_config_stub, qapp): + mm = modeman.init(0, parent=qapp) yield mm objreg.delete('mode-manager', scope='window', window=0) +def standarddir_tmpdir(folder, monkeypatch, tmpdir): + """Set tmpdir/config as the configdir. + + Use this to avoid creating a 'real' config dir (~/.config/qute_test). + """ + confdir = tmpdir / folder + confdir.ensure(dir=True) + if hasattr(standarddir, folder): + monkeypatch.setattr(standarddir, folder, + lambda **_kwargs: str(confdir)) + return confdir + + +@pytest.fixture +def download_tmpdir(monkeypatch, tmpdir): + """Set tmpdir/download as the downloaddir. + + Use this to avoid creating a 'real' download dir (~/.config/qute_test). + """ + return standarddir_tmpdir('download', monkeypatch, tmpdir) + + @pytest.fixture def config_tmpdir(monkeypatch, tmpdir): """Set tmpdir/config as the configdir. Use this to avoid creating a 'real' config dir (~/.config/qute_test). """ - confdir = tmpdir / 'config' - confdir.ensure(dir=True) - monkeypatch.setattr(standarddir, 'config', lambda auto=False: str(confdir)) - return confdir + return standarddir_tmpdir('config', monkeypatch, tmpdir) @pytest.fixture @@ -473,10 +552,7 @@ def data_tmpdir(monkeypatch, tmpdir): Use this to avoid creating a 'real' data dir (~/.local/share/qute_test). """ - datadir = tmpdir / 'data' - datadir.ensure(dir=True) - monkeypatch.setattr(standarddir, 'data', lambda system=False: str(datadir)) - return datadir + return standarddir_tmpdir('data', monkeypatch, tmpdir) @pytest.fixture @@ -485,10 +561,7 @@ def runtime_tmpdir(monkeypatch, tmpdir): Use this to avoid creating a 'real' runtime dir. """ - runtimedir = tmpdir / 'runtime' - runtimedir.ensure(dir=True) - monkeypatch.setattr(standarddir, 'runtime', lambda: str(runtimedir)) - return runtimedir + return standarddir_tmpdir('runtime', monkeypatch, tmpdir) @pytest.fixture @@ -497,10 +570,7 @@ def cache_tmpdir(monkeypatch, tmpdir): Use this to avoid creating a 'real' cache dir (~/.cache/qute_test). """ - cachedir = tmpdir / 'cache' - cachedir.ensure(dir=True) - monkeypatch.setattr(standarddir, 'cache', lambda: str(cachedir)) - return cachedir + return standarddir_tmpdir('cache', monkeypatch, tmpdir) @pytest.fixture @@ -538,7 +608,6 @@ class ModelValidator: """Validates completion models.""" def __init__(self, modeltester): - modeltester.data_display_may_return_none = True self._model = None self._modeltester = modeltester @@ -565,3 +634,14 @@ def download_stub(win_registry, tmpdir, stubs): objreg.register('qtnetwork-download-manager', stub) yield stub objreg.delete('qtnetwork-download-manager') + + +@pytest.fixture +def web_history(fake_save_manager, tmpdir, init_sql, config_stub, stubs): + """Create a web history and register it into objreg.""" + config_stub.val.completion.timestamp_format = '%Y-%m-%d' + config_stub.val.completion.web_history.max_items = -1 + web_history = history.WebHistory(stubs.FakeHistoryProgress()) + objreg.register('web-history', web_history) + yield web_history + objreg.delete('web-history') diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 84e5b0125..7b81b8c64 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -257,7 +257,7 @@ class FakeWebTab(browsertab.AbstractTab): self.history = FakeWebTabHistory(self, can_go_back=can_go_back, can_go_forward=can_go_forward) self.scroller = FakeWebTabScroller(self, scroll_pos_perc) - self.audio = FakeWebTabAudio() + self.audio = FakeWebTabAudio(self) wrapped = QWidget() self._layout.wrap(self, wrapped) @@ -475,6 +475,9 @@ class HostBlockerStub: def __init__(self): self.blocked_hosts = set() + def is_blocked(self, url, first_party_url=None): + return url in self.blocked_hosts + class SessionManagerStub: @@ -632,3 +635,22 @@ class FakeDownloadManager: shutil.copyfileobj(fake_url_file, download_item.fileobj) self.downloads.append(download_item) return download_item + + +class FakeHistoryProgress: + + """Fake for a WebHistoryProgress object.""" + + def __init__(self): + self._started = False + self._finished = False + self._value = 0 + + def start(self, _text, _maximum): + self._started = True + + def tick(self): + self._value += 1 + + def finish(self): + self._finished = True diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 06cf54467..731103b68 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -27,8 +27,6 @@ import contextlib import pytest -from PyQt5.QtCore import QObject, pyqtSignal - from qutebrowser.utils import qtutils @@ -182,28 +180,3 @@ def abs_datapath(): @contextlib.contextmanager def nop_contextmanager(): yield - - -class CallbackChecker(QObject): - - """Check if a value provided by a callback is the expected one.""" - - got_result = pyqtSignal(object) - UNSET = object() - - def __init__(self, qtbot, parent=None): - super().__init__(parent) - self._qtbot = qtbot - self._result = self.UNSET - - def callback(self, result): - """Callback which can be passed to runJavaScript.""" - self._result = result - self.got_result.emit(result) - - def check(self, expected): - """Wait until the JS result arrived and compare it.""" - if self._result is self.UNSET: - with self._qtbot.waitSignal(self.got_result, timeout=2000): - pass - assert self._result == expected diff --git a/tests/unit/browser/test_adblock.py b/tests/unit/browser/test_adblock.py index 8ab3b930d..aaa36d20b 100644 --- a/tests/unit/browser/test_adblock.py +++ b/tests/unit/browser/test_adblock.py @@ -28,12 +28,13 @@ import pytest from PyQt5.QtCore import QUrl from qutebrowser.browser import adblock +from qutebrowser.utils import urlmatch pytestmark = pytest.mark.usefixtures('qapp', 'config_tmpdir') # TODO See ../utils/test_standarddirutils for OSError and caplog assertion -WHITELISTED_HOSTS = ('qutebrowser.org', 'mediumhost.io') +WHITELISTED_HOSTS = ('qutebrowser.org', 'mediumhost.io', 'http://*.edu') BLOCKLIST_HOSTS = ('localhost', 'mediumhost.io', @@ -50,7 +51,8 @@ URLS_TO_CHECK = ('http://localhost', 'http://ads.worsthostever.net', 'http://goodhost.gov', 'ftp://verygoodhost.com', - 'http://qutebrowser.org') + 'http://qutebrowser.org', + 'http://veryverygoodhost.edu') class BaseDirStub: @@ -215,6 +217,23 @@ def test_disabled_blocking_update(basedir, config_stub, download_stub, assert not host_blocker.is_blocked(QUrl(str_url)) +def test_disabled_blocking_per_url(config_stub, data_tmpdir): + example_com = 'https://www.example.com/' + + config_stub.val.content.host_blocking.lists = [] + pattern = urlmatch.UrlPattern(example_com) + config_stub.set_obj('content.host_blocking.enabled', False, + pattern=pattern) + + url = QUrl('blocked.example.com') + + host_blocker = adblock.HostBlocker() + host_blocker._blocked_hosts.add(url.host()) + + assert host_blocker.is_blocked(url) + assert not host_blocker.is_blocked(url, first_party_url=QUrl(example_com)) + + def test_no_blocklist_update(config_stub, download_stub, data_tmpdir, basedir, tmpdir, win_registry): """Ensure no URL is blocked when no block list exists.""" @@ -259,6 +278,33 @@ def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub, assert_urls(host_blocker, whitelisted=[]) +@pytest.mark.parametrize('ip, host', [ + ('127.0.0.1', 'localhost'), + ('27.0.0.1', 'localhost.localdomain'), + ('27.0.0.1', 'local'), + ('55.255.255.255', 'broadcasthost'), + (':1', 'localhost'), + (':1', 'ip6-localhost'), + (':1', 'ip6-loopback'), + ('e80::1%lo0', 'localhost'), + ('f00::0', 'ip6-localnet'), + ('f00::0', 'ip6-mcastprefix'), + ('f02::1', 'ip6-allnodes'), + ('f02::2', 'ip6-allrouters'), + ('ff02::3', 'ip6-allhosts'), + ('.0.0.0', '0.0.0.0'), + ('127.0.1.1', 'myhostname'), + ('127.0.0.53', 'myhostname'), +]) +def test_whitelisted_lines(config_stub, basedir, download_stub, data_tmpdir, + tmpdir, win_registry, caplog, ip, host): + """Make sure we don't block hosts we don't want to.""" + host_blocker = adblock.HostBlocker() + line = ('{} {}'.format(ip, host)).encode('ascii') + host_blocker._parse_line(line) + assert host not in host_blocker._blocked_hosts + + def test_failed_dl_update(config_stub, basedir, download_stub, data_tmpdir, tmpdir, win_registry, caplog): """One blocklist fails to download. @@ -402,7 +448,26 @@ def test_config_change(config_stub, basedir, download_stub, host_blocker = adblock.HostBlocker() host_blocker.read_hosts() - config_stub.set_obj('content.host_blocking.lists', None) + config_stub.val.content.host_blocking.lists = None host_blocker.read_hosts() for str_url in URLS_TO_CHECK: assert not host_blocker.is_blocked(QUrl(str_url)) + + +def test_add_directory(config_stub, basedir, download_stub, + data_tmpdir, tmpdir): + """Ensure adblocker can import all files in a directory.""" + blocklist_hosts2 = [] + for i in BLOCKLIST_HOSTS[1:]: + blocklist_hosts2.append('1' + i) + + create_blocklist(tmpdir, blocked_hosts=BLOCKLIST_HOSTS, + name='blocked-hosts', line_format='one_per_line') + create_blocklist(tmpdir, blocked_hosts=blocklist_hosts2, + name='blocked-hosts2', line_format='one_per_line') + + config_stub.val.content.host_blocking.lists = [tmpdir.strpath] + config_stub.val.content.host_blocking.enabled = True + host_blocker = adblock.HostBlocker() + host_blocker.adblock_update() + assert len(host_blocker._blocked_hosts) == len(blocklist_hosts2) * 2 diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py new file mode 100644 index 000000000..6165546e5 --- /dev/null +++ b/tests/unit/browser/test_caret.py @@ -0,0 +1,370 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for caret browsing mode.""" + +import textwrap + +import pytest +from PyQt5.QtCore import QUrl + +from qutebrowser.utils import usertypes, qtutils + + +@pytest.fixture +def caret(web_tab, qtbot, mode_manager): + with qtbot.wait_signal(web_tab.load_finished): + web_tab.openurl(QUrl('qute://testdata/data/caret.html')) + + mode_manager.enter(usertypes.KeyMode.caret) + + return web_tab.caret + + +class Selection: + + """Helper to interact with the caret selection.""" + + def __init__(self, qtbot, caret): + self._qtbot = qtbot + self._caret = caret + + def check(self, expected, *, strip=False): + """Check whether we got the expected selection. + + Since (especially on Windows) the selection is empty if we're checking + too quickly, we try to read it multiple times. + """ + for _ in range(10): + with self._qtbot.wait_callback() as callback: + self._caret.selection(callback) + + selection = callback.args[0] + if selection: + if strip: + selection = selection.strip() + assert selection == expected + return + + self._qtbot.wait(50) + + def check_multiline(self, expected, *, strip=False): + self.check(textwrap.dedent(expected).strip(), strip=strip) + + def toggle(self): + with self._qtbot.wait_signal(self._caret.selection_toggled): + self._caret.toggle_selection() + + +@pytest.fixture +def selection(qtbot, caret): + return Selection(qtbot, caret) + + +class TestDocument: + + def test_selecting_entire_document(self, caret, selection): + selection.toggle() + caret.move_to_end_of_document() + selection.check_multiline(""" + one two three + eins zwei drei + + four five six + vier fünf sechs + """, strip=True) + + def test_moving_to_end_and_start(self, caret, selection): + caret.move_to_end_of_document() + caret.move_to_start_of_document() + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + def test_moving_to_end_and_start_with_selection(self, caret, selection): + caret.move_to_end_of_document() + selection.toggle() + caret.move_to_start_of_document() + selection.check_multiline(""" + one two three + eins zwei drei + + four five six + vier fünf sechs + """, strip=True) + + +class TestBlock: + + def test_selecting_block(self, caret, selection): + selection.toggle() + caret.move_to_end_of_next_block() + selection.check_multiline(""" + one two three + eins zwei drei + """) + + def test_moving_back_to_the_end_of_prev_block_with_sel(self, caret, selection): + caret.move_to_end_of_next_block(2) + selection.toggle() + caret.move_to_end_of_prev_block() + caret.move_to_prev_word() + selection.check_multiline(""" + drei + + four five six + """) + + def test_moving_back_to_the_end_of_prev_block(self, caret, selection): + caret.move_to_end_of_next_block(2) + caret.move_to_end_of_prev_block() + selection.toggle() + caret.move_to_prev_word() + selection.check("drei") + + def test_moving_back_to_the_start_of_prev_block_with_sel(self, caret, selection): + caret.move_to_end_of_next_block(2) + selection.toggle() + caret.move_to_start_of_prev_block() + selection.check_multiline(""" + eins zwei drei + + four five six + """) + + def test_moving_back_to_the_start_of_prev_block(self, caret, selection): + caret.move_to_end_of_next_block(2) + caret.move_to_start_of_prev_block() + selection.toggle() + caret.move_to_next_word() + selection.check("eins ") + + def test_moving_to_the_start_of_next_block_with_sel(self, caret, selection): + selection.toggle() + caret.move_to_start_of_next_block() + selection.check("one two three\n") + + def test_moving_to_the_start_of_next_block(self, caret, selection): + caret.move_to_start_of_next_block() + selection.toggle() + caret.move_to_end_of_word() + selection.check("eins") + + +class TestLine: + + def test_selecting_a_line(self, caret, selection): + selection.toggle() + caret.move_to_end_of_line() + selection.check("one two three") + + def test_moving_and_selecting_a_line(self, caret, selection): + caret.move_to_next_line() + selection.toggle() + caret.move_to_end_of_line() + selection.check("eins zwei drei") + + def test_selecting_next_line(self, caret, selection): + selection.toggle() + caret.move_to_next_line() + selection.check("one two three\n") + + def test_moving_to_end_and_to_start_of_line(self, caret, selection): + caret.move_to_end_of_line() + caret.move_to_start_of_line() + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + def test_selecting_a_line_backwards(self, caret, selection): + caret.move_to_end_of_line() + selection.toggle() + caret.move_to_start_of_line() + selection.check("one two three") + + def test_selecting_previous_line(self, caret, selection): + caret.move_to_next_line() + selection.toggle() + caret.move_to_prev_line() + selection.check("one two three\n") + + def test_moving_to_previous_line(self, caret, selection): + caret.move_to_next_line() + caret.move_to_prev_line() + selection.toggle() + caret.move_to_next_line() + selection.check("one two three\n") + + +class TestWord: + + def test_selecting_a_word(self, caret, selection): + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + def test_moving_to_end_and_selecting_a_word(self, caret, selection): + caret.move_to_end_of_word() + selection.toggle() + caret.move_to_end_of_word() + selection.check(" two") + + def test_moving_to_next_word_and_selecting_a_word(self, caret, selection): + caret.move_to_next_word() + selection.toggle() + caret.move_to_end_of_word() + selection.check("two") + + def test_moving_to_next_word_and_selecting_until_next_word(self, caret, selection): + caret.move_to_next_word() + selection.toggle() + caret.move_to_next_word() + selection.check("two ") + + def test_moving_to_previous_word_and_selecting_a_word(self, caret, selection): + caret.move_to_end_of_word() + selection.toggle() + caret.move_to_prev_word() + selection.check("one") + + def test_moving_to_previous_word(self, caret, selection): + caret.move_to_end_of_word() + caret.move_to_prev_word() + selection.toggle() + caret.move_to_end_of_word() + selection.check("one") + + +class TestChar: + + def test_selecting_a_char(self, caret, selection): + selection.toggle() + caret.move_to_next_char() + selection.check("o") + + def test_moving_and_selecting_a_char(self, caret, selection): + caret.move_to_next_char() + selection.toggle() + caret.move_to_next_char() + selection.check("n") + + def test_selecting_previous_char(self, caret, selection): + caret.move_to_end_of_word() + selection.toggle() + caret.move_to_prev_char() + selection.check("e") + + def test_moving_to_previous_char(self, caret, selection): + caret.move_to_end_of_word() + caret.move_to_prev_char() + selection.toggle() + caret.move_to_end_of_word() + selection.check("e") + + +def test_drop_selection(caret, selection): + selection.toggle() + caret.move_to_end_of_word() + caret.drop_selection() + selection.check("") + + +class TestSearch: + + # https://bugreports.qt.io/browse/QTBUG-60673 + + @pytest.mark.qtbug60673 + @pytest.mark.no_xvfb + def test_yanking_a_searched_line(self, caret, selection, mode_manager, web_tab, qtbot): + web_tab.show() + mode_manager.leave(usertypes.KeyMode.caret) + + with qtbot.wait_callback() as callback: + web_tab.search.search('fiv', result_cb=callback) + callback.assert_called_with(True) + + mode_manager.enter(usertypes.KeyMode.caret) + caret.move_to_end_of_line() + selection.check('five six') + + @pytest.mark.qtbug60673 + @pytest.mark.no_xvfb + def test_yanking_a_searched_line_with_multiple_matches(self, caret, selection, mode_manager, web_tab, qtbot): + web_tab.show() + mode_manager.leave(usertypes.KeyMode.caret) + + with qtbot.wait_callback() as callback: + web_tab.search.search('w', result_cb=callback) + callback.assert_called_with(True) + + with qtbot.wait_callback() as callback: + web_tab.search.next_result(result_cb=callback) + callback.assert_called_with(True) + + mode_manager.enter(usertypes.KeyMode.caret) + + caret.move_to_end_of_line() + selection.check('wei drei') + + +class TestFollowSelected: + + LOAD_STARTED_DELAY = 50 + + @pytest.fixture(params=[True, False], autouse=True) + def toggle_js(self, request, config_stub): + config_stub.val.content.javascript.enabled = request.param + + @pytest.fixture(autouse=True) + def expose(self, web_tab): + """Expose the web view if needed. + + On QtWebKit, or Qt < 5.11 on QtWebEngine, we need to show the tab for + selections to work properly. + """ + if (web_tab.backend == usertypes.Backend.QtWebKit or + not qtutils.version_check('5.11', compiled=False)): + web_tab.container.expose() + + def test_follow_selected_without_a_selection(self, qtbot, caret, selection, web_tab, + mode_manager): + caret.move_to_next_word() # Move cursor away from the link + mode_manager.leave(usertypes.KeyMode.caret) + with qtbot.wait_signal(caret.follow_selected_done): + with qtbot.assert_not_emitted(web_tab.load_started, + wait=self.LOAD_STARTED_DELAY): + caret.follow_selected() + + def test_follow_selected_with_text(self, qtbot, caret, selection, web_tab): + caret.move_to_next_word() + selection.toggle() + caret.move_to_end_of_word() + with qtbot.wait_signal(caret.follow_selected_done): + with qtbot.assert_not_emitted(web_tab.load_started, + wait=self.LOAD_STARTED_DELAY): + caret.follow_selected() + + def test_follow_selected_with_link(self, caret, selection, config_stub, + qtbot, web_tab): + selection.toggle() + caret.move_to_end_of_word() + with qtbot.wait_signal(web_tab.load_finished): + with qtbot.wait_signal(caret.follow_selected_done): + caret.follow_selected() + assert web_tab.url().path() == '/data/hello.txt' diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 43476d7e0..5b84eac4c 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -37,410 +37,450 @@ def prerequisites(config_stub, fake_save_manager, init_sql, fake_args): config_stub.data = {'general': {'private-browsing': False}} -@pytest.fixture() -def hist(tmpdir): - return history.WebHistory() +class TestSpecialMethods: + + def test_iter(self, web_history): + urlstr = 'http://www.example.com/' + url = QUrl(urlstr) + web_history.add_url(url, atime=12345) + + assert list(web_history) == [(urlstr, '', 12345, False)] + + def test_len(self, web_history): + assert len(web_history) == 0 + + url = QUrl('http://www.example.com/') + web_history.add_url(url) + + assert len(web_history) == 1 + + def test_contains(self, web_history): + web_history.add_url(QUrl('http://www.example.com/'), + title='Title', atime=12345) + assert 'http://www.example.com/' in web_history + assert 'www.example.com' not in web_history + assert 'Title' not in web_history + assert 12345 not in web_history -@pytest.fixture() -def mock_time(mocker): - m = mocker.patch('qutebrowser.browser.history.time') - m.time.return_value = 12345 - return 12345 +class TestGetting: + + def test_get_recent(self, web_history): + web_history.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) + web_history.add_url(QUrl('http://example.com/'), atime=12345) + assert list(web_history.get_recent()) == [ + ('http://www.qutebrowser.org/', '', 67890, False), + ('http://example.com/', '', 12345, False), + ] + + def test_entries_between(self, web_history): + web_history.add_url(QUrl('http://www.example.com/1'), atime=12345) + web_history.add_url(QUrl('http://www.example.com/2'), atime=12346) + web_history.add_url(QUrl('http://www.example.com/3'), atime=12347) + web_history.add_url(QUrl('http://www.example.com/4'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/5'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/6'), atime=12349) + web_history.add_url(QUrl('http://www.example.com/7'), atime=12350) + + times = [x.atime for x in web_history.entries_between(12346, 12349)] + assert times == [12349, 12348, 12348, 12347] + + def test_entries_before(self, web_history): + web_history.add_url(QUrl('http://www.example.com/1'), atime=12346) + web_history.add_url(QUrl('http://www.example.com/2'), atime=12346) + web_history.add_url(QUrl('http://www.example.com/3'), atime=12347) + web_history.add_url(QUrl('http://www.example.com/4'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/5'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/6'), atime=12348) + web_history.add_url(QUrl('http://www.example.com/7'), atime=12349) + web_history.add_url(QUrl('http://www.example.com/8'), atime=12349) + + times = [x.atime for x in + web_history.entries_before(12348, limit=3, offset=2)] + assert times == [12348, 12347, 12346] -def test_iter(hist): - urlstr = 'http://www.example.com/' - url = QUrl(urlstr) - hist.add_url(url, atime=12345) +class TestDelete: - assert list(hist) == [(urlstr, '', 12345, False)] + def test_clear(self, qtbot, tmpdir, web_history, mocker): + web_history.add_url(QUrl('http://example.com/')) + web_history.add_url(QUrl('http://www.qutebrowser.org/')) + + m = mocker.patch('qutebrowser.browser.history.message.confirm_async', + new=mocker.Mock, spec=[]) + web_history.clear() + assert m.called + + def test_clear_force(self, qtbot, tmpdir, web_history): + web_history.add_url(QUrl('http://example.com/')) + web_history.add_url(QUrl('http://www.qutebrowser.org/')) + web_history.clear(force=True) + assert not len(web_history) + assert not len(web_history.completion) + + @pytest.mark.parametrize('raw, escaped', [ + ('http://example.com/1', 'http://example.com/1'), + ('http://example.com/1 2', 'http://example.com/1%202'), + ]) + def test_delete_url(self, web_history, raw, escaped): + web_history.add_url(QUrl('http://example.com/'), atime=0) + web_history.add_url(QUrl(escaped), atime=0) + web_history.add_url(QUrl('http://example.com/2'), atime=0) + + before = set(web_history) + completion_before = set(web_history.completion) + + web_history.delete_url(QUrl(raw)) + + diff = before.difference(set(web_history)) + assert diff == {(escaped, '', 0, False)} + + completion_diff = completion_before.difference( + set(web_history.completion)) + assert completion_diff == {(raw, '', 0)} -def test_len(hist): - assert len(hist) == 0 +class TestAdd: - url = QUrl('http://www.example.com/') - hist.add_url(url) + @pytest.fixture() + def mock_time(self, mocker): + m = mocker.patch('qutebrowser.browser.history.time') + m.time.return_value = 12345 + return 12345 - assert len(hist) == 1 - - -def test_contains(hist): - hist.add_url(QUrl('http://www.example.com/'), title='Title', atime=12345) - assert 'http://www.example.com/' in hist - assert 'www.example.com' not in hist - assert 'Title' not in hist - assert 12345 not in hist - - -def test_get_recent(hist): - hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890) - hist.add_url(QUrl('http://example.com/'), atime=12345) - assert list(hist.get_recent()) == [ - ('http://www.qutebrowser.org/', '', 67890, False), - ('http://example.com/', '', 12345, False), - ] - - -def test_entries_between(hist): - hist.add_url(QUrl('http://www.example.com/1'), atime=12345) - hist.add_url(QUrl('http://www.example.com/2'), atime=12346) - hist.add_url(QUrl('http://www.example.com/3'), atime=12347) - hist.add_url(QUrl('http://www.example.com/4'), atime=12348) - hist.add_url(QUrl('http://www.example.com/5'), atime=12348) - hist.add_url(QUrl('http://www.example.com/6'), atime=12349) - hist.add_url(QUrl('http://www.example.com/7'), atime=12350) - - times = [x.atime for x in hist.entries_between(12346, 12349)] - assert times == [12349, 12348, 12348, 12347] - - -def test_entries_before(hist): - hist.add_url(QUrl('http://www.example.com/1'), atime=12346) - hist.add_url(QUrl('http://www.example.com/2'), atime=12346) - hist.add_url(QUrl('http://www.example.com/3'), atime=12347) - hist.add_url(QUrl('http://www.example.com/4'), atime=12348) - hist.add_url(QUrl('http://www.example.com/5'), atime=12348) - hist.add_url(QUrl('http://www.example.com/6'), atime=12348) - hist.add_url(QUrl('http://www.example.com/7'), atime=12349) - hist.add_url(QUrl('http://www.example.com/8'), atime=12349) - - times = [x.atime for x in hist.entries_before(12348, limit=3, offset=2)] - assert times == [12348, 12347, 12346] - - -def test_clear(qtbot, tmpdir, hist, mocker): - hist.add_url(QUrl('http://example.com/')) - hist.add_url(QUrl('http://www.qutebrowser.org/')) - - m = mocker.patch('qutebrowser.browser.history.message.confirm_async', - new=mocker.Mock, spec=[]) - hist.clear() - assert m.called - - -def test_clear_force(qtbot, tmpdir, hist): - hist.add_url(QUrl('http://example.com/')) - hist.add_url(QUrl('http://www.qutebrowser.org/')) - hist.clear(force=True) - assert not len(hist) - assert not len(hist.completion) - - -@pytest.mark.parametrize('raw, escaped', [ - ('http://example.com/1', 'http://example.com/1'), - ('http://example.com/1 2', 'http://example.com/1%202'), -]) -def test_delete_url(hist, raw, escaped): - hist.add_url(QUrl('http://example.com/'), atime=0) - hist.add_url(QUrl(escaped), atime=0) - hist.add_url(QUrl('http://example.com/2'), atime=0) - - before = set(hist) - completion_before = set(hist.completion) - - hist.delete_url(QUrl(raw)) - - diff = before.difference(set(hist)) - assert diff == {(escaped, '', 0, False)} - - completion_diff = completion_before.difference(set(hist.completion)) - assert completion_diff == {(raw, '', 0)} - - -@pytest.mark.parametrize( - 'url, atime, title, redirect, history_url, completion_url', [ - - ('http://www.example.com', 12346, 'the title', False, - 'http://www.example.com', 'http://www.example.com'), - ('http://www.example.com', 12346, 'the title', True, - 'http://www.example.com', None), - ('http://www.example.com/sp ce', 12346, 'the title', False, - 'http://www.example.com/sp%20ce', 'http://www.example.com/sp ce'), - ('https://user:pass@example.com', 12346, 'the title', False, - 'https://user@example.com', 'https://user@example.com'), - ] -) -def test_add_url(qtbot, hist, url, atime, title, redirect, history_url, - completion_url): - hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect) - assert list(hist) == [(history_url, title, atime, redirect)] - if completion_url is None: - assert not len(hist.completion) - else: - assert list(hist.completion) == [(completion_url, title, atime)] - - -def test_add_url_invalid(qtbot, hist, caplog): - with caplog.at_level(logging.WARNING): - hist.add_url(QUrl()) - assert not list(hist) - assert not list(hist.completion) - - -@pytest.mark.parametrize('environmental', [True, False]) -@pytest.mark.parametrize('completion', [True, False]) -def test_add_url_error(monkeypatch, hist, message_mock, caplog, - environmental, completion): - def raise_error(url, replace=False): - raise sql.SqlError("Error message", environmental=environmental) - - if completion: - monkeypatch.setattr(hist.completion, 'insert', raise_error) - else: - monkeypatch.setattr(hist, 'insert', raise_error) - - if environmental: - with caplog.at_level(logging.ERROR): - hist.add_url(QUrl('https://www.example.org/')) - msg = message_mock.getmsg(usertypes.MessageLevel.error) - assert msg.text == "Failed to write history: Error message" - else: - with pytest.raises(sql.SqlError): - hist.add_url(QUrl('https://www.example.org/')) - - -@pytest.mark.parametrize('level, url, req_url, expected', [ - (logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]), - (logging.DEBUG, 'a.com', 'b.com', [('a.com', 'title', 12345, False), - ('b.com', 'title', 12345, True)]), - (logging.WARNING, 'a.com', '', [('a.com', 'title', 12345, False)]), - (logging.WARNING, '', '', []), - (logging.WARNING, 'data:foo', '', []), - (logging.WARNING, 'a.com', 'data:foo', []), -]) -def test_add_from_tab(hist, level, url, req_url, expected, mock_time, caplog): - with caplog.at_level(level): - hist.add_from_tab(QUrl(url), QUrl(req_url), 'title') - assert set(hist) == set(expected) - - -@pytest.fixture -def hist_interface(hist): - # pylint: disable=invalid-name - QtWebKit = pytest.importorskip('PyQt5.QtWebKit') - from qutebrowser.browser.webkit import webkithistory - QWebHistoryInterface = QtWebKit.QWebHistoryInterface - # pylint: enable=invalid-name - hist.add_url(url=QUrl('http://www.example.com/'), title='example') - interface = webkithistory.WebHistoryInterface(hist) - QWebHistoryInterface.setDefaultInterface(interface) - yield - QWebHistoryInterface.setDefaultInterface(None) - - -def test_history_interface(qtbot, webview, hist_interface): - html = b"foo" - url = urlutils.data_url('text/html', html) - with qtbot.waitSignal(webview.loadFinished): - webview.load(url) - - -@pytest.fixture -def cleanup_init(): - # prevent test_init from leaking state - yield - hist = objreg.get('web-history', None) - if hist is not None: - hist.setParent(None) - objreg.delete('web-history') - try: - from PyQt5.QtWebKit import QWebHistoryInterface - QWebHistoryInterface.setDefaultInterface(None) - except ImportError: - pass - - -@pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine, - usertypes.Backend.QtWebKit]) -def test_init(backend, qapp, tmpdir, monkeypatch, cleanup_init): - if backend == usertypes.Backend.QtWebKit: - pytest.importorskip('PyQt5.QtWebKitWidgets') - else: - assert backend == usertypes.Backend.QtWebEngine - - monkeypatch.setattr(history.objects, 'backend', backend) - history.init(qapp) - hist = objreg.get('web-history') - assert hist.parent() is qapp - - try: - from PyQt5.QtWebKit import QWebHistoryInterface - except ImportError: - QWebHistoryInterface = None - - if backend == usertypes.Backend.QtWebKit: - default_interface = QWebHistoryInterface.defaultInterface() - assert default_interface._history is hist - else: - assert backend == usertypes.Backend.QtWebEngine - if QWebHistoryInterface is None: - default_interface = None + @pytest.mark.parametrize( + 'url, atime, title, redirect, history_url, completion_url', [ + ('http://www.example.com', 12346, 'the title', False, + 'http://www.example.com', 'http://www.example.com'), + ('http://www.example.com', 12346, 'the title', True, + 'http://www.example.com', None), + ('http://www.example.com/sp ce', 12346, 'the title', False, + 'http://www.example.com/sp%20ce', 'http://www.example.com/sp ce'), + ('https://user:pass@example.com', 12346, 'the title', False, + 'https://user@example.com', 'https://user@example.com'), + ] + ) + def test_add_url(self, qtbot, web_history, + url, atime, title, redirect, history_url, completion_url): + web_history.add_url(QUrl(url), atime=atime, title=title, + redirect=redirect) + assert list(web_history) == [(history_url, title, atime, redirect)] + if completion_url is None: + assert not len(web_history.completion) else: + expected = [(completion_url, title, atime)] + assert list(web_history.completion) == expected + + def test_no_sql_web_history(self, web_history, fake_args): + fake_args.debug_flags = 'no-sql-history' + web_history.add_url(QUrl('https://www.example.com/'), atime=12346, + title='Hello World', redirect=False) + assert not list(web_history) + + def test_invalid(self, qtbot, web_history, caplog): + with caplog.at_level(logging.WARNING): + web_history.add_url(QUrl()) + assert not list(web_history) + assert not list(web_history.completion) + + @pytest.mark.parametrize('environmental', [True, False]) + @pytest.mark.parametrize('completion', [True, False]) + def test_error(self, monkeypatch, web_history, message_mock, caplog, + environmental, completion): + def raise_error(url, replace=False): + if environmental: + raise sql.SqlEnvironmentError("Error message") + else: + raise sql.SqlBugError("Error message") + + if completion: + monkeypatch.setattr(web_history.completion, 'insert', raise_error) + else: + monkeypatch.setattr(web_history, 'insert', raise_error) + + if environmental: + with caplog.at_level(logging.ERROR): + web_history.add_url(QUrl('https://www.example.org/')) + msg = message_mock.getmsg(usertypes.MessageLevel.error) + assert msg.text == "Failed to write history: Error message" + else: + with pytest.raises(sql.SqlBugError): + web_history.add_url(QUrl('https://www.example.org/')) + + @pytest.mark.parametrize('level, url, req_url, expected', [ + (logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]), + (logging.DEBUG, 'a.com', 'b.com', [('a.com', 'title', 12345, False), + ('b.com', 'title', 12345, True)]), + (logging.WARNING, 'a.com', '', [('a.com', 'title', 12345, False)]), + (logging.WARNING, '', '', []), + (logging.WARNING, 'data:foo', '', []), + (logging.WARNING, 'a.com', 'data:foo', []), + ]) + def test_from_tab(self, web_history, caplog, mock_time, + level, url, req_url, expected): + with caplog.at_level(level): + web_history.add_from_tab(QUrl(url), QUrl(req_url), 'title') + assert set(web_history) == set(expected) + + def test_exclude(self, web_history, config_stub): + """Excluded URLs should be in the history but not completion.""" + config_stub.val.completion.web_history.exclude = ['*.example.org'] + url = QUrl('http://www.example.org/') + web_history.add_from_tab(url, url, 'title') + assert list(web_history) + assert not list(web_history.completion) + + +class TestHistoryInterface: + + @pytest.fixture + def hist_interface(self, web_history): + # pylint: disable=invalid-name + QtWebKit = pytest.importorskip('PyQt5.QtWebKit') + from qutebrowser.browser.webkit import webkithistory + QWebHistoryInterface = QtWebKit.QWebHistoryInterface + # pylint: enable=invalid-name + web_history.add_url(url=QUrl('http://www.example.com/'), + title='example') + interface = webkithistory.WebHistoryInterface(web_history) + QWebHistoryInterface.setDefaultInterface(interface) + yield + QWebHistoryInterface.setDefaultInterface(None) + + def test_history_interface(self, qtbot, webview, hist_interface): + html = b"foo" + url = urlutils.data_url('text/html', html) + with qtbot.waitSignal(webview.loadFinished): + webview.load(url) + + +class TestInit: + + @pytest.fixture + def cleanup_init(self): + # prevent test_init from leaking state + yield + web_history = objreg.get('web-history', None) + if web_history is not None: + web_history.setParent(None) + objreg.delete('web-history') + try: + from PyQt5.QtWebKit import QWebHistoryInterface + QWebHistoryInterface.setDefaultInterface(None) + except ImportError: + pass + + @pytest.mark.parametrize('backend', [usertypes.Backend.QtWebEngine, + usertypes.Backend.QtWebKit]) + def test_init(self, backend, qapp, tmpdir, monkeypatch, cleanup_init): + if backend == usertypes.Backend.QtWebKit: + pytest.importorskip('PyQt5.QtWebKitWidgets') + else: + assert backend == usertypes.Backend.QtWebEngine + + monkeypatch.setattr(history.objects, 'backend', backend) + history.init(qapp) + hist = objreg.get('web-history') + assert hist.parent() is qapp + + try: + from PyQt5.QtWebKit import QWebHistoryInterface + except ImportError: + QWebHistoryInterface = None + + if backend == usertypes.Backend.QtWebKit: default_interface = QWebHistoryInterface.defaultInterface() - # For this to work, nothing can ever have called setDefaultInterface - # before (so we need to test webengine before webkit) - assert default_interface is None + assert default_interface._history is hist + else: + assert backend == usertypes.Backend.QtWebEngine + if QWebHistoryInterface is None: + default_interface = None + else: + default_interface = QWebHistoryInterface.defaultInterface() + # For this to work, nothing can ever have called + # setDefaultInterface before (so we need to test webengine before + # webkit) + assert default_interface is None -def test_import_txt(hist, data_tmpdir, monkeypatch, stubs): - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - # empty line is deliberate, to test skipping empty lines - histfile.write('''12345 http://example.com/ title - 12346 http://qutebrowser.org/ - 67890 http://example.com/path +class TestDump: - 68891-r http://example.com/path/other ''') + def test_debug_dump_history(self, web_history, tmpdir): + web_history.add_url(QUrl('http://example.com/1'), + title="Title1", atime=12345) + web_history.add_url(QUrl('http://example.com/2'), + title="Title2", atime=12346) + web_history.add_url(QUrl('http://example.com/3'), + title="Title3", atime=12347) + web_history.add_url(QUrl('http://example.com/4'), + title="Title4", atime=12348, redirect=True) + histfile = tmpdir / 'history' + web_history.debug_dump_history(str(histfile)) + expected = ['12345 http://example.com/1 Title1', + '12346 http://example.com/2 Title2', + '12347 http://example.com/3 Title3', + '12348-r http://example.com/4 Title4'] + assert histfile.read() == '\n'.join(expected) - hist.import_txt() - - assert list(hist) == [ - ('http://example.com/', 'title', 12345, False), - ('http://qutebrowser.org/', '', 12346, False), - ('http://example.com/path', '', 67890, False), - ('http://example.com/path/other', '', 68891, True) - ] - - assert not histfile.exists() - assert (data_tmpdir / 'history.bak').exists() + def test_nonexistent(self, web_history, tmpdir): + histfile = tmpdir / 'nonexistent' / 'history' + with pytest.raises(cmdexc.CommandError): + web_history.debug_dump_history(str(histfile)) -def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs): - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - bakfile = data_tmpdir / 'history.bak' - histfile.write('12345 http://example.com/ title') - bakfile.write('12346 http://qutebrowser.org/') +class TestRebuild: - hist.import_txt() + def test_delete(self, web_history, stubs): + web_history.insert({'url': 'example.com/1', 'title': 'example1', + 'redirect': False, 'atime': 1}) + web_history.insert({'url': 'example.com/1', 'title': 'example1', + 'redirect': False, 'atime': 2}) + web_history.insert({'url': 'example.com/2%203', 'title': 'example2', + 'redirect': False, 'atime': 3}) + web_history.insert({'url': 'example.com/3', 'title': 'example3', + 'redirect': True, 'atime': 4}) + web_history.insert({'url': 'example.com/2 3', 'title': 'example2', + 'redirect': False, 'atime': 5}) + web_history.completion.delete_all() - assert list(hist) == [('http://example.com/', 'title', 12345, False)] + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist2.completion) == [ + ('example.com/1', 'example1', 2), + ('example.com/2 3', 'example2', 5), + ] - assert not histfile.exists() - assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/', - '12345 http://example.com/ title'] + def test_no_rebuild(self, web_history, stubs): + """Ensure that completion is not regenerated unless empty.""" + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') + + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist2.completion) == [('example.com/1', '', 1)] + + def test_user_version(self, web_history, stubs, monkeypatch): + """Ensure that completion is regenerated if user_version changes.""" + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') + + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist2.completion) == [('example.com/1', '', 1)] + + monkeypatch.setattr(history, '_USER_VERSION', + history._USER_VERSION + 1) + hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist3.completion) == [ + ('example.com/1', '', 1), + ('example.com/2', '', 2), + ] + + def test_force_rebuild(self, web_history, stubs): + """Ensure that completion is regenerated if we force a rebuild.""" + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.completion.delete('url', 'example.com/2') + + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist2.completion) == [('example.com/1', '', 1)] + hist2.metainfo['force_rebuild'] = True + + hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist3.completion) == [ + ('example.com/1', '', 1), + ('example.com/2', '', 2), + ] + assert not hist3.metainfo['force_rebuild'] + + def test_exclude(self, config_stub, web_history, stubs): + """Ensure that patterns in completion.web_history.exclude are ignored. + + This setting should only be used for the completion. + """ + config_stub.val.completion.web_history.exclude = ['*.example.org'] + assert web_history.metainfo['force_rebuild'] + + web_history.add_url(QUrl('http://example.com'), + redirect=False, atime=1) + web_history.add_url(QUrl('http://example.org'), + redirect=False, atime=2) + + hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress()) + assert list(hist2.completion) == [('http://example.com', '', 1)] + + def test_unrelated_config_change(self, config_stub, web_history): + config_stub.val.history_gap_interval = 1234 + assert not web_history.metainfo['force_rebuild'] + + @pytest.mark.parametrize('patch_threshold', [True, False]) + def test_progress(self, web_history, config_stub, monkeypatch, stubs, + patch_threshold): + web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1) + web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2) + web_history.metainfo['force_rebuild'] = True + + if patch_threshold: + monkeypatch.setattr(history.WebHistory, '_PROGRESS_THRESHOLD', 1) + + progress = stubs.FakeHistoryProgress() + history.WebHistory(progress=progress) + assert progress._value == 2 + assert progress._finished + assert progress._started == patch_threshold -@pytest.mark.parametrize('line', [ - '', - '#12345 http://example.com/commented', +class TestCompletionMetaInfo: - # https://bugreports.qt.io/browse/QTBUG-60364 - '12345 http://.com/', - '12345 https://.com/', - '12345 http://www..com/', - '12345 https://www..com/', + @pytest.fixture + def metainfo(self): + return history.CompletionMetaInfo() - # issue #2646 - '12345 data:text/html;charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-', -]) -def test_import_txt_skip(hist, data_tmpdir, line, monkeypatch, stubs): - """import_txt should skip certain lines silently.""" - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - histfile.write(line) + def test_contains_keyerror(self, metainfo): + with pytest.raises(KeyError): + 'does_not_exist' in metainfo # pylint: disable=pointless-statement - hist.import_txt() + def test_getitem_keyerror(self, metainfo): + with pytest.raises(KeyError): + metainfo['does_not_exist'] # pylint: disable=pointless-statement - assert not histfile.exists() - assert not len(hist) + def test_setitem_keyerror(self, metainfo): + with pytest.raises(KeyError): + metainfo['does_not_exist'] = 42 + + def test_contains(self, metainfo): + assert 'force_rebuild' in metainfo + + def test_modify(self, metainfo): + assert not metainfo['force_rebuild'] + metainfo['force_rebuild'] = True + assert metainfo['force_rebuild'] -@pytest.mark.parametrize('line', [ - 'xyz http://example.com/bad-timestamp', - '12345', - 'http://example.com/no-timestamp', - '68891-r-r http://example.com/double-flag', - '68891-x http://example.com/bad-flag', - '68891 http://.com', -]) -def test_import_txt_invalid(hist, data_tmpdir, line, monkeypatch, stubs, - caplog): - """import_txt should fail on certain lines.""" - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - histfile = data_tmpdir / 'history' - histfile.write(line) +class TestHistoryProgress: - with caplog.at_level(logging.ERROR): - hist.import_txt() + @pytest.fixture + def progress(self): + return history.HistoryProgress() - assert any(rec.msg.startswith("Failed to import history:") - for rec in caplog.records) + def test_no_start(self, progress): + """Test calling tick/finish without start.""" + progress.tick() + progress.finish() + assert progress._progress is None + assert progress._value == 1 - assert histfile.exists() + def test_gui(self, qtbot, progress): + progress.start("Hello World", 42) + dialog = progress._progress + qtbot.add_widget(dialog) + progress.tick() + assert dialog.isVisible() + assert dialog.labelText() == "Hello World" + assert dialog.minimum() == 0 + assert dialog.maximum() == 42 + assert dialog.value() == 1 + assert dialog.minimumDuration() == 500 -def test_import_txt_nonexistent(hist, data_tmpdir, monkeypatch, stubs): - """import_txt should do nothing if the history file doesn't exist.""" - monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer) - hist.import_txt() - - -def test_debug_dump_history(hist, tmpdir): - hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345) - hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346) - hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347) - hist.add_url(QUrl('http://example.com/4'), title="Title4", atime=12348, - redirect=True) - histfile = tmpdir / 'history' - hist.debug_dump_history(str(histfile)) - expected = ['12345 http://example.com/1 Title1', - '12346 http://example.com/2 Title2', - '12347 http://example.com/3 Title3', - '12348-r http://example.com/4 Title4'] - assert histfile.read() == '\n'.join(expected) - - -def test_debug_dump_history_nonexistent(hist, tmpdir): - histfile = tmpdir / 'nonexistent' / 'history' - with pytest.raises(cmdexc.CommandError): - hist.debug_dump_history(str(histfile)) - - -def test_rebuild_completion(hist): - hist.insert({'url': 'example.com/1', 'title': 'example1', - 'redirect': False, 'atime': 1}) - hist.insert({'url': 'example.com/1', 'title': 'example1', - 'redirect': False, 'atime': 2}) - hist.insert({'url': 'example.com/2%203', 'title': 'example2', - 'redirect': False, 'atime': 3}) - hist.insert({'url': 'example.com/3', 'title': 'example3', - 'redirect': True, 'atime': 4}) - hist.insert({'url': 'example.com/2 3', 'title': 'example2', - 'redirect': False, 'atime': 5}) - hist.completion.delete_all() - - hist2 = history.WebHistory() - assert list(hist2.completion) == [ - ('example.com/1', 'example1', 2), - ('example.com/2 3', 'example2', 5), - ] - - -def test_no_rebuild_completion(hist): - """Ensure that completion is not regenerated unless completely empty.""" - hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) - hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) - hist.completion.delete('url', 'example.com/2') - - hist2 = history.WebHistory() - assert list(hist2.completion) == [('example.com/1', '', 1)] - - -def test_user_version(hist, monkeypatch): - """Ensure that completion is regenerated if user_version is incremented.""" - hist.add_url(QUrl('example.com/1'), redirect=False, atime=1) - hist.add_url(QUrl('example.com/2'), redirect=False, atime=2) - hist.completion.delete('url', 'example.com/2') - - hist2 = history.WebHistory() - assert list(hist2.completion) == [('example.com/1', '', 1)] - - monkeypatch.setattr(history, '_USER_VERSION', history._USER_VERSION + 1) - hist3 = history.WebHistory() - assert list(hist3.completion) == [ - ('example.com/1', '', 1), - ('example.com/2', '', 2), - ] + progress.finish() + assert not dialog.isVisible() diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py index a33dae5bf..768180e11 100644 --- a/tests/unit/browser/test_pdfjs.py +++ b/tests/unit/browser/test_pdfjs.py @@ -17,51 +17,146 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import textwrap +import logging +import os.path import pytest from PyQt5.QtCore import QUrl from qutebrowser.browser import pdfjs +from qutebrowser.utils import usertypes, utils + + +pytestmark = [pytest.mark.usefixtures('data_tmpdir')] + + +@pytest.mark.parametrize('available, snippet', [ + (True, 'PDF.js viewer'), + (False, '

No pdf.js installation found

'), + ('force', 'fake PDF.js'), +]) +def test_generate_pdfjs_page(available, snippet, monkeypatch): + if available == 'force': + monkeypatch.setattr(pdfjs, 'is_available', lambda: True) + monkeypatch.setattr(pdfjs, 'get_pdfjs_res', + lambda filename: b'fake PDF.js') + elif available: + if not pdfjs.is_available(): + pytest.skip("PDF.js unavailable") + monkeypatch.setattr(pdfjs, 'is_available', lambda: True) + else: + monkeypatch.setattr(pdfjs, 'is_available', lambda: False) + + content = pdfjs.generate_pdfjs_page('example.pdf', QUrl()) + print(content) + assert snippet in content # Note that we got double protection, once because we use QUrl.FullyEncoded and -# because we use qutebrowser.utils.javascript.string_escape. Characters -# like " are already replaced by QUrl. -@pytest.mark.parametrize('url, expected', [ - ('http://foo.bar', "http://foo.bar"), - ('http://"', ''), - ('\0', '%00'), - ('http://foobar/");alert("attack!");', - 'http://foobar/%22);alert(%22attack!%22);'), +# because we use qutebrowser.utils.javascript.to_js. Characters like " are +# already replaced by QUrl. +@pytest.mark.parametrize('filename, expected', [ + ('foo.bar', "foo.bar"), + ('foo"bar', "foo%22bar"), + ('foo\0bar', 'foo%00bar'), + ('foobar");alert("attack!");', + 'foobar%22);alert(%22attack!%22);'), ]) -def test_generate_pdfjs_script(url, expected): - expected_open = 'open("{}");'.format(expected) - url = QUrl(url) - actual = pdfjs._generate_pdfjs_script(url) +def test_generate_pdfjs_script(filename, expected): + expected_open = 'open("qute://pdfjs/file?filename={}");'.format(expected) + actual = pdfjs._generate_pdfjs_script(filename) assert expected_open in actual assert 'PDFView' in actual -def test_fix_urls(): - page = textwrap.dedent(""" - - - - - - """).strip() +@pytest.mark.parametrize('qt, backend, expected', [ + ('new', usertypes.Backend.QtWebEngine, False), + ('new', usertypes.Backend.QtWebKit, False), + ('old', usertypes.Backend.QtWebEngine, True), + ('old', usertypes.Backend.QtWebKit, False), + ('5.7', usertypes.Backend.QtWebEngine, False), + ('5.7', usertypes.Backend.QtWebKit, False), +]) +def test_generate_pdfjs_script_disable_object_url(monkeypatch, + qt, backend, expected): + if qt == 'new': + monkeypatch.setattr(pdfjs.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + False if version == '5.7.1' else True) + elif qt == 'old': + monkeypatch.setattr(pdfjs.qtutils, 'version_check', + lambda version, exact=False, compiled=True: False) + elif qt == '5.7': + monkeypatch.setattr(pdfjs.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + True if version == '5.7.1' else False) + else: + raise utils.Unreachable - expected = textwrap.dedent(""" - - - - - - """).strip() + monkeypatch.setattr(pdfjs.objects, 'backend', backend) - actual = pdfjs.fix_urls(page) - assert actual == expected + script = pdfjs._generate_pdfjs_script('testfile') + assert ('PDFJS.disableCreateObjectURL' in script) == expected + + +class TestResources: + + @pytest.fixture + def read_system_mock(self, mocker): + return mocker.patch.object(pdfjs, '_read_from_system', autospec=True) + + @pytest.fixture + def read_file_mock(self, mocker): + return mocker.patch.object(pdfjs.utils, 'read_file', autospec=True) + + def test_get_pdfjs_res_system(self, read_system_mock): + read_system_mock.return_value = (b'content', 'path') + + assert pdfjs.get_pdfjs_res_and_path('web/test') == (b'content', 'path') + assert pdfjs.get_pdfjs_res('web/test') == b'content' + + read_system_mock.assert_called_with('/usr/share/pdf.js/', + ['web/test', 'test']) + + def test_get_pdfjs_res_bundled(self, read_system_mock, read_file_mock, + tmpdir): + read_system_mock.return_value = (None, None) + + read_file_mock.return_value = b'content' + + assert pdfjs.get_pdfjs_res_and_path('web/test') == (b'content', None) + assert pdfjs.get_pdfjs_res('web/test') == b'content' + + for path in ['/usr/share/pdf.js/', + str(tmpdir / 'data' / 'pdfjs'), + # hardcoded for --temp-basedir + os.path.expanduser('~/.local/share/qutebrowser/pdfjs/')]: + read_system_mock.assert_any_call(path, ['web/test', 'test']) + + def test_get_pdfjs_res_not_found(self, read_system_mock, read_file_mock, + caplog): + read_system_mock.return_value = (None, None) + read_file_mock.side_effect = FileNotFoundError + + with pytest.raises(pdfjs.PDFJSNotFound, + match="Path 'web/test' not found"): + pdfjs.get_pdfjs_res_and_path('web/test') + + assert not caplog.records + + def test_get_pdfjs_res_oserror(self, read_system_mock, read_file_mock, + caplog): + read_system_mock.return_value = (None, None) + read_file_mock.side_effect = OSError("Message") + + with caplog.at_level(logging.WARNING): + with pytest.raises(pdfjs.PDFJSNotFound, + match="Path 'web/test' not found"): + pdfjs.get_pdfjs_res_and_path('web/test') + + assert len(caplog.records) == 1 + rec = caplog.records[0] + assert rec.message == 'OSError while reading PDF.js file: Message' @pytest.mark.parametrize('path, expected', [ @@ -72,3 +167,75 @@ def test_fix_urls(): ]) def test_remove_prefix(path, expected): assert pdfjs._remove_prefix(path) == expected + + +@pytest.mark.parametrize('names, expected_name', [ + (['one'], 'one'), + (['doesnotexist', 'two'], 'two'), + (['one', 'two'], 'one'), + (['does', 'not', 'onexist'], None), +]) +def test_read_from_system(names, expected_name, tmpdir): + file1 = tmpdir / 'one' + file1.write_text('text1', encoding='ascii') + file2 = tmpdir / 'two' + file2.write_text('text2', encoding='ascii') + + if expected_name == 'one': + expected = (b'text1', str(file1)) + elif expected_name == 'two': + expected = (b'text2', str(file2)) + elif expected_name is None: + expected = (None, None) + + assert pdfjs._read_from_system(str(tmpdir), names) == expected + + +def test_read_from_system_oserror(tmpdir, caplog): + unreadable_file = tmpdir / 'unreadable' + unreadable_file.ensure() + unreadable_file.chmod(0) + if os.access(str(unreadable_file), os.R_OK): + # Docker container or similar + pytest.skip("File was still readable") + + expected = (None, None) + with caplog.at_level(logging.WARNING): + assert pdfjs._read_from_system(str(tmpdir), ['unreadable']) == expected + + assert len(caplog.records) == 1 + rec = caplog.records[0] + assert rec.message.startswith('OSError while reading PDF.js file:') + + +@pytest.mark.parametrize('available', [True, False]) +def test_is_available(available, mocker): + mock = mocker.patch.object(pdfjs, 'get_pdfjs_res', autospec=True) + if available: + mock.return_value = b'foo' + else: + mock.side_effect = pdfjs.PDFJSNotFound('build/pdf.js') + + assert pdfjs.is_available() == available + + +@pytest.mark.parametrize('mimetype, url, enabled, expected', [ + # PDF files + ('application/pdf', 'http://www.example.com', True, True), + ('application/x-pdf', 'http://www.example.com', True, True), + # Not a PDF + ('application/octet-stream', 'http://www.example.com', True, False), + # PDF.js disabled + ('application/pdf', 'http://www.example.com', False, False), + # Download button in PDF.js + ('application/pdf', 'blob:qute%3A///b45250b3', True, False), +]) +def test_should_use_pdfjs(mimetype, url, enabled, expected, config_stub): + config_stub.val.content.pdfjs = enabled + assert pdfjs.should_use_pdfjs(mimetype, QUrl(url)) == expected + + +def test_get_main_url(): + expected = ('qute://pdfjs/web/viewer.html?filename=' + 'hello?world.pdf&file=') + assert pdfjs.get_main_url('hello?world.pdf') == QUrl(expected) diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index 39df01389..64d525367 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -20,12 +20,13 @@ import json import os import time +import logging -from PyQt5.QtCore import QUrl +import py.path # pylint: disable=no-name-in-module +from PyQt5.QtCore import QUrl, QUrlQuery import pytest -from qutebrowser.browser import history, qutescheme -from qutebrowser.utils import objreg +from qutebrowser.browser import qutescheme, pdfjs, downloads class TestJavascriptHandler: @@ -61,13 +62,13 @@ class TestJavascriptHandler: def test_qutejavascript_404(self): url = QUrl("qute://javascript/404.js") - with pytest.raises(qutescheme.QuteSchemeOSError): + with pytest.raises(qutescheme.SchemeOSError): qutescheme.data_for_url(url) def test_qutejavascript_empty_query(self): url = QUrl("qute://javascript") - with pytest.raises(qutescheme.QuteSchemeError): + with pytest.raises(qutescheme.UrlInvalidError): qutescheme.qute_javascript(url) @@ -90,26 +91,18 @@ class TestHistoryHandler: for i in range(entry_count): entry_atime = now - i * interval entry = {"atime": str(entry_atime), - "url": QUrl("www.x.com/" + str(i)), + "url": QUrl("http://www.x.com/" + str(i)), "title": "Page " + str(i)} items.insert(0, entry) return items - @pytest.fixture - def fake_web_history(self, fake_save_manager, tmpdir, init_sql): - """Create a fake web-history and register it into objreg.""" - web_history = history.WebHistory() - objreg.register('web-history', web_history) - yield web_history - objreg.delete('web-history') - @pytest.fixture(autouse=True) - def fake_history(self, fake_web_history, fake_args, entries): + def fake_history(self, web_history, fake_args, entries): """Create fake history.""" fake_args.debug_flags = [] for item in entries: - fake_web_history.add_url(**item) + web_history.add_url(**item) @pytest.mark.parametrize("start_time_offset, expected_item_count", [ (0, 4), @@ -133,7 +126,16 @@ class TestHistoryHandler: assert item['time'] <= start_time assert item['time'] > end_time - def test_qute_history_benchmark(self, fake_web_history, benchmark, now): + def test_exclude(self, web_history, now, config_stub): + """Make sure the completion.web_history.exclude setting is not used.""" + config_stub.val.completion.web_history.exclude = ['www.x.com'] + + url = QUrl("qute://history/data?start_time={}".format(now)) + _mimetype, data = qutescheme.qute_history(url) + items = json.loads(data) + assert items + + def test_qute_history_benchmark(self, web_history, benchmark, now): r = range(100000) entries = { 'atime': [int(now - t) for t in r], @@ -142,7 +144,7 @@ class TestHistoryHandler: 'redirect': [False for _ in r], } - fake_web_history.insert_batch(entries) + web_history.insert_batch(entries) url = QUrl("qute://history/data?start_time={}".format(now)) _mimetype, data = benchmark(qutescheme.qute_history, url) assert len(json.loads(data)) > 1 @@ -169,3 +171,68 @@ class TestHelpHandler: mimetype, data = qutescheme.qute_help(QUrl('qute://help/foo.bin')) assert mimetype == 'application/octet-stream' assert data == b'\xff' + + +class TestPDFJSHandler: + + """Test the qute://pdfjs endpoint.""" + + @pytest.fixture(autouse=True) + def fake_pdfjs(self, monkeypatch): + def get_pdfjs_res(path): + if path == '/existing/file.html': + return b'foobar' + raise pdfjs.PDFJSNotFound(path) + + monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res) + + @pytest.fixture + def download_tmpdir(self): + tdir = downloads.temp_download_manager.get_tmpdir() + yield py.path.local(tdir.name) # pylint: disable=no-member + tdir.cleanup() + + def test_existing_resource(self): + """Test with a resource that exists.""" + _mimetype, data = qutescheme.data_for_url( + QUrl('qute://pdfjs/existing/file.html')) + assert data == b'foobar' + + def test_nonexisting_resource(self, caplog): + """Test with a resource that does not exist.""" + with caplog.at_level(logging.WARNING, 'misc'): + with pytest.raises(qutescheme.NotFoundError): + qutescheme.data_for_url(QUrl('qute://pdfjs/no/file.html')) + assert len(caplog.records) == 1 + assert (caplog.records[0].message == + 'pdfjs resource requested but not found: /no/file.html') + + def test_viewer_page(self, data_tmpdir): + """Load the /web/viewer.html page.""" + _mimetype, data = qutescheme.data_for_url( + QUrl('qute://pdfjs/web/viewer.html?filename=foobar')) + assert b'PDF.js' in data + + def test_viewer_no_filename(self): + with pytest.raises(qutescheme.UrlInvalidError): + qutescheme.data_for_url(QUrl('qute://pdfjs/web/viewer.html')) + + def test_file(self, download_tmpdir): + """Load a file via qute://pdfjs/file.""" + (download_tmpdir / 'testfile').write_binary(b'foo') + _mimetype, data = qutescheme.data_for_url( + QUrl('qute://pdfjs/file?filename=testfile')) + assert data == b'foo' + + def test_file_no_filename(self): + with pytest.raises(qutescheme.UrlInvalidError): + qutescheme.data_for_url(QUrl('qute://pdfjs/file')) + + @pytest.mark.parametrize('sep', ['/', os.sep]) + def test_file_pathsep(self, sep): + url = QUrl('qute://pdfjs/file') + query = QUrlQuery() + query.addQueryItem('filename', 'foo{}bar'.format(sep)) + url.setQuery(query) + with pytest.raises(qutescheme.RequestDeniedError): + qutescheme.data_for_url(url) diff --git a/tests/unit/browser/test_shared.py b/tests/unit/browser/test_shared.py index f24f7ad97..78302d8c1 100644 --- a/tests/unit/browser/test_shared.py +++ b/tests/unit/browser/test_shared.py @@ -19,6 +19,8 @@ import pytest +from PyQt5.QtCore import QUrl + from qutebrowser.browser import shared @@ -47,4 +49,4 @@ def test_custom_headers(config_stub, dnt, accept_language, custom_headers, headers.custom = custom_headers expected_items = sorted(expected.items()) - assert shared.custom_headers() == expected_items + assert shared.custom_headers(QUrl()) == expected_items diff --git a/tests/unit/browser/webengine/test_spell.py b/tests/unit/browser/webengine/test_spell.py index e8ce3cecc..14b343df5 100644 --- a/tests/unit/browser/webengine/test_spell.py +++ b/tests/unit/browser/webengine/test_spell.py @@ -17,14 +17,14 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Tests for qutebrowser.browser.webengine.spell module.""" - import logging import os +import pytest from PyQt5.QtCore import QLibraryInfo + from qutebrowser.browser.webengine import spell -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, qtutils, standarddir def test_version(message_mock, caplog): @@ -38,10 +38,19 @@ def test_version(message_mock, caplog): assert msg.text == expected -def test_dictionary_dir(monkeypatch): - monkeypatch.setattr(QLibraryInfo, 'location', lambda _: 'datapath') - assert spell.dictionary_dir() == os.path.join('datapath', - 'qtwebengine_dictionaries') +@pytest.mark.parametrize('qt_version, old, subdir', [ + ('5.9', True, 'global_datapath'), + ('5.9', False, 'global_datapath'), + ('5.10', True, 'global_datapath'), + ('5.10', False, 'user_datapath'), +]) +def test_dictionary_dir(monkeypatch, qt_version, old, subdir): + monkeypatch.setattr(qtutils, 'qVersion', lambda: qt_version) + monkeypatch.setattr(QLibraryInfo, 'location', lambda _: 'global_datapath') + monkeypatch.setattr(standarddir, 'data', lambda: 'user_datapath') + + expected = os.path.join(subdir, 'qtwebengine_dictionaries') + assert spell.dictionary_dir(old=old) == expected def test_local_filename_dictionary_does_not_exist(monkeypatch): @@ -83,3 +92,60 @@ def test_local_filename_installed_malformed(tmpdir, monkeypatch, caplog): (tmpdir / lang_file).ensure() with caplog.at_level(logging.WARNING): assert spell.local_filename('en-US') == 'en-US-11-0' + + +class TestInit: + + ENV = 'QTWEBENGINE_DICTIONARIES_PATH' + + @pytest.fixture(autouse=True) + def remove_envvar(self, monkeypatch): + monkeypatch.delenv(self.ENV, raising=False) + + @pytest.fixture + def patch_new_qt(self, monkeypatch): + monkeypatch.setattr(spell.qtutils, 'version_check', + lambda _ver, compiled: True) + + @pytest.fixture + def dict_dir(self, data_tmpdir): + return data_tmpdir / 'qtwebengine_dictionaries' + + @pytest.fixture + def old_dict_dir(self, monkeypatch, tmpdir): + data_dir = tmpdir / 'old' + dict_dir = data_dir / 'qtwebengine_dictionaries' + (dict_dir / 'somedict').ensure() + monkeypatch.setattr(spell.QLibraryInfo, 'location', + lambda _arg: str(data_dir)) + return dict_dir + + def test_old_qt(self, monkeypatch): + monkeypatch.setattr(spell.qtutils, 'version_check', + lambda _ver, compiled: False) + spell.init() + assert self.ENV not in os.environ + + def test_new_qt(self, dict_dir, patch_new_qt): + spell.init() + assert os.environ[self.ENV] == str(dict_dir) + + def test_moving(self, old_dict_dir, dict_dir, patch_new_qt): + spell.init() + assert (dict_dir / 'somedict').exists() + + def test_moving_oserror(self, mocker, caplog, + old_dict_dir, dict_dir, patch_new_qt): + mocker.patch('shutil.copytree', side_effect=OSError) + + with caplog.at_level(logging.ERROR): + spell.init() + + record = caplog.records[0] + assert record.message == 'Failed to copy old dictionaries' + + def test_moving_existing_destdir(self, old_dict_dir, dict_dir, + patch_new_qt): + dict_dir.ensure(dir=True) + spell.init() + assert not (dict_dir / 'somedict').exists() diff --git a/tests/unit/browser/webengine/test_webenginetab.py b/tests/unit/browser/webengine/test_webenginetab.py new file mode 100644 index 000000000..093ae85fa --- /dev/null +++ b/tests/unit/browser/webengine/test_webenginetab.py @@ -0,0 +1,93 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Test webenginetab.""" + +import logging + +import pytest +QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets") +QWebEnginePage = QtWebEngineWidgets.QWebEnginePage +QWebEngineScriptCollection = QtWebEngineWidgets.QWebEngineScriptCollection + +from qutebrowser.browser import greasemonkey + +pytestmark = pytest.mark.usefixtures('greasemonkey_manager') + + +class TestWebengineScripts: + + """Test the _WebEngineScripts utility class.""" + + @pytest.fixture + def webengine_scripts(self, webengine_tab): + return webengine_tab._scripts + + def test_greasemonkey_undefined_world(self, webengine_scripts, caplog): + """Make sure scripts with non-existent worlds are rejected.""" + scripts = [ + greasemonkey.GreasemonkeyScript( + [('qute-js-world', 'Mars'), ('name', 'test')], None) + ] + + with caplog.at_level(logging.ERROR, 'greasemonkey'): + webengine_scripts._inject_greasemonkey_scripts(scripts) + + assert len(caplog.records) == 1 + msg = caplog.records[0].message + assert "has invalid value for '@qute-js-world': Mars" in msg + collection = webengine_scripts._widget.page().scripts().toList() + assert not any(script.name().startswith('GM-') + for script in collection) + + @pytest.mark.parametrize("worldid", [-1, 257]) + def test_greasemonkey_out_of_range_world(self, worldid, webengine_scripts, + caplog): + """Make sure scripts with out-of-range worlds are rejected.""" + scripts = [ + greasemonkey.GreasemonkeyScript( + [('qute-js-world', worldid), ('name', 'test')], None) + ] + + with caplog.at_level(logging.ERROR, 'greasemonkey'): + webengine_scripts._inject_greasemonkey_scripts(scripts) + + assert len(caplog.records) == 1 + msg = caplog.records[0].message + assert "has invalid value for '@qute-js-world': " in msg + assert "should be between 0 and" in msg + collection = webengine_scripts._widget.page().scripts().toList() + assert not any(script.name().startswith('GM-') + for script in collection) + + @pytest.mark.parametrize("worldid", [0, 10]) + def test_greasemonkey_good_worlds_are_passed(self, worldid, + webengine_scripts, caplog): + """Make sure scripts with valid worlds have it set.""" + scripts = [ + greasemonkey.GreasemonkeyScript( + [('name', 'foo'), ('qute-js-world', worldid)], None + ) + ] + + with caplog.at_level(logging.ERROR, 'greasemonkey'): + webengine_scripts._inject_greasemonkey_scripts(scripts) + + collection = webengine_scripts._widget.page().scripts() + assert collection.toList()[-1].worldId() == worldid diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py index 5bdbb47cc..2654097ea 100644 --- a/tests/unit/browser/webkit/network/test_filescheme.py +++ b/tests/unit/browser/webkit/network/test_filescheme.py @@ -248,7 +248,7 @@ class TestFileSchemeHandler: def test_dir(self, tmpdir): url = QUrl.fromLocalFile(str(tmpdir)) req = QNetworkRequest(url) - reply = filescheme.handler(req) + reply = filescheme.handler(req, None, None) # The URL will always use /, even on Windows - so we force this here # too. tmpdir_path = str(tmpdir).replace(os.sep, '/') @@ -259,7 +259,7 @@ class TestFileSchemeHandler: filename.ensure() url = QUrl.fromLocalFile(str(filename)) req = QNetworkRequest(url) - reply = filescheme.handler(req) + reply = filescheme.handler(req, None, None) assert reply is None def test_unicode_encode_error(self, mocker): @@ -269,5 +269,5 @@ class TestFileSchemeHandler: err = UnicodeEncodeError('ascii', '', 0, 2, 'foo') mocker.patch('os.path.isdir', side_effect=err) - reply = filescheme.handler(req) + reply = filescheme.handler(req, None, None) assert reply is None diff --git a/tests/unit/browser/webkit/network/test_webkitqutescheme.py b/tests/unit/browser/webkit/network/test_webkitqutescheme.py deleted file mode 100644 index c1775121e..000000000 --- a/tests/unit/browser/webkit/network/test_webkitqutescheme.py +++ /dev/null @@ -1,63 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2016-2018 Daniel Schadt -# Copyright 2016-2018 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -import logging - -import pytest -from PyQt5.QtCore import QUrl - -from qutebrowser.utils import usertypes -from qutebrowser.browser import pdfjs, qutescheme -# pylint: disable=unused-import -from qutebrowser.browser.webkit.network import webkitqutescheme -# pylint: enable=unused-import - - -class TestPDFJSHandler: - """Test the qute://pdfjs endpoint.""" - - @pytest.fixture(autouse=True) - def fake_pdfjs(self, monkeypatch): - def get_pdfjs_res(path): - if path == '/existing/file.html': - return b'foobar' - raise pdfjs.PDFJSNotFound(path) - - monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res) - - @pytest.fixture(autouse=True) - def patch_backend(self, monkeypatch): - monkeypatch.setattr(qutescheme.objects, 'backend', - usertypes.Backend.QtWebKit) - - def test_existing_resource(self): - """Test with a resource that exists.""" - _mimetype, data = qutescheme.data_for_url( - QUrl('qute://pdfjs/existing/file.html')) - assert data == b'foobar' - - def test_nonexisting_resource(self, caplog): - """Test with a resource that does not exist.""" - with caplog.at_level(logging.WARNING, 'misc'): - with pytest.raises(qutescheme.QuteSchemeError): - qutescheme.data_for_url(QUrl('qute://pdfjs/no/file.html')) - assert len(caplog.records) == 1 - assert (caplog.records[0].message == - 'pdfjs resource requested but not found: /no/file.html') diff --git a/tests/unit/browser/webkit/test_downloads.py b/tests/unit/browser/webkit/test_downloads.py index 571e21704..1ab80ec65 100644 --- a/tests/unit/browser/webkit/test_downloads.py +++ b/tests/unit/browser/webkit/test_downloads.py @@ -62,16 +62,13 @@ def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache, '', None), ]) +@pytest.mark.fake_os('windows') def test_page_titles(url, title, out): assert downloads.suggested_fn_from_title(url, title) == out class TestDownloadTarget: - def test_base(self): - with pytest.raises(NotImplementedError): - downloads._DownloadTarget() - def test_filename(self): target = downloads.FileDownloadTarget("/foo/bar") assert target.filename == "/foo/bar" @@ -96,3 +93,29 @@ class TestDownloadTarget: ]) def test_class_hierarchy(self, obj): assert isinstance(obj, downloads._DownloadTarget) + + +@pytest.mark.parametrize('raw, expected', [ + pytest.param('http://foo/bar', 'bar', + marks=pytest.mark.fake_os('windows')), + pytest.param('A *|<>\\: bear!', 'A ______ bear!', + marks=pytest.mark.fake_os('windows')), + pytest.param('A *|<>\\: bear!', 'A *|<>\\: bear!', + marks=[pytest.mark.fake_os('posix'), pytest.mark.posix]), +]) +def test_sanitized_filenames(raw, expected, + config_stub, download_tmpdir, monkeypatch): + manager = downloads.AbstractDownloadManager() + target = downloads.FileDownloadTarget(str(download_tmpdir)) + item = downloads.AbstractDownloadItem() + + # Don't try to start a timer outside of a QThread + manager._update_timer.isActive = lambda: True + + # Abstract methods + item._ensure_can_set_filename = lambda *args: True + item._after_set_filename = lambda *args: True + + manager._init_item(item, True, raw) + item.set_target(target) + assert item._filename.endswith(expected) diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index df3de6310..09be16848 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -29,7 +29,7 @@ import pytest from PyQt5.QtCore import QRect, QPoint, QUrl QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement -from qutebrowser.browser import webelem, browsertab +from qutebrowser.browser import browsertab from qutebrowser.browser.webkit import webkitelem from qutebrowser.misc import objects from qutebrowser.utils import usertypes @@ -146,53 +146,48 @@ class SelectionAndFilterTests: TESTS = [ ('', []), ('', []), - ('', [webelem.Group.url]), - ('', [webelem.Group.url]), + ('', ['url']), + ('', ['url']), - ('', [webelem.Group.all]), - ('', [webelem.Group.all, webelem.Group.links, - webelem.Group.url]), - ('', [webelem.Group.all, - webelem.Group.links, - webelem.Group.url]), + ('', ['all']), + ('', ['all', 'links', 'url']), + ('', ['all', 'links', 'url']), - ('', [webelem.Group.all]), - ('', [webelem.Group.all, webelem.Group.links, - webelem.Group.url]), + ('', ['all']), + ('', ['all', 'links', 'url']), - ('', [webelem.Group.all]), - ('', [webelem.Group.all, webelem.Group.links, - webelem.Group.url]), + ('', ['all']), + ('', ['all', 'links', 'url']), - ('