Implement Fido2 User Verification (close #8533)
This commit is contained in:
parent
b417f2a23b
commit
fae2db9147
|
|
@ -10,7 +10,7 @@ import functools
|
|||
import dataclasses
|
||||
import re
|
||||
import html as html_utils
|
||||
from typing import cast, Union, Optional
|
||||
from typing import cast, Union, Optional, Any
|
||||
|
||||
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
|
||||
QObject, QByteArray, QTimer)
|
||||
|
|
@ -28,6 +28,15 @@ from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
|
|||
from qutebrowser.qt import sip, machinery
|
||||
from qutebrowser.misc import objects, miscwidgets
|
||||
|
||||
if machinery.IS_QT6:
|
||||
try:
|
||||
from qutebrowser.qt.webenginecore import QWebEngineWebAuthUxRequest
|
||||
except ImportError:
|
||||
# Added in Qt 6.7
|
||||
QWebEngineWebAuthUxRequest: None = None # type: ignore[no-redef]
|
||||
else:
|
||||
QWebEngineWebAuthUxRequest: Any = None
|
||||
|
||||
|
||||
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
||||
_JS_WORLD_MAP = {
|
||||
|
|
@ -1241,6 +1250,152 @@ class _WebEngineScripts(QObject):
|
|||
)
|
||||
|
||||
|
||||
class _WebEngineWebAuth(QObject):
|
||||
|
||||
"""Handling of Webauth events.
|
||||
|
||||
Signals:
|
||||
request_cancelled: Emitted when a Webauth request was cancelled.
|
||||
"""
|
||||
|
||||
request_cancelled = pyqtSignal()
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._request = None
|
||||
|
||||
def on_ux_requested(self, request):
|
||||
"""Handle a Webauth UX request."""
|
||||
log.webview.debug("Asking for Webauth user verification for "
|
||||
f"{request.relyingPartyId()}")
|
||||
self._request = request
|
||||
request.stateChanged.connect(self._on_ux_state_changed)
|
||||
self._on_ux_state_changed(request.state())
|
||||
|
||||
def _on_ux_state_changed(self, state):
|
||||
log.webview.debug(f"Webauth UX state: {state}")
|
||||
if state == QWebEngineWebAuthUxRequest.WebAuthUxState.CollectPin:
|
||||
self._ux_pin_request()
|
||||
elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.FinishTokenCollection:
|
||||
self._ux_finish_token_collection()
|
||||
elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount:
|
||||
self._ux_account_selection()
|
||||
elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.Completed:
|
||||
self._ux_request_completed()
|
||||
elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.Cancelled:
|
||||
self._ux_request_cancelled()
|
||||
elif state == QWebEngineWebAuthUxRequest.WebAuthUxState.RequestFailed:
|
||||
self._ux_request_failed()
|
||||
else:
|
||||
raise utils.Unreachable(state)
|
||||
|
||||
def _ux_pin_request(self):
|
||||
assert self._request is not None
|
||||
log.webview.debug("Collect Webauth pin for "
|
||||
f"{self._request.relyingPartyId()}")
|
||||
answer = self._verification_required(self._request.relyingPartyId())
|
||||
if answer is not None:
|
||||
log.webview.debug("User verification accepted by user")
|
||||
self._request.setPin(answer)
|
||||
else:
|
||||
log.webview.debug("User verification aborted by user")
|
||||
self._request.cancel()
|
||||
|
||||
def _ux_finish_token_collection(self):
|
||||
log.webview.debug("Finish Webauth token collection")
|
||||
message.info("Please touch your device now.")
|
||||
|
||||
def _ux_account_selection(self):
|
||||
assert self._request is not None
|
||||
log.webview.debug("Select Webauth account for "
|
||||
f"{self._request.relyingPartyId()}")
|
||||
answer = self._select_account(self._request.relyingPartyId(),
|
||||
self._request.userNames())
|
||||
if answer is not None:
|
||||
log.webview.debug(f"Username selection accepted by user: {answer}")
|
||||
self._request.setSelectedAccount(answer)
|
||||
else:
|
||||
log.webview.debug("Username selection aborted by user")
|
||||
self._request.cancel()
|
||||
|
||||
def _ux_request_completed(self):
|
||||
log.webview.debug("Webauth request completed")
|
||||
message.info("User verification completed.")
|
||||
self._request = None
|
||||
|
||||
def _ux_request_cancelled(self):
|
||||
log.webview.debug("Webauth verification cancelled")
|
||||
message.info("User verification cancelled.")
|
||||
self.request_cancelled.emit()
|
||||
|
||||
def _ux_request_failed(self):
|
||||
assert self._request is not None
|
||||
log.webview.debug("Webauth request failed for "
|
||||
f"{self._request.relyingPartyId()}: "
|
||||
f"{self._request.requestFailureReason()}")
|
||||
|
||||
reason_text = self._get_failure_reason_text(
|
||||
self._request.requestFailureReason()
|
||||
)
|
||||
message.info(f"User verification failed: {reason_text}")
|
||||
|
||||
def _get_failure_reason_text(self, reason):
|
||||
texts = {
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.Timeout:
|
||||
"The request timed out.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.KeyNotRegistered:
|
||||
"The device is not registered.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.KeyAlreadyRegistered:
|
||||
"You already registered this device.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.SoftPinBlock:
|
||||
"The device is soft-locked because the wrong PIN was entered "
|
||||
"too many times.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.HardPinBlock:
|
||||
"The device is hard-locked because the wrong PIN was entered "
|
||||
"too many times.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorRemovedDuringPinEntry:
|
||||
"The device was removed during verification. Please re-insert "
|
||||
"and try again",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingResidentKeys:
|
||||
"The device does not have resident key support.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingUserVerification:
|
||||
"The device is missing user verification.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingLargeBlob:
|
||||
"No common algorithms.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.NoCommonAlgorithms:
|
||||
"No common algorithms",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.StorageFull:
|
||||
"The storage on the device is full.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.UserConsentDenied:
|
||||
"User consent was denied.",
|
||||
QWebEngineWebAuthUxRequest.RequestFailureReason.WinUserCancelled:
|
||||
"User cancelled the request.",
|
||||
}
|
||||
if reason not in texts:
|
||||
raise utils.Unreachable(reason)
|
||||
return texts[reason]
|
||||
|
||||
def _verification_required(self, url: str) -> Any:
|
||||
"""Ask a prompt for a webauth user verification request."""
|
||||
return message.ask(
|
||||
title=f"User Verification for {url}",
|
||||
text="Please enter the PIN for your device:",
|
||||
mode=usertypes.PromptMode.pwd,
|
||||
abort_on=[self._tab.abort_questions, self.request_cancelled])
|
||||
|
||||
def _select_account(self, url: str, usernames: list[str]) -> Any:
|
||||
"""Ask a prompt for a webauth account selection."""
|
||||
usernames_html = "".join(f"<li>{html_utils.escape(name)}</li>"
|
||||
for name in usernames)
|
||||
text = f"Please select an account:<br><ul>{usernames_html}</ul>"
|
||||
|
||||
return message.ask(
|
||||
title=f"Account Selection for {url}", text=text,
|
||||
choices=usernames, mode=usertypes.PromptMode.select,
|
||||
abort_on=[self._tab.abort_questions, self.request_cancelled])
|
||||
|
||||
|
||||
class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
|
||||
|
||||
"""QtWebEngine-related methods which aren't part of the public API."""
|
||||
|
|
@ -1308,6 +1463,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
tab=self)
|
||||
self._permissions = _WebEnginePermissions(tab=self, parent=self)
|
||||
self._scripts = _WebEngineScripts(tab=self, parent=self)
|
||||
if QWebEngineWebAuthUxRequest is not None:
|
||||
self._webauth = _WebEngineWebAuth(tab=self, parent=self)
|
||||
# We're assigning settings in _set_widget
|
||||
self.settings = webenginesettings.WebEngineSettings(settings=None)
|
||||
self._set_widget(widget)
|
||||
|
|
@ -1735,6 +1892,8 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||
page.loadStarted.connect(self._on_load_started)
|
||||
page.certificate_error.connect(self._on_ssl_errors)
|
||||
page.authenticationRequired.connect(self._on_authentication_required)
|
||||
if machinery.IS_QT6 and QWebEngineWebAuthUxRequest is not None:
|
||||
page.webAuthUxRequested.connect(self._webauth.on_ux_requested)
|
||||
page.proxyAuthenticationRequired.connect(
|
||||
self._on_proxy_authentication_required)
|
||||
page.contentsSizeChanged.connect(self.contents_size_changed)
|
||||
|
|
|
|||
|
|
@ -317,6 +317,8 @@ class PromptContainer(QWidget):
|
|||
usertypes.PromptMode.user_pwd: AuthenticationPrompt,
|
||||
usertypes.PromptMode.download: DownloadFilenamePrompt,
|
||||
usertypes.PromptMode.alert: AlertPrompt,
|
||||
usertypes.PromptMode.pwd: PasswordPrompt,
|
||||
usertypes.PromptMode.select: SelectPrompt,
|
||||
}
|
||||
klass = classes[question.mode]
|
||||
prompt = klass(question)
|
||||
|
|
@ -1046,6 +1048,28 @@ class AlertPrompt(_BasePrompt):
|
|||
return [('prompt-accept', "Hide")]
|
||||
|
||||
|
||||
class PasswordPrompt(LineEditPrompt):
|
||||
|
||||
"""A prompt for a password/pin."""
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._lineedit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
|
||||
|
||||
class SelectPrompt(LineEditPrompt):
|
||||
|
||||
"""A prompt for selecting one out of multiple choices."""
|
||||
|
||||
def accept(self, value=None, save=False):
|
||||
self._check_save_support(save)
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
if text in self.question.choices:
|
||||
self.question.answer = text
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize global prompt objects."""
|
||||
global prompt_queue
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ def _build_question(title: str,
|
|||
default: Union[None, bool, str] = None,
|
||||
abort_on: Iterable[pyqtBoundSignal] = (),
|
||||
url: str = None,
|
||||
option: bool = None) -> usertypes.Question:
|
||||
option: bool = None,
|
||||
choices: list[str] = None) -> usertypes.Question:
|
||||
"""Common function for ask/ask_async."""
|
||||
question = usertypes.Question()
|
||||
question.title = title
|
||||
|
|
@ -127,6 +128,10 @@ def _build_question(title: str,
|
|||
raise ValueError("Need 'url' given when 'option' is given")
|
||||
question.option = option
|
||||
|
||||
if choices is not None and mode != usertypes.PromptMode.select:
|
||||
raise ValueError("Can only use 'choices' with PromptMode.select")
|
||||
question.choices = choices
|
||||
|
||||
for sig in abort_on:
|
||||
sig.connect(question.abort)
|
||||
return question
|
||||
|
|
@ -142,6 +147,8 @@ def ask(*args: Any, **kwargs: Any) -> Any:
|
|||
text: Additional text to show
|
||||
option: The option for always/never question answers.
|
||||
Only available with PromptMode.yesno.
|
||||
choices: The choices for options question answers.
|
||||
Only available with PromptMode.selection.
|
||||
abort_on: A list of signals which abort the question if emitted.
|
||||
|
||||
Return:
|
||||
|
|
@ -190,6 +197,7 @@ def confirm_async(*, yes_action: _ActionType,
|
|||
question.
|
||||
default: True/False to set a default value, or None.
|
||||
option: The option for always/never question answers.
|
||||
choices: The choices for options question answers.
|
||||
text: Additional text to show.
|
||||
|
||||
Return:
|
||||
|
|
|
|||
|
|
@ -227,6 +227,8 @@ class PromptMode(enum.Enum):
|
|||
user_pwd = enum.auto()
|
||||
alert = enum.auto()
|
||||
download = enum.auto()
|
||||
pwd = enum.auto()
|
||||
select = enum.auto()
|
||||
|
||||
|
||||
class ClickTarget(enum.Enum):
|
||||
|
|
@ -359,6 +361,8 @@ class Question(QObject):
|
|||
yesno: A question which can be answered with yes/no.
|
||||
text: A question which requires a free text answer.
|
||||
user_pwd: A question for a username and password.
|
||||
pwd: A question for a password or PIN.
|
||||
select: A question for selecting an option.
|
||||
default: The default value.
|
||||
For yesno, None (no default), True or False.
|
||||
For text, a default text as string.
|
||||
|
|
@ -399,6 +403,7 @@ class Question(QObject):
|
|||
self.text: Optional[str] = None
|
||||
self.url: Optional[str] = None
|
||||
self.option: Optional[bool] = None
|
||||
self.choices: Optional[list[str]] = None
|
||||
self.answer: Union[str, bool, None] = None
|
||||
self.is_aborted = False
|
||||
self.interrupted = False
|
||||
|
|
@ -406,7 +411,7 @@ class Question(QObject):
|
|||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, title=self.title, text=self.text,
|
||||
mode=self.mode, default=self.default,
|
||||
option=self.option)
|
||||
option=self.option, choices=self.choices)
|
||||
|
||||
@pyqtSlot()
|
||||
def done(self) -> None:
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ Feature: Downloading things from a website.
|
|||
And I set downloads.location.prompt to true
|
||||
And I open data/downloads/issue1243.html
|
||||
And I hint with args "links download" and follow a
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='qutebrowser-download' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
Then the error "Download error: Invalid host (from path): ''" should be shown
|
||||
And "UrlInvalidError while handling qute://* URL" should be logged
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ Feature: Downloading things from a website.
|
|||
And I set downloads.location.prompt to true
|
||||
And I open data/data_link.html
|
||||
And I hint with args "links download" and follow s
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='download.pdf' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='download.pdf' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I run :mode-leave
|
||||
Then no crash should happen
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ Feature: Downloading things from a website.
|
|||
When I set downloads.location.suggestion to filename
|
||||
And I set downloads.location.prompt to true
|
||||
And I open data/downloads/download.bin in a new window without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> *" in the log
|
||||
And I run :window-only
|
||||
And I run :mode-leave
|
||||
Then no crash should happen
|
||||
|
|
@ -132,7 +132,7 @@ Feature: Downloading things from a website.
|
|||
Scenario: Shutting down with a download question
|
||||
When I set downloads.location.prompt to true
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I run :close
|
||||
Then qutebrowser should quit
|
||||
# (and no crash should happen)
|
||||
|
|
@ -162,7 +162,7 @@ Feature: Downloading things from a website.
|
|||
Scenario: Downloading a file to a reserved path
|
||||
When I set downloads.location.prompt to true
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept COM1
|
||||
And I run :mode-leave
|
||||
Then the error "Invalid filename" should be shown
|
||||
|
|
@ -171,7 +171,7 @@ Feature: Downloading things from a website.
|
|||
Scenario: Downloading a file to a drive-relative working directory
|
||||
When I set downloads.location.prompt to true
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept C:foobar
|
||||
And I run :mode-leave
|
||||
Then the error "Invalid filename" should be shown
|
||||
|
|
@ -251,14 +251,14 @@ Feature: Downloading things from a website.
|
|||
|
||||
Scenario: :download with a filename and directory which doesn't exist
|
||||
When I run :download --dest (tmpdir)(dirsep)downloads(dirsep)somedir(dirsep)file http://localhost:(port)/data/downloads/download.bin
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> option=None text='<b>*</b> does not exist. Create it?' title='Create directory?'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default=None mode=<PromptMode.yesno: 1> option=None text='<b>*</b> does not exist. Create it?' title='Create directory?'>, *" in the log
|
||||
And I run :prompt-accept yes
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file somedir/file should exist
|
||||
|
||||
Scenario: :download with a directory which doesn't exist
|
||||
When I run :download --dest (tmpdir)(dirsep)downloads(dirsep)somedir(dirsep) http://localhost:(port)/data/downloads/download.bin
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> option=None text='<b>*</b> does not exist. Create it?' title='Create directory?'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default=None mode=<PromptMode.yesno: 1> option=None text='<b>*</b> does not exist. Create it?' title='Create directory?'>, *" in the log
|
||||
And I run :prompt-accept yes
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file somedir/download.bin should exist
|
||||
|
|
@ -283,13 +283,13 @@ Feature: Downloading things from a website.
|
|||
When I set downloads.location.prompt to true
|
||||
And I open data/title.html
|
||||
And I run :download --mhtml
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/title.html</b>' title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/title.html</b>' title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept
|
||||
And I wait for "File successfully written." in the log
|
||||
And I run :download --mhtml
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/title.html</b>' title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text='Please enter a location for <b>http://localhost:*/data/title.html</b>' title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> option=None text='<b>*</b> already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default=None mode=<PromptMode.yesno: 1> option=None text='<b>*</b> already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log
|
||||
And I run :prompt-accept yes
|
||||
And I wait for "File successfully written." in the log
|
||||
Then the downloaded file Test title.mhtml should exist
|
||||
|
|
@ -668,9 +668,9 @@ Feature: Downloading things from a website.
|
|||
Scenario: Answering a question for a cancelled download (#415)
|
||||
When I set downloads.location.prompt to true
|
||||
And I run :download http://localhost:(port)/data/downloads/download.bin
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I run :download http://localhost:(port)/data/downloads/download2.bin
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I run :download-cancel with count 2
|
||||
And I run :prompt-accept
|
||||
And I wait until the download is finished
|
||||
|
|
@ -681,11 +681,11 @@ Feature: Downloading things from a website.
|
|||
Scenario: Nested download prompts (#8674)
|
||||
When I set downloads.location.prompt to true
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I open data/downloads/download.bin without waiting
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I run :prompt-accept
|
||||
And I run :mode-leave
|
||||
And I run :mode-leave
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ Feature: Special qute:// pages
|
|||
And I open data/misc/test.pdf without waiting
|
||||
And I wait until PDF.js is ready
|
||||
And I run :jseval (document.getElementById("downloadButton") || document.getElementById("download")).click()
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default=* mode=<PromptMode.download: 5> option=None text=* title='Save file to:'>, *" in the log
|
||||
And I run :mode-leave
|
||||
Then no crash should happen
|
||||
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ Feature: Saving and loading sessions
|
|||
Scenario: Saving session with an empty download tab
|
||||
When I open data/downloads/downloads.html
|
||||
And I run :click-element --force-event -t tab id download
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question choices=None default='*' mode=<PromptMode.download: 5> *" in the log
|
||||
And I run :mode-leave
|
||||
And I run :session-save current
|
||||
And I run :session-load --clear current
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ bdd.scenarios('downloads.feature')
|
|||
|
||||
|
||||
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
|
||||
"default={!r} mode=<PromptMode.download: 5> option=None "
|
||||
"text=* title='Save file to:'>, *")
|
||||
"choices=None default={!r} mode=<PromptMode.download: 5> "
|
||||
"option=None text=* title='Save file to:'>, *")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
Loading…
Reference in New Issue