Merge branch 'master' into download-page
This commit is contained in:
commit
3438a45b19
@ -1,3 +1,4 @@
|
||||
shallow_clone: true
|
||||
version: '{branch}-{build}'
|
||||
cache:
|
||||
- C:\projects\qutebrowser\.cache
|
||||
@ -13,7 +14,7 @@ install:
|
||||
- C:\Python27\python -u scripts\dev\ci_install.py
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" --junitxml=junit.xml
|
||||
- C:\Python34\Scripts\tox -e %TESTENV% -- -p "no:sugar" -v --junitxml=junit.xml
|
||||
|
||||
after_test:
|
||||
- ps: |
|
||||
@ -22,6 +23,3 @@ after_test:
|
||||
$file = '.\junit.xml'
|
||||
(New-Object 'System.Net.WebClient').UploadFile($url, (Resolve-Path $file))
|
||||
}
|
||||
if ($env:TESTENV -eq 'py34') {
|
||||
C:\Python34\Scripts\codecov -e TESTENV -X gcov
|
||||
}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,6 +22,7 @@ __pycache__
|
||||
/.coverage
|
||||
/htmlcov
|
||||
/.coverage.xml
|
||||
/.coverage.*
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
|
@ -37,10 +37,10 @@ install:
|
||||
- python scripts/dev/ci_install.py
|
||||
|
||||
script:
|
||||
- tox -e $TESTENV -- -p no:sugar --faulthandler-timeout=70 -v --cov-report term tests
|
||||
- tox -e $TESTENV -- -p no:sugar -v --cov-report term tests
|
||||
|
||||
after_success:
|
||||
- '[[ $TESTENV == py34 || $TESTENV == py35 ]] && codecov -e TESTENV -X gcov'
|
||||
- '[[ ($TESTENV == py34 || $TESTENV == py35) && $TRAVIS_OX == linux ]] && codecov -e TESTENV -X gcov'
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
|
@ -68,6 +68,7 @@ Changed
|
||||
finished. When set to `-1`, downloads are never removed.
|
||||
- The `:follow-hint` command now optionally takes the keystring of a hint to
|
||||
follow.
|
||||
- `:scroll-px` now doesn't take floats anymore, which made little sense.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
@ -86,6 +87,7 @@ Fixed
|
||||
`storage -> prompt-download-directory` was unset.
|
||||
- Fixed crash when using `:follow-hint` outside of hint mode.
|
||||
- Fixed crash when using `:set foo bar?` with invalid section/option.
|
||||
- Fixed scrolling to the very left/right with `:scroll-perc`.
|
||||
|
||||
v0.4.1
|
||||
------
|
||||
|
@ -548,6 +548,13 @@ workaround.
|
||||
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
|
||||
bugs] and check if they're fixed.
|
||||
|
||||
New PyQt release
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* See above
|
||||
* Install new PyQt in Windows VM (32- and 64-bit)
|
||||
* Download new installer and update PyQt installer path in `ci_install.py`.
|
||||
|
||||
qutebrowser release
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
11
FAQ.asciidoc
11
FAQ.asciidoc
@ -1,5 +1,6 @@
|
||||
Frequently asked questions
|
||||
==========================
|
||||
:title: Frequently asked questions
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
|
||||
[qanda]
|
||||
@ -87,6 +88,16 @@ Note that you might need an additional package (e.g.
|
||||
https://www.archlinux.org/packages/community/any/youtube-dl/[youtube-dl] on
|
||||
Archlinux) to play web videos with mpv.
|
||||
|
||||
How do I use qutebrowser with mutt?::
|
||||
Due to a Qt limitation, local files without `.html` extensions are
|
||||
"downloaded" instead of displayed, see
|
||||
https://github.com/The-Compiler/qutebrowser/issues/566[#566]. You can work
|
||||
around this by using this in your `mailcap`:
|
||||
+
|
||||
----
|
||||
text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal;
|
||||
----
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
Configuration not saved after modifying config.::
|
||||
|
@ -17,6 +17,7 @@ include requirements.txt
|
||||
include tox.ini
|
||||
include qutebrowser.py
|
||||
|
||||
prune www
|
||||
prune scripts/dev
|
||||
exclude scripts/asciidoc2html.py
|
||||
exclude doc/notes
|
||||
|
@ -6,6 +6,7 @@
|
||||
qutebrowser
|
||||
===========
|
||||
|
||||
// QUTE_WEB_HIDE
|
||||
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
|
||||
|
||||
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
|
||||
@ -14,6 +15,7 @@ image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=fl
|
||||
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
|
||||
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
|
||||
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
|
||||
// QUTE_WEB_HIDE_END
|
||||
|
||||
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
|
||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
@ -24,7 +26,7 @@ Screenshots
|
||||
-----------
|
||||
|
||||
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
|
||||
image:doc/img/downloads.png["screenshot 2",width=300j,link="doc/img/downloads.png"]
|
||||
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
|
||||
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
|
||||
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
|
||||
|
||||
@ -89,11 +91,11 @@ Requirements
|
||||
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.4
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.5.0 recommended)
|
||||
* http://www.python.org/[Python] 3.4 or newer
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended)
|
||||
* QtWebKit
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
||||
(5.5.0 recommended) for Python 3
|
||||
(5.5.1 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
@ -139,32 +141,33 @@ Contributors, sorted by the number of commits in descending order:
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Antoni Boucher
|
||||
* Bruno Oliveira
|
||||
* Lamar Pavel
|
||||
* Bruno Oliveira
|
||||
* Alexander Cogneau
|
||||
* Martin Tournoij
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Daniel
|
||||
* Claude
|
||||
* meles5
|
||||
* Nathan Isom
|
||||
* Austin Anderson
|
||||
* Artur Shaik
|
||||
* Thorsten Wißmann
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* meles5
|
||||
* ZDarian
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Peter Vilim
|
||||
* Jonas Schürmann
|
||||
* Jimmy
|
||||
* skinnay
|
||||
* error800
|
||||
* Zach-Button
|
||||
* Halfwit
|
||||
* Felix Van der Jeugt
|
||||
* rikn00
|
||||
* Patric Schmitz
|
||||
* Martin Zimmermann
|
||||
* Error 800
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* neeasade
|
||||
@ -180,7 +183,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* zwarag
|
||||
* error800
|
||||
* Tim Harder
|
||||
* Thiago Barroso Perrotta
|
||||
* Matthias Lisin
|
||||
|
60
misc/userscripts/qutedmenu
Executable file
60
misc/userscripts/qutedmenu
Executable file
@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Handle open -s && open -t with bemenu
|
||||
|
||||
#:bind o spawn --userscript /path/to/userscripts/qutedmenu open
|
||||
#:bind O spawn --userscript /path/to/userscripts/qutedmenu tab
|
||||
|
||||
# If you would like to set a custom colorscheme/font use these dirs.
|
||||
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
|
||||
readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
readonly datadir=${XDG_DATA_HOME:-$HOME/.local/share}
|
||||
|
||||
readonly optsfile=$confdir/dmenu/bemenucolors
|
||||
|
||||
create_menu() {
|
||||
# Check quickmarks
|
||||
while read -r url; do
|
||||
printf -- '%s\n' "$url"
|
||||
done < "$confdir"/qutebrowser/quickmarks
|
||||
|
||||
# Next bookmarks
|
||||
while read -r url _; do
|
||||
printf -- '%s\n' "$url"
|
||||
done < "$confdir"/qutebrowser/bookmarks/urls
|
||||
|
||||
# Finally history
|
||||
while read -r _ url; do
|
||||
printf -- '%s\n' "$url"
|
||||
done < "$datadir"/qutebrowser/history
|
||||
}
|
||||
|
||||
get_selection() {
|
||||
opts+=(-p qutebrowser)
|
||||
#create_menu | dmenu -l 10 "${opts[@]}"
|
||||
create_menu | bemenu -l 10 "${opts[@]}"
|
||||
}
|
||||
|
||||
# Main
|
||||
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
|
||||
if [[ -s $confdir/dmenu/font ]]; then
|
||||
read -r font < "$confdir"/dmenu/font
|
||||
fi
|
||||
|
||||
if [[ $font ]]; then
|
||||
opts+=(-fn "$font")
|
||||
fi
|
||||
|
||||
if [[ -s $optsfile ]]; then
|
||||
source "$optsfile"
|
||||
fi
|
||||
|
||||
url=$(get_selection)
|
||||
url=${url/*http/http}
|
||||
|
||||
# If no selection is made, exit (escape pressed, e.g.)
|
||||
[[ ! $url ]] && exit 0
|
||||
|
||||
case $1 in
|
||||
open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
|
||||
tab) printf '%s' "open -t $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
|
||||
esac
|
@ -36,3 +36,4 @@ qt_log_ignore =
|
||||
^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\.
|
||||
^QWaitCondition: Destroyed while threads are still waiting
|
||||
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
|
||||
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||
|
@ -814,12 +814,9 @@ class EventFilter(QObject):
|
||||
Return:
|
||||
True if the event should be filtered, False if it's passed through.
|
||||
"""
|
||||
if qApp.overrideCursor() is None:
|
||||
# Mouse cursor shown -> don't filter event
|
||||
return False
|
||||
else:
|
||||
# Mouse cursor hidden -> filter event
|
||||
return True
|
||||
# Mouse cursor shown (overrideCursor None) -> don't filter event
|
||||
# Mouse cursor hidden (overrideCursor not None) -> filter event
|
||||
return qApp.overrideCursor() is not None
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Handle an event.
|
||||
|
@ -152,30 +152,6 @@ class CommandDispatcher:
|
||||
else:
|
||||
return None
|
||||
|
||||
def _scroll_percent(self, perc=None, count=None, orientation=None):
|
||||
"""Inner logic for scroll_percent_(x|y).
|
||||
|
||||
Args:
|
||||
perc: How many percent to scroll, or None
|
||||
count: How many percent to scroll, or None
|
||||
orientation: Qt.Horizontal or Qt.Vertical
|
||||
"""
|
||||
if perc is None and count is None:
|
||||
perc = 100
|
||||
elif perc is None:
|
||||
perc = count
|
||||
if perc == 0:
|
||||
self.scroll('top')
|
||||
elif perc == 100:
|
||||
self.scroll('bottom')
|
||||
else:
|
||||
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
m = frame.scrollBarMaximum(orientation)
|
||||
if m == 0:
|
||||
return
|
||||
frame.setScrollBarValue(orientation, int(m * perc / 100))
|
||||
|
||||
def _tab_move_absolute(self, idx):
|
||||
"""Get an index for moving a tab absolutely.
|
||||
|
||||
@ -412,20 +388,27 @@ class CommandDispatcher:
|
||||
|
||||
def _back_forward(self, tab, bg, window, count, forward):
|
||||
"""Helper function for :back/:forward."""
|
||||
if (not forward and not
|
||||
self._current_widget().page().history().canGoBack()):
|
||||
# Catch common cases before e.g. cloning tab
|
||||
history = self._current_widget().page().history()
|
||||
if not forward and not history.canGoBack():
|
||||
raise cmdexc.CommandError("At beginning of history.")
|
||||
if (forward and not
|
||||
self._current_widget().page().history().canGoForward()):
|
||||
elif forward and not history.canGoForward():
|
||||
raise cmdexc.CommandError("At end of history.")
|
||||
|
||||
if tab or bg or window:
|
||||
widget = self.tab_clone(bg, window)
|
||||
else:
|
||||
widget = self._current_widget()
|
||||
|
||||
history = widget.page().history()
|
||||
for _ in range(count):
|
||||
if forward:
|
||||
if not history.canGoForward():
|
||||
raise cmdexc.CommandError("At end of history.")
|
||||
widget.forward()
|
||||
else:
|
||||
if not history.canGoBack():
|
||||
raise cmdexc.CommandError("At beginning of history.")
|
||||
widget.back()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
@ -538,7 +521,7 @@ class CommandDispatcher:
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
scope='window', count='count')
|
||||
def scroll_px(self, dx: {'type': float}, dy: {'type': float}, count=1):
|
||||
def scroll_px(self, dx: {'type': int}, dy: {'type': int}, count=1):
|
||||
"""Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|
||||
Args:
|
||||
@ -555,8 +538,8 @@ class CommandDispatcher:
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
scope='window', count='count')
|
||||
def scroll(self,
|
||||
direction: {'type': (str, float)},
|
||||
dy: {'type': float, 'hide': True}=None,
|
||||
direction: {'type': (str, int)},
|
||||
dy: {'type': int, 'hide': True}=None,
|
||||
count=1):
|
||||
"""Scroll the current tab in the given direction.
|
||||
|
||||
@ -569,8 +552,8 @@ class CommandDispatcher:
|
||||
# pylint: disable=too-many-locals
|
||||
try:
|
||||
# Check for deprecated dx/dy form (like with scroll-px).
|
||||
dx = float(direction)
|
||||
dy = float(dy)
|
||||
dx = int(direction)
|
||||
dy = int(dy)
|
||||
except (ValueError, TypeError):
|
||||
# Invalid values will get handled later.
|
||||
pass
|
||||
@ -643,8 +626,24 @@ class CommandDispatcher:
|
||||
horizontal: Scroll horizontally instead of vertically.
|
||||
count: Percentage to scroll.
|
||||
"""
|
||||
self._scroll_percent(perc, count,
|
||||
Qt.Horizontal if horizontal else Qt.Vertical)
|
||||
if perc is None and count is None:
|
||||
perc = 100
|
||||
elif perc is None:
|
||||
perc = count
|
||||
|
||||
orientation = Qt.Horizontal if horizontal else Qt.Vertical
|
||||
|
||||
if perc == 0 and orientation == Qt.Vertical:
|
||||
self.scroll('top')
|
||||
elif perc == 100 and orientation == Qt.Vertical:
|
||||
self.scroll('bottom')
|
||||
else:
|
||||
perc = qtutils.check_overflow(perc, 'int', fatal=False)
|
||||
frame = self._current_widget().page().currentFrame()
|
||||
m = frame.scrollBarMaximum(orientation)
|
||||
if m == 0:
|
||||
return
|
||||
frame.setScrollBarValue(orientation, int(m * perc / 100))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
scope='window', count='count')
|
||||
@ -686,7 +685,7 @@ class CommandDispatcher:
|
||||
pass
|
||||
elif mult_y < 0:
|
||||
self.scroll('page-up', count=-int(mult_y))
|
||||
elif mult_y > 0:
|
||||
elif mult_y > 0: # pragma: no branch
|
||||
self.scroll('page-down', count=int(mult_y))
|
||||
mult_y = 0
|
||||
if mult_x == 0 and mult_y == 0:
|
||||
|
@ -29,7 +29,8 @@ from qutebrowser.utils import message, log, objreg, qtutils
|
||||
from qutebrowser.misc import split
|
||||
|
||||
|
||||
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline')
|
||||
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
|
||||
'count'])
|
||||
|
||||
|
||||
def replace_variables(win_id, arglist):
|
||||
@ -117,6 +118,26 @@ class CommandRunner(QObject):
|
||||
for sub in sub_texts:
|
||||
yield self.parse(sub, *args, **kwargs)
|
||||
|
||||
def _parse_count(self, cmdstr):
|
||||
"""Split a count prefix off from a command for parse().
|
||||
|
||||
Args:
|
||||
cmdstr: The command/args including the count.
|
||||
|
||||
Return:
|
||||
A (count, cmdstr) tuple, with count being None or int.
|
||||
"""
|
||||
if ':' not in cmdstr:
|
||||
return (None, cmdstr)
|
||||
|
||||
count, cmdstr = cmdstr.split(':', maxsplit=1)
|
||||
try:
|
||||
count = int(count)
|
||||
except ValueError:
|
||||
# We just ignore invalid prefixes
|
||||
count = None
|
||||
return (count, cmdstr)
|
||||
|
||||
def parse(self, text, *, aliases=True, fallback=False, keep=False):
|
||||
"""Split the commandline text into command and arguments.
|
||||
|
||||
@ -128,9 +149,11 @@ class CommandRunner(QObject):
|
||||
keep: Whether to keep special chars and whitespace
|
||||
|
||||
Return:
|
||||
A (cmd, args, cmdline) ParseResult tuple.
|
||||
A ParseResult tuple.
|
||||
"""
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
count, cmdstr = self._parse_count(cmdstr)
|
||||
|
||||
if not cmdstr and not fallback:
|
||||
raise cmdexc.NoSuchCommandError("No command given")
|
||||
if aliases:
|
||||
@ -161,7 +184,7 @@ class CommandRunner(QObject):
|
||||
cmdline = [cmdstr, sep]
|
||||
else:
|
||||
cmdline = [cmdstr] + args[:]
|
||||
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
|
||||
return ParseResult(cmd=cmd, args=args, cmdline=cmdline, count=count)
|
||||
|
||||
def _split_args(self, cmd, argstr, keep):
|
||||
"""Split the arguments from an arg string.
|
||||
@ -216,7 +239,12 @@ class CommandRunner(QObject):
|
||||
for result in self.parse_all(text):
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if count is not None:
|
||||
if result.count is not None:
|
||||
raise cmdexc.CommandMetaError("Got count via command and "
|
||||
"prefix!")
|
||||
result.cmd.run(self._win_id, args, count=count)
|
||||
elif result.count is not None:
|
||||
result.cmd.run(self._win_id, args, count=result.count)
|
||||
else:
|
||||
result.cmd.run(self._win_id, args)
|
||||
|
||||
|
@ -37,7 +37,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Sections")
|
||||
for name in configdata.DATA.keys():
|
||||
for name in configdata.DATA:
|
||||
desc = configdata.SECTION_DESC[name].splitlines()[0].strip()
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
@ -62,7 +62,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
|
||||
self._misc_items = {}
|
||||
self._section = section
|
||||
objreg.get('config').changed.connect(self.update_misc_column)
|
||||
for name in sectdata.keys():
|
||||
for name in sectdata:
|
||||
try:
|
||||
desc = sectdata.descriptions[name]
|
||||
except (KeyError, AttributeError):
|
||||
|
@ -70,7 +70,7 @@ def _init_setting_completions():
|
||||
model = configmodel.SettingOptionCompletionModel(sectname)
|
||||
_instances[usertypes.Completion.option][sectname] = model
|
||||
_instances[usertypes.Completion.value][sectname] = {}
|
||||
for opt in configdata.DATA[sectname].keys():
|
||||
for opt in configdata.DATA[sectname]:
|
||||
model = configmodel.SettingValueCompletionModel(sectname, opt)
|
||||
_instances[usertypes.Completion.value][sectname][opt] = model
|
||||
|
||||
|
@ -77,7 +77,7 @@ class HelpCompletionModel(base.BaseCompletionModel):
|
||||
"""Fill completion with section->option entries."""
|
||||
cat = self.new_category("Settings")
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata.keys():
|
||||
for optname in sectdata:
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
|
@ -368,7 +368,7 @@ class ConfigManager(QObject):
|
||||
self.sections = configdata.data()
|
||||
self._interpolation = configparser.ExtendedInterpolation()
|
||||
self._proxies = {}
|
||||
for sectname in self.sections.keys():
|
||||
for sectname in self.sections:
|
||||
self._proxies[sectname] = SectionProxy(self, sectname)
|
||||
self._fname = fname
|
||||
if configdir is None:
|
||||
|
@ -1569,49 +1569,41 @@ class UserAgent(BaseType):
|
||||
def validate(self, value):
|
||||
self._basic_validation(value)
|
||||
|
||||
# To update the following list of user agents, run the script 'ua_fetch.py'
|
||||
# Vim-protip: Place your cursor below this comment and run
|
||||
# :r!python scripts/dev/ua_fetch.py
|
||||
def complete(self):
|
||||
"""Complete a list of common user agents."""
|
||||
out = [
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
|
||||
'Firefox/35.0',
|
||||
"Firefox 35.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
|
||||
'Firefox/35.0',
|
||||
"Firefox 35.0 Ubuntu"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
|
||||
'Gecko/20100101 Firefox/35.0',
|
||||
"Firefox 35.0 MacOSX"),
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 '
|
||||
'Firefox/41.0',
|
||||
"Firefox 41.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) '
|
||||
'Gecko/20100101 Firefox/41.0',
|
||||
"Firefox 41.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64; rv:41.0) Gecko/20100101 '
|
||||
'Firefox/41.0',
|
||||
"Firefox 41.0 Linux"),
|
||||
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
||||
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
|
||||
'Safari/600.3.18',
|
||||
"Safari 8.0 MacOSX"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) '
|
||||
'AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 '
|
||||
'Safari/601.2.7',
|
||||
"Safari Generic MacOSX"),
|
||||
('Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) '
|
||||
'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 '
|
||||
'Mobile/13B143 Safari/601.1',
|
||||
"Mobile Safari Generic iOS"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
|
||||
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
||||
"Chrome 40.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
|
||||
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
|
||||
"Chrome 46.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 '
|
||||
'Safari/537.36',
|
||||
"Chrome 40.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
||||
"Chrome 40.0 Linux"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||
'Gecko',
|
||||
"IE 11.0 Win7 64-bit"),
|
||||
|
||||
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
|
||||
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
|
||||
'Mobile/12B440 Safari/600.1.4',
|
||||
"Mobile Safari 8.0 iOS"),
|
||||
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
|
||||
"Firefox 35, Android"),
|
||||
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
|
||||
'Chrome/37.0.0.0 Mobile Safari/537.36',
|
||||
"Android Browser"),
|
||||
"Chrome 46.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, '
|
||||
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
|
||||
"Chrome 46.0 Linux"),
|
||||
|
||||
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
||||
'+http://www.google.com/bot.html',
|
||||
@ -1619,7 +1611,11 @@ class UserAgent(BaseType):
|
||||
('Wget/1.16.1 (linux-gnu)',
|
||||
"wget 1.16.1"),
|
||||
('curl/7.40.0',
|
||||
"curl 7.40.0")
|
||||
"curl 7.40.0"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||
'Gecko',
|
||||
"IE 11.0 for Desktop Win7 64-bit")
|
||||
]
|
||||
return out
|
||||
|
||||
|
@ -273,10 +273,8 @@ class KeyConfigParser(QObject):
|
||||
return True
|
||||
if keychain in bindings:
|
||||
return False
|
||||
elif command in bindings.values():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return command not in bindings.values()
|
||||
|
||||
def _read(self, relaxed=False):
|
||||
"""Read the config file from disk and parse it.
|
||||
|
@ -371,10 +371,8 @@ class QtWarningFilter(logging.Filter):
|
||||
|
||||
def filter(self, record):
|
||||
"""Determine if the specified record is to be logged."""
|
||||
if record.msg.strip().startswith(self._pattern):
|
||||
return False # filter
|
||||
else:
|
||||
return True # log
|
||||
do_log = not record.msg.strip().startswith(self._pattern)
|
||||
return do_log
|
||||
|
||||
|
||||
class LogFilter(logging.Filter):
|
||||
|
@ -122,10 +122,7 @@ def _is_url_naive(urlstr):
|
||||
if not QHostAddress(urlstr).isNull():
|
||||
return False
|
||||
|
||||
if '.' in url.host():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return '.' in url.host()
|
||||
|
||||
|
||||
def _is_url_dns(urlstr):
|
||||
@ -254,10 +251,7 @@ def is_url(urlstr):
|
||||
# no autosearch, so everything is a URL unless it has an explicit
|
||||
# search engine.
|
||||
engine, _term = _parse_search_term(urlstr)
|
||||
if engine is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return engine is None
|
||||
|
||||
if not qurl_userinput.isValid():
|
||||
# This will also catch URLs containing spaces.
|
||||
|
@ -20,60 +20,213 @@
|
||||
|
||||
"""Generate the html documentation based on the asciidoc files."""
|
||||
|
||||
import re
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import subprocess
|
||||
import glob
|
||||
import shutil
|
||||
import tempfile
|
||||
import argparse
|
||||
import io
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from scripts import utils
|
||||
|
||||
|
||||
def _get_asciidoc_cmd(args):
|
||||
"""Try to find out what commandline to use to invoke asciidoc."""
|
||||
if args.asciidoc is not None:
|
||||
return args.asciidoc
|
||||
class AsciiDoc:
|
||||
|
||||
try:
|
||||
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
return ['asciidoc']
|
||||
"""Abstraction of an asciidoc subprocess."""
|
||||
|
||||
try:
|
||||
subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
return ['asciidoc.py']
|
||||
FILES = [
|
||||
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
|
||||
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
|
||||
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
|
||||
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
|
||||
]
|
||||
|
||||
raise FileNotFoundError
|
||||
def __init__(self, args):
|
||||
self._cmd = None
|
||||
self._args = args
|
||||
self._homedir = None
|
||||
self._themedir = None
|
||||
self._tempdir = None
|
||||
self._failed = False
|
||||
|
||||
def prepare(self):
|
||||
"""Get the asciidoc command and create the homedir to use."""
|
||||
self._cmd = self._get_asciidoc_cmd()
|
||||
self._homedir = tempfile.mkdtemp()
|
||||
self._themedir = os.path.join(
|
||||
self._homedir, '.asciidoc', 'themes', 'qute')
|
||||
self._tempdir = os.path.join(self._homedir, 'tmp')
|
||||
os.makedirs(self._tempdir)
|
||||
os.makedirs(self._themedir)
|
||||
|
||||
def call_asciidoc(args, src, dst):
|
||||
"""Call asciidoc for the given files.
|
||||
def cleanup(self):
|
||||
"""Clean up the temporary home directory for asciidoc."""
|
||||
if self._homedir is not None and not self._failed:
|
||||
shutil.rmtree(self._homedir)
|
||||
|
||||
Args:
|
||||
args: The asciidoc binary to use, as a list.
|
||||
src: The source .asciidoc file.
|
||||
dst: The destination .html file, or None to auto-guess.
|
||||
"""
|
||||
print("Calling asciidoc for {}...".format(os.path.basename(src)))
|
||||
args = args[:]
|
||||
if dst is not None:
|
||||
args += ['--out-file', dst]
|
||||
args.append(src)
|
||||
try:
|
||||
subprocess.check_call(args)
|
||||
except (subprocess.CalledProcessError, OSError) as e:
|
||||
utils.print_col(str(e), 'red')
|
||||
sys.exit(1)
|
||||
def build(self):
|
||||
if self._args.website:
|
||||
self._build_website()
|
||||
else:
|
||||
self._build_docs()
|
||||
|
||||
def _build_docs(self):
|
||||
"""Render .asciidoc files to .html sites."""
|
||||
files = self.FILES[:]
|
||||
for src in glob.glob('doc/help/*.asciidoc'):
|
||||
name, _ext = os.path.splitext(os.path.basename(src))
|
||||
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
||||
files.append((src, dst))
|
||||
for src, dst in files:
|
||||
self.call(src, dst)
|
||||
|
||||
def _build_website_file(self, root, filename):
|
||||
"""Build a single website file."""
|
||||
# pylint: disable=too-many-locals
|
||||
src = os.path.join(root, filename)
|
||||
src_basename = os.path.basename(src)
|
||||
parts = [self._args.website[0]]
|
||||
dirname = os.path.dirname(src)
|
||||
if dirname:
|
||||
parts.append(os.path.relpath(os.path.dirname(src)))
|
||||
parts.append(
|
||||
os.extsep.join((os.path.splitext(src_basename)[0],
|
||||
'html')))
|
||||
dst = os.path.join(*parts)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
|
||||
modified_src = os.path.join(self._tempdir, src_basename)
|
||||
shutil.copy('www/header.asciidoc', modified_src)
|
||||
|
||||
outfp = io.StringIO()
|
||||
|
||||
with open(modified_src, 'r', encoding='utf-8') as header_file:
|
||||
header = header_file.read()
|
||||
header += "\n\n"
|
||||
|
||||
with open(src, 'r', encoding='utf-8') as infp:
|
||||
outfp.write("\n\n")
|
||||
hidden = False
|
||||
found_title = False
|
||||
title = ""
|
||||
last_line = ""
|
||||
|
||||
for line in infp:
|
||||
if line.strip() == '// QUTE_WEB_HIDE':
|
||||
assert not hidden
|
||||
hidden = True
|
||||
elif line.strip() == '// QUTE_WEB_HIDE_END':
|
||||
assert hidden
|
||||
hidden = False
|
||||
elif line == "The Compiler <mail@qutebrowser.org>\n":
|
||||
continue
|
||||
elif re.match(r'^:\w+:.*', line):
|
||||
# asciidoc field
|
||||
continue
|
||||
|
||||
if not found_title:
|
||||
if re.match(r'^=+$', line):
|
||||
line = line.replace('=', '-')
|
||||
found_title = True
|
||||
title = last_line + "=" * (len(last_line) - 1)
|
||||
elif re.match(r'^= .+', line):
|
||||
line = '==' + line[1:]
|
||||
found_title = True
|
||||
title = last_line + "=" * (len(last_line) - 1)
|
||||
|
||||
if not hidden:
|
||||
outfp.write(line.replace(".asciidoc[", ".html["))
|
||||
last_line = line
|
||||
|
||||
current_lines = outfp.getvalue()
|
||||
outfp.close()
|
||||
|
||||
with open(modified_src, 'w+', encoding='utf-8') as final_version:
|
||||
final_version.write(title + "\n\n" + header + current_lines)
|
||||
|
||||
self.call(modified_src, dst, '--theme=qute')
|
||||
|
||||
def _build_website(self):
|
||||
"""Prepare and build the website."""
|
||||
theme_file = os.path.abspath(os.path.join('www', 'qute.css'))
|
||||
shutil.copy(theme_file, self._themedir)
|
||||
|
||||
outdir = self._args.website[0]
|
||||
|
||||
for root, _dirs, files in os.walk(os.getcwd()):
|
||||
for filename in files:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if (ext != '.asciidoc' or
|
||||
basename in ('header', 'OpenSans-License')):
|
||||
continue
|
||||
self._build_website_file(root, filename)
|
||||
|
||||
copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'}
|
||||
|
||||
for src, dest in copy.items():
|
||||
full_dest = os.path.join(outdir, dest)
|
||||
try:
|
||||
shutil.rmtree(full_dest)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
shutil.copytree(src, full_dest)
|
||||
|
||||
try:
|
||||
os.symlink('README.html', os.path.join(outdir, 'index.html'))
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
def _get_asciidoc_cmd(self):
|
||||
"""Try to find out what commandline to use to invoke asciidoc."""
|
||||
if self._args.asciidoc is not None:
|
||||
return self._args.asciidoc
|
||||
|
||||
try:
|
||||
subprocess.call(['asciidoc'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
return ['asciidoc']
|
||||
|
||||
try:
|
||||
subprocess.call(['asciidoc.py'], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
return ['asciidoc.py']
|
||||
|
||||
raise FileNotFoundError
|
||||
|
||||
def call(self, src, dst, *args):
|
||||
"""Call asciidoc for the given files.
|
||||
|
||||
Args:
|
||||
src: The source .asciidoc file.
|
||||
dst: The destination .html file, or None to auto-guess.
|
||||
*args: Additional arguments passed to asciidoc.
|
||||
"""
|
||||
print("Calling asciidoc for {}...".format(os.path.basename(src)))
|
||||
cmdline = self._cmd[:]
|
||||
if dst is not None:
|
||||
cmdline += ['--out-file', dst]
|
||||
cmdline += args
|
||||
cmdline.append(src)
|
||||
try:
|
||||
subprocess.check_call(cmdline, env={'HOME': self._homedir})
|
||||
self._failed = True
|
||||
except (subprocess.CalledProcessError, OSError) as e:
|
||||
self._failed = True
|
||||
utils.print_col(str(e), 'red')
|
||||
print("Keeping modified sources in {}.".format(self._homedir))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(colors=False):
|
||||
@ -81,53 +234,31 @@ def main(colors=False):
|
||||
utils.change_cwd()
|
||||
utils.use_color = colors
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--all', help="Build all documentation into a given "
|
||||
parser.add_argument('--website', help="Build website into a given "
|
||||
"directory.", nargs=1)
|
||||
parser.add_argument('--asciidoc', help="Full path to python and "
|
||||
"asciidoc.py. If not given, it's searched in PATH.",
|
||||
nargs=2, required=False,
|
||||
metavar=('PYTHON', 'ASCIIDOC'))
|
||||
args = parser.parse_args()
|
||||
asciidoc_files = [
|
||||
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
|
||||
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
|
||||
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
|
||||
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
|
||||
]
|
||||
try:
|
||||
os.mkdir('qutebrowser/html/doc')
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
asciidoc = AsciiDoc(args)
|
||||
try:
|
||||
asciidoc = _get_asciidoc_cmd(args)
|
||||
asciidoc.prepare()
|
||||
except FileNotFoundError:
|
||||
utils.print_col("Could not find asciidoc! Please install it, or use "
|
||||
"the --asciidoc argument to point this script to the "
|
||||
"correct python/asciidoc.py location!", 'red')
|
||||
sys.exit(1)
|
||||
if args.all:
|
||||
for root, _dirs, files in os.walk(os.getcwd()):
|
||||
for filename in files:
|
||||
if os.path.splitext(filename)[1] != '.asciidoc':
|
||||
continue
|
||||
src = os.path.join(root, filename)
|
||||
parts = [args.all[0]]
|
||||
dirname = os.path.dirname(src)
|
||||
if dirname:
|
||||
parts.append(os.path.relpath(os.path.dirname(src)))
|
||||
parts.append(
|
||||
os.extsep.join((os.path.splitext(os.path.basename(src))[0],
|
||||
'html')))
|
||||
dst = os.path.join(*parts)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
call_asciidoc(asciidoc, src, dst)
|
||||
else:
|
||||
for src in glob.glob('doc/help/*.asciidoc'):
|
||||
name, _ext = os.path.splitext(os.path.basename(src))
|
||||
dst = 'qutebrowser/html/doc/{}.html'.format(name)
|
||||
asciidoc_files.append((src, dst))
|
||||
for src, dst in asciidoc_files:
|
||||
call_asciidoc(asciidoc, src, dst)
|
||||
|
||||
try:
|
||||
asciidoc.build()
|
||||
finally:
|
||||
asciidoc.cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -34,7 +34,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
|
||||
from scripts import utils
|
||||
|
||||
|
||||
Message = collections.namedtuple('Message', 'typ, text')
|
||||
MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file')
|
||||
|
||||
@ -143,6 +142,18 @@ class Skipped(Exception):
|
||||
super().__init__("Skipping coverage checks " + reason)
|
||||
|
||||
|
||||
def _get_filename(filename):
|
||||
"""Transform the absolute test filenames to relative ones."""
|
||||
if os.path.isabs(filename):
|
||||
basedir = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
common_path = os.path.commonprefix([basedir, filename])
|
||||
if common_path:
|
||||
filename = filename[len(common_path):].lstrip('/')
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def check(fileobj, perfect_files):
|
||||
"""Main entry point which parses/checks coverage.xml if applicable."""
|
||||
if sys.platform != 'linux':
|
||||
@ -151,6 +162,8 @@ def check(fileobj, perfect_files):
|
||||
raise Skipped("because -k is given.")
|
||||
elif '-m' in sys.argv[1:]:
|
||||
raise Skipped("because -m is given.")
|
||||
elif '--lf' in sys.argv[1:]:
|
||||
raise Skipped("because --lf is given.")
|
||||
|
||||
perfect_src_files = [e[1] for e in perfect_files]
|
||||
|
||||
@ -168,7 +181,8 @@ def check(fileobj, perfect_files):
|
||||
messages = []
|
||||
|
||||
for klass in classes:
|
||||
filename = klass.attrib['filename']
|
||||
filename = _get_filename(klass.attrib['filename'])
|
||||
|
||||
line_cov = float(klass.attrib['line-rate']) * 100
|
||||
branch_cov = float(klass.attrib['branch-rate']) * 100
|
||||
|
||||
|
@ -44,7 +44,7 @@ INSTALL_PYQT = TESTENV in ('py34', 'py35', 'unittests-nodisp', 'vulture',
|
||||
'pylint')
|
||||
XVFB = TRAVIS_OS == 'linux' and TESTENV == 'py34'
|
||||
pip_packages = ['tox']
|
||||
if TESTENV in ['py34', 'py35']:
|
||||
if TESTENV in ['py34', 'py35'] and TRAVIS_OS == 'linux':
|
||||
pip_packages.append('codecov')
|
||||
|
||||
|
||||
@ -69,9 +69,12 @@ def check_setup(executable):
|
||||
|
||||
if 'APPVEYOR' in os.environ:
|
||||
print("Getting PyQt5...")
|
||||
urllib.urlretrieve(
|
||||
'http://www.qutebrowser.org/pyqt/PyQt5-5.5-gpl-Py3.4-Qt5.5.0-x32.exe',
|
||||
r'C:\install-PyQt5.exe')
|
||||
qt_version = '5.5.1'
|
||||
pyqt_version = '5.5.1'
|
||||
pyqt_url = ('http://www.qutebrowser.org/pyqt/'
|
||||
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
|
||||
pyqt_version, qt_version))
|
||||
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
|
||||
|
||||
print("Fixing registry...")
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
|
@ -93,10 +93,8 @@ def is_qutebrowser_dump(parsed):
|
||||
return True
|
||||
else:
|
||||
return '-m qutebrowser' in cmdline
|
||||
elif basename == 'qutebrowser':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return basename == 'qutebrowser'
|
||||
|
||||
|
||||
def dump_infos_gdb(parsed):
|
||||
|
@ -27,7 +27,7 @@ import sys
|
||||
import pytest
|
||||
import pytestqt.plugin
|
||||
import pytest_mock
|
||||
import pytest_capturelog
|
||||
import pytest_catchlog
|
||||
|
||||
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
|
||||
pytest_capturelog]))
|
||||
pytest_catchlog]))
|
||||
|
@ -111,11 +111,7 @@ def filter_func(item):
|
||||
True if the missing function should be filtered/ignored, False
|
||||
otherwise.
|
||||
"""
|
||||
if re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)):
|
||||
# probably a virtual Qt method
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item)))
|
||||
|
||||
|
||||
def report(items):
|
||||
|
@ -389,6 +389,8 @@ def _get_authors():
|
||||
'binix': 'sbinix',
|
||||
'Averrin': 'Alexey "Averrin" Nabrodov',
|
||||
'Alexey Nabrodov': 'Alexey "Averrin" Nabrodov',
|
||||
'Michael': 'Halfwit',
|
||||
'Error 800': 'error800',
|
||||
}
|
||||
commits = subprocess.check_output(['git', 'log', '--format=%aN'])
|
||||
authors = [corrections.get(author, author)
|
||||
|
124
scripts/dev/ua_fetch.py
Normal file
124
scripts/dev/ua_fetch.py
Normal file
@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 lamarpavel
|
||||
# Copyright 2015 Alexey Nabrodov (Averrin)
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Fetch list of popular user-agents.
|
||||
|
||||
The script is based on a gist posted by github.com/averrin, the output of this
|
||||
script is formatted to be pasted into configtypes.py.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from lxml import html # pylint: disable=import-error
|
||||
|
||||
|
||||
def fetch():
|
||||
"""Fetch list of popular user-agents.
|
||||
|
||||
Return:
|
||||
List of relevant strings.
|
||||
"""
|
||||
url = 'https://techblog.willshouse.com/2012/01/03/most-common-user-agents/'
|
||||
page = requests.get(url)
|
||||
page = html.fromstring(page.text)
|
||||
path = '//*[@id="post-2229"]/div[2]/table/tbody'
|
||||
return page.xpath(path)[0]
|
||||
|
||||
|
||||
def filter_list(complete_list, browsers):
|
||||
"""Filter the received list based on a look up table.
|
||||
|
||||
The LUT should be a dictionary of the format {browser: versions}, where
|
||||
'browser' is the name of the browser (eg. "Firefox") as string and
|
||||
'versions' is a set of different versions of this browser that should be
|
||||
included when found (eg. {"Linux", "MacOSX"}). This function returns a
|
||||
dictionary with the same keys as the LUT, but storing lists of tuples
|
||||
(user_agent, browser_description) as values.
|
||||
"""
|
||||
table = {}
|
||||
for entry in complete_list:
|
||||
# Tuple of (user_agent, browser_description)
|
||||
candidate = (entry[1].text_content(), entry[2].text_content())
|
||||
for name in browsers:
|
||||
found = False
|
||||
if name.lower() in candidate[1].lower():
|
||||
for version in browsers[name]:
|
||||
if version.lower() in candidate[1].lower():
|
||||
if table.get(name) is None:
|
||||
table[name] = []
|
||||
table[name].append(candidate)
|
||||
browsers[name].remove(version)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
return table
|
||||
|
||||
|
||||
def add_diversity(table):
|
||||
"""Insert a few additional entries for diversity into the dict.
|
||||
|
||||
(as returned by filter_list())
|
||||
"""
|
||||
table["Obscure"] = [
|
||||
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
||||
'+http://www.google.com/bot.html',
|
||||
"Google Bot"),
|
||||
('Wget/1.16.1 (linux-gnu)',
|
||||
"wget 1.16.1"),
|
||||
('curl/7.40.0',
|
||||
"curl 7.40.0")
|
||||
]
|
||||
return table
|
||||
|
||||
|
||||
def main():
|
||||
"""Generate user agent code."""
|
||||
fetched = fetch()
|
||||
lut = {
|
||||
"Firefox": {"Win", "MacOSX", "Linux", "Android"},
|
||||
"Chrome": {"Win", "MacOSX", "Linux"},
|
||||
"Safari": {"MacOSX", "iOS"}
|
||||
}
|
||||
filtered = filter_list(fetched, lut)
|
||||
filtered = add_diversity(filtered)
|
||||
|
||||
tab = " "
|
||||
print(tab + "def complete(self):")
|
||||
print((2 * tab) + "\"\"\"Complete a list of common user agents.\"\"\"")
|
||||
print((2 * tab) + "%sout = [")
|
||||
|
||||
for browser in ["Firefox", "Safari", "Chrome", "Obscure"]:
|
||||
for it in filtered[browser]:
|
||||
print("{}(\'{}\',\n{} \"{}\"),".format(3 * tab, it[0],
|
||||
3 * tab, it[1]))
|
||||
print("")
|
||||
|
||||
print("""\
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||
'Gecko',
|
||||
"IE 11.0 for Desktop Win7 64-bit")""")
|
||||
|
||||
print("{}]\n{}return out\n".format(2 * tab, 2 * tab))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -33,7 +33,7 @@ import pytest
|
||||
|
||||
import helpers.stubs as stubsmod
|
||||
from helpers import logfail
|
||||
from helpers.logfail import fail_on_logging, caplog_bug_workaround
|
||||
from helpers.logfail import fail_on_logging
|
||||
from helpers.messagemock import message_mock
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg
|
||||
@ -121,11 +121,8 @@ def pytest_collection_modifyitems(items):
|
||||
def pytest_ignore_collect(path):
|
||||
"""Ignore BDD tests during collection if frozen."""
|
||||
rel_path = path.relto(os.path.dirname(__file__))
|
||||
if (rel_path == os.path.join('integration', 'features') and
|
||||
hasattr(sys, 'frozen')):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return (rel_path == os.path.join('integration', 'features') and
|
||||
hasattr(sys, 'frozen'))
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
|
@ -24,7 +24,7 @@ import logging
|
||||
import pytest
|
||||
|
||||
try:
|
||||
import pytest_capturelog as caplog_mod
|
||||
import pytest_catchlog as catchlog_mod
|
||||
except ImportError:
|
||||
# When using pytest for pyflakes/pep8/..., the plugin won't be available
|
||||
# but conftest.py will still be loaded.
|
||||
@ -47,18 +47,18 @@ class LogFailHandler(logging.Handler):
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
for h in root_logger.handlers:
|
||||
if isinstance(h, caplog_mod.CaptureLogHandler):
|
||||
caplog_handler = h
|
||||
if isinstance(h, catchlog_mod.LogCaptureHandler):
|
||||
catchlog_handler = h
|
||||
break
|
||||
else:
|
||||
# The CaptureLogHandler is not available anymore during fixture
|
||||
# The LogCaptureHandler is not available anymore during fixture
|
||||
# teardown, so we ignore logging messages emitted there..
|
||||
return
|
||||
|
||||
if (logger.level == record.levelno or
|
||||
caplog_handler.level == record.levelno):
|
||||
# caplog.atLevel(...) was used with the level of this message, i.e.
|
||||
# it was expected.
|
||||
catchlog_handler.level == record.levelno):
|
||||
# caplog.at_level(...) was used with the level of this message,
|
||||
# i.e. it was expected.
|
||||
return
|
||||
if record.levelno < self._min_level:
|
||||
return
|
||||
@ -74,25 +74,3 @@ def fail_on_logging():
|
||||
yield
|
||||
logging.getLogger().removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
|
||||
@pytest.yield_fixture(autouse=True)
|
||||
def caplog_bug_workaround(request):
|
||||
"""WORKAROUND for pytest-capturelog bug.
|
||||
|
||||
https://bitbucket.org/memedough/pytest-capturelog/issues/7/
|
||||
|
||||
This would lead to LogFailHandler failing after skipped tests as there are
|
||||
multiple CaptureLogHandlers.
|
||||
"""
|
||||
yield
|
||||
if caplog_mod is None:
|
||||
return
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
caplog_handlers = [h for h in root_logger.handlers
|
||||
if isinstance(h, caplog_mod.CaptureLogHandler)]
|
||||
|
||||
for h in caplog_handlers:
|
||||
root_logger.removeHandler(h)
|
||||
h.close()
|
||||
|
@ -61,7 +61,7 @@ class MessageMock:
|
||||
}
|
||||
log_level = log_levels[level]
|
||||
|
||||
with self._caplog.atLevel(log_level): # needed so we don't fail
|
||||
with self._caplog.at_level(log_level): # needed so we don't fail
|
||||
logging.getLogger('message').log(log_level, text)
|
||||
|
||||
self.messages.append(Message(level, win_id, text, immediately))
|
||||
|
@ -48,3 +48,24 @@ def test_partial_compare_equal(val1, val2):
|
||||
])
|
||||
def test_partial_compare_not_equal(val1, val2):
|
||||
assert not utils.partial_compare(val1, val2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, value, expected', [
|
||||
('foo', 'foo', True),
|
||||
('foo', 'bar', False),
|
||||
('foo', 'Foo', False),
|
||||
('foo', 'foobar', False),
|
||||
('foo', 'barfoo', False),
|
||||
|
||||
('foo*', 'foobarbaz', True),
|
||||
('*bar', 'foobar', True),
|
||||
('foo*baz', 'foobarbaz', True),
|
||||
|
||||
('foo[b]ar', 'foobar', False),
|
||||
('foo[b]ar', 'foo[b]ar', True),
|
||||
|
||||
('foo?ar', 'foobar', False),
|
||||
('foo?ar', 'foo?ar', True),
|
||||
])
|
||||
def test_pattern_match(pattern, value, expected):
|
||||
assert utils.pattern_match(pattern=pattern, value=value) == expected
|
||||
|
@ -23,7 +23,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import pytest_capturelog # pylint: disable=import-error
|
||||
import pytest_catchlog # pylint: disable=import-error
|
||||
|
||||
|
||||
def test_log_debug():
|
||||
@ -36,33 +36,33 @@ def test_log_warning():
|
||||
|
||||
|
||||
def test_log_expected(caplog):
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
logging.error('foo')
|
||||
|
||||
|
||||
def test_log_expected_logger(caplog):
|
||||
logger = 'logfail_test_logger'
|
||||
with caplog.atLevel(logging.ERROR, logger):
|
||||
with caplog.at_level(logging.ERROR, logger):
|
||||
logging.getLogger(logger).error('foo')
|
||||
|
||||
|
||||
def test_log_expected_wrong_level(caplog):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
logging.critical('foo')
|
||||
|
||||
|
||||
def test_log_expected_logger_wrong_level(caplog):
|
||||
logger = 'logfail_test_logger'
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
with caplog.atLevel(logging.ERROR, logger):
|
||||
with caplog.at_level(logging.ERROR, logger):
|
||||
logging.getLogger(logger).critical('foo')
|
||||
|
||||
|
||||
def test_log_expected_wrong_logger(caplog):
|
||||
logger = 'logfail_test_logger'
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
with caplog.atLevel(logging.ERROR, logger):
|
||||
with caplog.at_level(logging.ERROR, logger):
|
||||
logging.error('foo')
|
||||
|
||||
|
||||
@ -82,6 +82,6 @@ def test_caplog_bug_workaround_2():
|
||||
"""
|
||||
caplog_handler = None
|
||||
for h in logging.getLogger().handlers:
|
||||
if isinstance(h, pytest_capturelog.CaptureLogHandler):
|
||||
if isinstance(h, pytest_catchlog.LogCaptureHandler):
|
||||
assert caplog_handler is None
|
||||
caplog_handler = h
|
||||
|
@ -20,32 +20,39 @@
|
||||
"""Partial comparison of dicts/lists."""
|
||||
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
import pprint
|
||||
|
||||
|
||||
def _partial_compare_dict(val1, val2):
|
||||
def print_i(text, indent, error=False):
|
||||
if error:
|
||||
text = '| ****** {} ******'.format(text)
|
||||
for line in text.splitlines():
|
||||
print('| ' * indent + line)
|
||||
|
||||
|
||||
def _partial_compare_dict(val1, val2, *, indent=0):
|
||||
for key in val2:
|
||||
if key not in val1:
|
||||
print("Key {!r} is in second dict but not in first!".format(key))
|
||||
print_i("Key {!r} is in second dict but not in first!".format(key),
|
||||
indent, error=True)
|
||||
return False
|
||||
if not partial_compare(val1[key], val2[key]):
|
||||
print("Comparison failed for {!r} and {!r}!".format(
|
||||
val1[key], val2[key]))
|
||||
if not partial_compare(val1[key], val2[key], indent=indent+1):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _partial_compare_list(val1, val2):
|
||||
def _partial_compare_list(val1, val2, *, indent=0):
|
||||
if len(val1) < len(val2):
|
||||
print("Second list is longer than first list -> False!")
|
||||
print_i("Second list is longer than first list", indent, error=True)
|
||||
return False
|
||||
for item1, item2 in zip(val1, val2):
|
||||
if not partial_compare(item1, item2):
|
||||
if not partial_compare(item1, item2, indent=indent+1):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def partial_compare(val1, val2):
|
||||
def partial_compare(val1, val2, *, indent=0):
|
||||
"""Do a partial comparison between the given values.
|
||||
|
||||
For dicts, keys in val2 are checked, others are ignored.
|
||||
@ -54,31 +61,44 @@ def partial_compare(val1, val2):
|
||||
|
||||
This happens recursively.
|
||||
"""
|
||||
print()
|
||||
print("Comparing\n {!r}\nto\n {!r}".format(val1, val2))
|
||||
print_i("Comparing", indent)
|
||||
print_i(pprint.pformat(val1), indent + 1)
|
||||
print_i("|---- to ----", indent)
|
||||
print_i(pprint.pformat(val2), indent + 1)
|
||||
|
||||
|
||||
if val2 is Ellipsis:
|
||||
print("Ignoring ellipsis comparison")
|
||||
print_i("Ignoring ellipsis comparison", indent, error=True)
|
||||
return True
|
||||
elif type(val1) != type(val2): # pylint: disable=unidiomatic-typecheck
|
||||
print("Different types ({}, {}) -> False".format(
|
||||
type(val1), type(val2)))
|
||||
print_i("Different types ({}, {}) -> False".format(
|
||||
type(val1), type(val2)), indent, error=True)
|
||||
return False
|
||||
|
||||
if isinstance(val2, dict):
|
||||
print("Comparing as dicts")
|
||||
equal = _partial_compare_dict(val1, val2)
|
||||
print_i("|======= Comparing as dicts", indent)
|
||||
equal = _partial_compare_dict(val1, val2, indent=indent)
|
||||
elif isinstance(val2, list):
|
||||
print("Comparing as lists")
|
||||
equal = _partial_compare_list(val1, val2)
|
||||
print_i("|======= Comparing as lists", indent)
|
||||
equal = _partial_compare_list(val1, val2, indent=indent)
|
||||
elif isinstance(val2, float):
|
||||
print("Doing float comparison")
|
||||
print_i("|======= Doing float comparison", indent)
|
||||
equal = abs(val1 - val2) < 0.00001
|
||||
elif isinstance(val2, str):
|
||||
print("Doing string comparison")
|
||||
equal = fnmatch.fnmatchcase(val1, val2)
|
||||
print_i("|======= Doing string comparison", indent)
|
||||
equal = pattern_match(pattern=val2, value=val1)
|
||||
else:
|
||||
print("Comparing via ==")
|
||||
print_i("|======= Comparing via ==", indent)
|
||||
equal = val1 == val2
|
||||
print("---> {}".format(equal))
|
||||
print_i("---> {}".format(equal), indent)
|
||||
return equal
|
||||
|
||||
|
||||
def pattern_match(*, pattern, value):
|
||||
"""Do fnmatch.fnmatchcase like matching, but only with * active.
|
||||
|
||||
Return:
|
||||
True on a match, False otherwise.
|
||||
"""
|
||||
re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*'))
|
||||
return re.fullmatch(re_pattern, value) is not None
|
||||
|
1
tests/integration/data/backforward/3.txt
Normal file
1
tests/integration/data/backforward/3.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello World!
|
1
tests/integration/data/hello2.txt
Normal file
1
tests/integration/data/hello2.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello World 2!
|
1
tests/integration/data/hello3.txt
Normal file
1
tests/integration/data/hello3.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello World 3!
|
@ -207,5 +207,8 @@
|
||||
198
|
||||
199
|
||||
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet.
|
||||
</pre>
|
||||
<a href="/data/hello2.txt">next</a> link to test the --top-navigate argument for :scroll-page.
|
||||
<a href="/data/hello3.txt">prev</a> link to test the --bottom-navigate argument for :scroll-page.
|
||||
</body>
|
||||
</html>
|
||||
|
@ -43,6 +43,19 @@ Feature: Going back and forward.
|
||||
url: http://localhost:*/data/backforward/1.txt
|
||||
- url: http://localhost:*/data/backforward/2.txt
|
||||
|
||||
Scenario: Going back in a new tab without history
|
||||
Given I open data/backforward/1.txt
|
||||
When I run :tab-only
|
||||
And I run :back -t
|
||||
Then the error "At beginning of history." should be shown.
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- active: true
|
||||
history:
|
||||
- active: true
|
||||
url: http://localhost:*/data/backforward/1.txt
|
||||
|
||||
Scenario: Going back in a new background tab
|
||||
Given I open data/backforward/1.txt
|
||||
When I open data/backforward/2.txt
|
||||
@ -62,6 +75,31 @@ Feature: Going back and forward.
|
||||
url: http://localhost:*/data/backforward/1.txt
|
||||
- url: http://localhost:*/data/backforward/2.txt
|
||||
|
||||
Scenario: Going back with count.
|
||||
Given I open data/backforward/1.txt
|
||||
When I open data/backforward/2.txt
|
||||
And I open data/backforward/3.txt
|
||||
And I run :tab-only
|
||||
And I run :back with count 2
|
||||
And I wait until data/backforward/1.txt is loaded
|
||||
And I reload
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- active: true
|
||||
url: http://localhost:*/data/backforward/1.txt
|
||||
- url: http://localhost:*/data/backforward/2.txt
|
||||
- url: http://localhost:*/data/backforward/3.txt
|
||||
|
||||
Scenario: Going back with very big count.
|
||||
Given I open data/backforward/1.txt
|
||||
When I run :back with count 99999999999
|
||||
# Make sure it doesn't hang
|
||||
And I run :message-info "Still alive!"
|
||||
Then the error "At beginning of history." should be shown.
|
||||
And the message "Still alive!" should be shown.
|
||||
|
||||
Scenario: Going back in a new window
|
||||
Given I have a fresh instance
|
||||
When I open data/backforward/1.txt
|
||||
|
@ -62,8 +62,13 @@ def fresh_instance(quteproc):
|
||||
|
||||
@bdd.when(bdd.parsers.parse("I run {command}"))
|
||||
def run_command_when(quteproc, httpbin, command):
|
||||
if 'with count' in command:
|
||||
command, count = command.split(' with count ')
|
||||
count = int(count)
|
||||
else:
|
||||
count = None
|
||||
command = command.replace('(port)', str(httpbin.port))
|
||||
quteproc.send_cmd(command)
|
||||
quteproc.send_cmd(command, count=count)
|
||||
|
||||
|
||||
@bdd.when(bdd.parsers.parse("I reload"))
|
||||
@ -93,13 +98,12 @@ def wait_for_message(quteproc, httpbin, category, message):
|
||||
|
||||
@bdd.then(bdd.parsers.parse("{path} should be loaded"))
|
||||
def path_should_be_loaded(httpbin, path):
|
||||
requests = httpbin.get_requests()
|
||||
assert requests[-1] == httpbin.Request('GET', '/' + path)
|
||||
httpbin.wait_for(verb='GET', path='/' + path)
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
|
||||
def list_of_loaded_pages(httpbin, pages):
|
||||
expected_requests = [httpbin.Request('GET', '/' + path.strip())
|
||||
expected_requests = [httpbin.ExpectedRequest('GET', '/' + path.strip())
|
||||
for path in pages.split('\n')]
|
||||
actual_requests = httpbin.get_requests()
|
||||
assert actual_requests == expected_requests
|
||||
@ -131,6 +135,11 @@ def compare_session(quteproc, expected):
|
||||
assert utils.partial_compare(data, expected)
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse('"{pattern}" should not be logged'))
|
||||
def ensure_not_logged(quteproc, pattern):
|
||||
quteproc.ensure_not_logged(message=pattern)
|
||||
|
||||
|
||||
@bdd.then("no crash should happen")
|
||||
def no_crash():
|
||||
"""Don't do anything.
|
||||
|
@ -1,5 +1,7 @@
|
||||
Feature: Various utility commands.
|
||||
|
||||
## :set-cmd-text
|
||||
|
||||
Scenario: :set-cmd-text and :command-accept
|
||||
When I run :set-cmd-text :message-info "Hello World"
|
||||
And I run :command-accept
|
||||
@ -31,6 +33,8 @@ Feature: Various utility commands.
|
||||
When I run :set-cmd-text foo
|
||||
Then the error "Invalid command text 'foo'." should be shown.
|
||||
|
||||
## :message-*
|
||||
|
||||
Scenario: :message-error
|
||||
When I run :message-error "Hello World"
|
||||
Then the error "Hello World" should be shown.
|
||||
@ -42,3 +46,57 @@ Feature: Various utility commands.
|
||||
Scenario: :message-warning
|
||||
When I run :message-warning "Hello World"
|
||||
Then the warning "Hello World" should be shown.
|
||||
|
||||
## :jseval
|
||||
|
||||
Scenario: :jseval
|
||||
When I set general -> log-javascript-console to true
|
||||
And I run :jseval console.log("Hello from JS!");
|
||||
And I wait for "[:0] Hello from JS!" in the log
|
||||
Then the message "No output or error" should be shown.
|
||||
|
||||
Scenario: :jseval without logging
|
||||
When I set general -> log-javascript-console to false
|
||||
And I run :jseval console.log("Hello from JS!");
|
||||
Then the message "No output or error" should be shown.
|
||||
And "[:0] Hello from JS!" should not be logged
|
||||
|
||||
Scenario: :jseval with --quiet
|
||||
When I set general -> log-javascript-console to true
|
||||
And I run :jseval --quiet console.log("Hello from JS!");
|
||||
And I wait for "[:0] Hello from JS!" in the log
|
||||
Then "No output or error" should not be logged
|
||||
|
||||
Scenario: :jseval with a value
|
||||
When I run :jseval "foo"
|
||||
Then the message "foo" should be shown.
|
||||
|
||||
Scenario: :jseval with a long, truncated value
|
||||
When I run :jseval Array(5002).join("x")
|
||||
Then the message "x* [...trimmed...]" should be shown.
|
||||
|
||||
# :debug-webaction
|
||||
|
||||
Scenario: :debug-webaction with valid value
|
||||
Given I open data/backforward/1.txt
|
||||
When I open data/backforward/2.txt
|
||||
And I run :tab-only
|
||||
And I run :debug-webaction Back
|
||||
And I wait until data/backforward/1.txt is loaded
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- active: true
|
||||
url: http://localhost:*/data/backforward/1.txt
|
||||
- url: http://localhost:*/data/backforward/2.txt
|
||||
|
||||
Scenario: :debug-webaction with invalid value
|
||||
When I open data/hello.txt
|
||||
And I run :debug-webaction blah
|
||||
Then the error "blah is not a valid web action!" should be shown.
|
||||
|
||||
Scenario: :debug-webaction with non-webaction member
|
||||
When I open data/hello.txt
|
||||
And I run :debug-webaction PermissionUnknown
|
||||
Then the error "PermissionUnknown is not a valid web action!" should be shown.
|
||||
|
@ -5,6 +5,8 @@ Feature: Scrolling
|
||||
Given I open data/scroll.html
|
||||
And I run :tab-only
|
||||
|
||||
## :scroll-px
|
||||
|
||||
Scenario: Scrolling pixel-wise vertically
|
||||
When I run :scroll-px 0 10
|
||||
Then the page should be scrolled vertically.
|
||||
@ -13,6 +15,45 @@ Feature: Scrolling
|
||||
When I run :scroll-px 10 0
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: Scrolling down and up
|
||||
When I run :scroll-px 10 0
|
||||
And I run :scroll-px -10 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling right and left
|
||||
When I run :scroll-px 0 10
|
||||
And I run :scroll-px 0 -10
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling down and up with count
|
||||
When I run :scroll-px 0 10 with count 2
|
||||
When I run :scroll-px 0 -10
|
||||
When I run :scroll-px 0 -10
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling left and right with count
|
||||
When I run :scroll-px 10 0 with count 2
|
||||
When I run :scroll-px -10 0
|
||||
When I run :scroll-px -10 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: :scroll-px with a very big value
|
||||
When I run :scroll-px 99999999999 0
|
||||
Then the error "Numeric argument is too large for internal int representation." should be shown.
|
||||
|
||||
Scenario: :scroll-px on a page without scrolling
|
||||
When I open data/hello.txt
|
||||
And I run :scroll-px 10 10
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: :scroll-px with floats
|
||||
# This used to be allowed, but doesn't make much sense.
|
||||
When I run :scroll-px 2.5 2.5
|
||||
Then the error "scroll-px: Argument dx: invalid int value: '2.5'" should be shown.
|
||||
And the page should not be scrolled.
|
||||
|
||||
## :scroll
|
||||
|
||||
Scenario: Scrolling down
|
||||
When I run :scroll down
|
||||
Then the page should be scrolled vertically.
|
||||
@ -58,3 +99,155 @@ Feature: Scrolling
|
||||
When I run :scroll 0 10
|
||||
Then the warning ":scroll with dx/dy arguments is deprecated - use :scroll-px instead!" should be shown.
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: :scroll with deprecated pixel argument (float)
|
||||
When I run :scroll 2.5 2.5
|
||||
Then the error "scroll: Argument dy: invalid int value: '2.5'" should be shown.
|
||||
And the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling down and up with count
|
||||
When I run :scroll down with count 2
|
||||
And I run :scroll up
|
||||
And I run :scroll up
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling right
|
||||
When I run :scroll right
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: Scrolling right and left
|
||||
When I run :scroll right
|
||||
And I run :scroll left
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling right and left with count
|
||||
When I run :scroll right with count 2
|
||||
And I run :scroll left
|
||||
And I run :scroll left
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling down with a very big count
|
||||
When I run :scroll down with count 99999999999
|
||||
# Make sure it doesn't hang
|
||||
And I run :message-info "Still alive!"
|
||||
Then the message "Still alive!" should be shown.
|
||||
|
||||
Scenario: :scroll on a page without scrolling
|
||||
When I open data/hello.txt
|
||||
And I run :scroll down
|
||||
Then no crash should happen
|
||||
|
||||
## :scroll-perc
|
||||
|
||||
Scenario: Scrolling to bottom with :scroll-perc
|
||||
When I run :scroll-perc 100
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: Scrolling to bottom and to top with :scroll-perc
|
||||
When I run :scroll-perc 100
|
||||
And I run :scroll-perc 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling to middle with :scroll-perc
|
||||
When I run :scroll-perc 50
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: Scrolling to middle with :scroll-perc (float)
|
||||
When I run :scroll-perc 50.5
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: Scrolling to middle and to top with :scroll-perc
|
||||
When I run :scroll-perc 50
|
||||
And I run :scroll-perc 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling to right with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 100
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: Scrolling to right and to left with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 100
|
||||
And I run :scroll-perc --horizontal 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling to middle (horizontally) with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 50
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: Scrolling to middle and to left with :scroll-perc
|
||||
When I run :scroll-perc --horizontal 50
|
||||
And I run :scroll-perc --horizontal 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: :scroll-perc without argument
|
||||
When I run :scroll-perc
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: :scroll-perc without argument and --horizontal
|
||||
When I run :scroll-perc --horizontal
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: :scroll-perc with count
|
||||
When I run :scroll-perc with count 50
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: :scroll-perc with a very big value
|
||||
When I run :scroll-perc 99999999999
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: :scroll-perc on a page without scrolling
|
||||
When I open data/hello.txt
|
||||
And I run :scroll-perc 20
|
||||
Then no crash should happen
|
||||
|
||||
## :scroll-page
|
||||
|
||||
Scenario: Scrolling down with :scroll-page
|
||||
When I run :scroll-page 0 1
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: Scrolling down with :scroll-page (float)
|
||||
When I run :scroll-page 0 1.5
|
||||
Then the page should be scrolled vertically.
|
||||
|
||||
Scenario: Scrolling down and up with :scroll-page
|
||||
When I run :scroll-page 0 1
|
||||
And I run :scroll-page 0 -1
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling right with :scroll-page
|
||||
When I run :scroll-page 1 0
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: Scrolling right with :scroll-page (float)
|
||||
When I run :scroll-page 1.5 0
|
||||
Then the page should be scrolled horizontally.
|
||||
|
||||
Scenario: Scrolling right and left with :scroll-page
|
||||
When I run :scroll-page 1 0
|
||||
And I run :scroll-page -1 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: Scrolling right and left with :scroll-page and count
|
||||
When I run :scroll-page 1 0 with count 2
|
||||
And I run :scroll-page -1 0
|
||||
And I run :scroll-page -1 0
|
||||
Then the page should not be scrolled.
|
||||
|
||||
Scenario: :scroll-page with --bottom-navigate
|
||||
When I run :scroll-perc 100
|
||||
And I run :scroll-page --bottom-navigate next 0 1
|
||||
Then data/hello2.txt should be loaded
|
||||
|
||||
Scenario: :scroll-page with --top-navigate
|
||||
When I run :scroll-page --top-navigate prev 0 -1
|
||||
Then data/hello3.txt should be loaded
|
||||
|
||||
Scenario: :scroll-page with a very big value
|
||||
When I run :scroll-page 99999999999 99999999999
|
||||
Then the error "Numeric argument is too large for internal int representation." should be shown.
|
||||
|
||||
Scenario: :scroll-page on a page without scrolling
|
||||
When I open data/hello.txt
|
||||
And I run :scroll-page 1 1
|
||||
Then no crash should happen
|
||||
|
@ -27,22 +27,46 @@ import pytest_bdd as bdd
|
||||
bdd.scenarios('yankpaste.feature')
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def skip_with_broken_clipboard(qapp):
|
||||
"""The clipboard seems to be broken on some platforms (OS X Yosemite?).
|
||||
|
||||
This skips the tests if this is the case.
|
||||
"""
|
||||
clipboard = qapp.clipboard()
|
||||
clipboard.setText("Does this work?")
|
||||
if clipboard.text() != "Does this work?":
|
||||
pytest.skip("Clipboard seems to be broken on this platform.")
|
||||
|
||||
|
||||
def _get_mode(qapp, what):
|
||||
"""Get the QClipboard::Mode to use based on a string."""
|
||||
if what == 'clipboard':
|
||||
return QClipboard.Clipboard
|
||||
elif what == 'primary selection':
|
||||
assert qapp.clipboard().supportsSelection()
|
||||
return QClipboard.Selection
|
||||
else:
|
||||
raise AssertionError
|
||||
|
||||
|
||||
@bdd.when("selection is supported")
|
||||
def selection_supported(qapp):
|
||||
if not qapp.clipboard().supportsSelection():
|
||||
pytest.skip("OS doesn't support primary selection!")
|
||||
|
||||
@bdd.when(bdd.parsers.re(r'I put "(?P<content>.*)" into the '
|
||||
r'(?P<what>primary selection|clipboard)'))
|
||||
def fill_clipboard(qapp, httpbin, what, content):
|
||||
mode = _get_mode(qapp, what)
|
||||
content = content.replace('(port)', str(httpbin.port))
|
||||
qapp.clipboard().setText(content, mode)
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
|
||||
r'contain "(?P<content>.*)"'))
|
||||
def clipboard_contains(qapp, httpbin, what, content):
|
||||
if what == 'clipboard':
|
||||
mode = QClipboard.Clipboard
|
||||
elif what == 'primary selection':
|
||||
mode = QClipboard.Selection
|
||||
else:
|
||||
raise AssertionError
|
||||
|
||||
mode = _get_mode(qapp, what)
|
||||
expected = content.replace('(port)', str(httpbin.port))
|
||||
|
||||
data = qapp.clipboard().text(mode=mode)
|
||||
|
@ -19,3 +19,10 @@
|
||||
|
||||
import pytest_bdd as bdd
|
||||
bdd.scenarios('zoom.feature')
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse("the zoom should be {zoom}%"))
|
||||
def check_zoom(quteproc, zoom):
|
||||
data = quteproc.get_session()
|
||||
value = data['windows'][0]['tabs'][0]['history'][0]['zoom'] * 100
|
||||
assert abs(value - float(zoom)) < 0.0001
|
||||
|
@ -3,26 +3,112 @@ Feature: Yanking and pasting.
|
||||
clipboard and primary selection.
|
||||
|
||||
Background:
|
||||
Given I open data/yankpaste.html
|
||||
Given I run :tab-only
|
||||
|
||||
#### :yank
|
||||
|
||||
Scenario: Yanking URLs to clipboard
|
||||
When I run :yank
|
||||
When I open data/yankpaste.html
|
||||
And I run :yank
|
||||
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/yankpaste.html" should be shown.
|
||||
And the clipboard should contain "http://localhost:(port)/data/yankpaste.html"
|
||||
|
||||
Scenario: Yanking URLs to primary selection
|
||||
When selection is supported
|
||||
And I open data/yankpaste.html
|
||||
And I run :yank --sel
|
||||
Then the message "Yanked URL to primary selection: http://localhost:(port)/data/yankpaste.html" should be shown.
|
||||
And the primary selection should contain "http://localhost:(port)/data/yankpaste.html"
|
||||
|
||||
Scenario: Yanking title to clipboard
|
||||
When I wait for regex "Changing title for idx \d to 'Test title'" in the log
|
||||
When I open data/yankpaste.html
|
||||
And I wait for regex "Changing title for idx \d to 'Test title'" in the log
|
||||
And I run :yank --title
|
||||
Then the message "Yanked title to clipboard: Test title" should be shown.
|
||||
And the clipboard should contain "Test title"
|
||||
|
||||
Scenario: Yanking domain to clipboard
|
||||
When I run :yank --domain
|
||||
When I open data/yankpaste.html
|
||||
And I run :yank --domain
|
||||
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown.
|
||||
And the clipboard should contain "http://localhost:(port)"
|
||||
|
||||
#### :paste
|
||||
|
||||
Scenario: Pasting an URL
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the requests should be:
|
||||
data/hello.txt
|
||||
|
||||
Scenario: Pasting an URL from primary selection
|
||||
When selection is supported
|
||||
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection
|
||||
And I run :paste --sel
|
||||
And I wait until data/hello2.txt is loaded
|
||||
Then the requests should be:
|
||||
data/hello2.txt
|
||||
|
||||
Scenario: Pasting with empty clipboard
|
||||
When I put "" into the clipboard
|
||||
And I run :paste
|
||||
Then the error "Clipboard is empty." should be shown.
|
||||
|
||||
Scenario: Pasting with empty selection
|
||||
When selection is supported
|
||||
And I put "" into the primary selection
|
||||
And I run :paste --sel
|
||||
Then the error "Primary selection is empty." should be shown.
|
||||
|
||||
Scenario: Pasting in a new tab
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste -t
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- active: true
|
||||
url: about:blank
|
||||
- active: true
|
||||
history:
|
||||
- active: true
|
||||
url: http://localhost:*/data/hello.txt
|
||||
|
||||
Scenario: Pasting in a background tab
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste -b
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- active: true
|
||||
history:
|
||||
- active: true
|
||||
url: about:blank
|
||||
- history:
|
||||
- active: true
|
||||
url: http://localhost:*/data/hello.txt
|
||||
|
||||
Scenario: Pasting in a new window
|
||||
Given I have a fresh instance
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste -w
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- active: true
|
||||
history:
|
||||
- active: true
|
||||
url: about:blank
|
||||
- tabs:
|
||||
- active: true
|
||||
history:
|
||||
- active: true
|
||||
url: http://localhost:*/data/hello.txt
|
||||
|
@ -2,41 +2,56 @@ Feature: Zooming in and out
|
||||
|
||||
Background:
|
||||
Given I open data/hello.txt
|
||||
And I set ui -> zoom-levels to 50%,90%,100%,110%,120%
|
||||
And I run :tab-only
|
||||
|
||||
Scenario: Zooming in
|
||||
When I run :zoom-in
|
||||
Then the message "Zoom level: 110%" should be shown.
|
||||
And the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- zoom: 1.1
|
||||
And the zoom should be 110%
|
||||
|
||||
Scenario: Zooming out
|
||||
When I run :zoom-out
|
||||
Then the message "Zoom level: 90%" should be shown.
|
||||
And the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- zoom: 0.9
|
||||
And the zoom should be 90%
|
||||
|
||||
Scenario: Zooming in with count
|
||||
When I run :zoom-in with count 2
|
||||
Then the message "Zoom level: 120%" should be shown.
|
||||
And the zoom should be 120%
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1118
|
||||
Scenario: Zooming in with very big count
|
||||
When I run :zoom-in with count 99999999999
|
||||
Then the message "Zoom level: 100%" should be shown.
|
||||
And the zoom should be 100%
|
||||
|
||||
Scenario: Zooming out with count
|
||||
When I run :zoom-out with count 2
|
||||
Then the message "Zoom level: 50%" should be shown.
|
||||
And the zoom should be 50%
|
||||
|
||||
Scenario: Setting zoom
|
||||
When I run :zoom 50
|
||||
Then the message "Zoom level: 50%" should be shown.
|
||||
And the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- zoom: 0.5
|
||||
And the zoom should be 50%
|
||||
|
||||
Scenario: Setting zoom with count
|
||||
When I run :zoom with count 40
|
||||
Then the message "Zoom level: 40%" should be shown.
|
||||
And the zoom should be 40%
|
||||
|
||||
Scenario: Resetting zoom
|
||||
When I run :zoom 50
|
||||
When I set ui -> default-zoom to 42%
|
||||
And I run :zoom 50
|
||||
And I run :zoom
|
||||
Then the message "Zoom level: 100%" should be shown.
|
||||
And the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
- zoom: 1.0
|
||||
Then the message "Zoom level: 42%" should be shown.
|
||||
And the zoom should be 42%
|
||||
|
||||
Scenario: Setting zoom to invalid value
|
||||
When I run :zoom -1
|
||||
Then the error "Can't zoom -1%!" should be shown.
|
||||
|
||||
Scenario: Setting zoom with very big count
|
||||
When I run :zoom with count 99999999999
|
||||
Then the message "Zoom level: 99999999999%" should be shown.
|
||||
|
@ -47,13 +47,6 @@ def is_ignored_qt_message(message):
|
||||
return False
|
||||
|
||||
|
||||
class NoLineMatch(Exception):
|
||||
|
||||
"""Raised by LogLine on unmatched lines."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LogLine(testprocess.Line):
|
||||
|
||||
"""A parsed line from the qutebrowser log output.
|
||||
@ -78,7 +71,7 @@ class LogLine(testprocess.Line):
|
||||
super().__init__(data)
|
||||
match = self.LOG_RE.match(data)
|
||||
if match is None:
|
||||
raise NoLineMatch(data)
|
||||
raise testprocess.InvalidLine(data)
|
||||
|
||||
self.timestamp = datetime.datetime.strptime(match.group('timestamp'),
|
||||
'%H:%M:%S')
|
||||
@ -127,6 +120,9 @@ class QuteProc(testprocess.Process):
|
||||
|
||||
got_error = pyqtSignal()
|
||||
|
||||
KEYS = ['timestamp', 'loglevel', 'category', 'module', 'function', 'line',
|
||||
'message']
|
||||
|
||||
def __init__(self, httpbin, parent=None):
|
||||
super().__init__(parent)
|
||||
self._httpbin = httpbin
|
||||
@ -135,7 +131,7 @@ class QuteProc(testprocess.Process):
|
||||
def _parse_line(self, line):
|
||||
try:
|
||||
log_line = LogLine(line)
|
||||
except NoLineMatch:
|
||||
except testprocess.InvalidLine:
|
||||
if line.startswith(' '):
|
||||
# Multiple lines in some log output...
|
||||
return None
|
||||
@ -144,7 +140,7 @@ class QuteProc(testprocess.Process):
|
||||
elif is_ignored_qt_message(line):
|
||||
return None
|
||||
else:
|
||||
raise testprocess.InvalidLine
|
||||
raise
|
||||
|
||||
if (log_line.loglevel in ['INFO', 'WARNING', 'ERROR'] or
|
||||
pytest.config.getoption('--verbose')):
|
||||
@ -177,6 +173,12 @@ class QuteProc(testprocess.Process):
|
||||
'about:blank']
|
||||
return executable, args
|
||||
|
||||
def _path_to_url(self, path):
|
||||
if path.startswith('about:') or path.startswith('qute:'):
|
||||
return path
|
||||
else:
|
||||
return 'http://localhost:{}/{}'.format(self._httpbin.port, path)
|
||||
|
||||
def after_test(self):
|
||||
bad_msgs = [msg for msg in self._data
|
||||
if msg.loglevel > logging.INFO and not msg.expected]
|
||||
@ -186,9 +188,12 @@ class QuteProc(testprocess.Process):
|
||||
str(e) for e in bad_msgs)
|
||||
pytest.fail(text, pytrace=False)
|
||||
|
||||
def send_cmd(self, command):
|
||||
def send_cmd(self, command, count=None):
|
||||
assert self._ipc_socket is not None
|
||||
|
||||
if count is not None:
|
||||
command = ':{}:{}'.format(count, command.lstrip(':'))
|
||||
|
||||
ipc.send_to_running_instance(self._ipc_socket, [command],
|
||||
target_arg='')
|
||||
self.wait_for(category='commands', module='command', function='run',
|
||||
@ -199,7 +204,7 @@ class QuteProc(testprocess.Process):
|
||||
self.wait_for(category='config', message='Config option changed: *')
|
||||
|
||||
def open_path(self, path, new_tab=False):
|
||||
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path)
|
||||
url = self._path_to_url(path)
|
||||
if new_tab:
|
||||
self.send_cmd(':open -t ' + url)
|
||||
else:
|
||||
@ -218,11 +223,12 @@ class QuteProc(testprocess.Process):
|
||||
self._data is cleared after every test to provide at least some
|
||||
isolation.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
return super().wait_for(timeout, **kwargs)
|
||||
|
||||
def wait_for_load_finished(self, path, timeout=15000):
|
||||
"""Wait until any tab has finished loading."""
|
||||
url = 'http://localhost:{}/{}'.format(self._httpbin.port, path)
|
||||
url = self._path_to_url(path)
|
||||
pattern = re.compile(
|
||||
r"(load status for <qutebrowser.browser.webview.WebView "
|
||||
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "
|
||||
|
@ -25,6 +25,7 @@ import datetime
|
||||
import pytest
|
||||
|
||||
import quteprocess
|
||||
import testprocess
|
||||
from qutebrowser.utils import log
|
||||
|
||||
|
||||
@ -113,5 +114,5 @@ def test_log_line_parse(data, attrs):
|
||||
|
||||
|
||||
def test_log_line_no_match():
|
||||
with pytest.raises(quteprocess.NoLineMatch):
|
||||
with pytest.raises(testprocess.InvalidLine):
|
||||
quteprocess.LogLine("Hello World!")
|
||||
|
@ -74,19 +74,20 @@ class PythonProcess(testprocess.Process):
|
||||
return (sys.executable, ['-c', ';'.join(code)])
|
||||
|
||||
|
||||
class TestWaitFor:
|
||||
@pytest.yield_fixture
|
||||
def pyproc():
|
||||
proc = PythonProcess()
|
||||
yield proc
|
||||
proc.terminate()
|
||||
|
||||
@pytest.yield_fixture
|
||||
def pyproc(self):
|
||||
proc = PythonProcess()
|
||||
yield proc
|
||||
proc.terminate()
|
||||
|
||||
class TestWaitFor:
|
||||
|
||||
def test_successful(self, pyproc):
|
||||
"""Using wait_for with the expected text."""
|
||||
pyproc.code = "time.sleep(0.5); print('foobar')"
|
||||
pyproc.start()
|
||||
with stopwatch(min_ms=300): # on Windows, this can be done faster...
|
||||
with stopwatch(min_ms=500):
|
||||
pyproc.start()
|
||||
pyproc.wait_for(data="foobar")
|
||||
|
||||
def test_other_text(self, pyproc):
|
||||
@ -103,12 +104,13 @@ class TestWaitFor:
|
||||
with pytest.raises(testprocess.WaitForTimeout):
|
||||
pyproc.wait_for(data="foobar", timeout=100)
|
||||
|
||||
def test_existing_message(self, pyproc):
|
||||
@pytest.mark.parametrize('message', ['foobar', 'literal [x]'])
|
||||
def test_existing_message(self, message, pyproc):
|
||||
"""Test with a message which already passed when waiting."""
|
||||
pyproc.code = "print('foobar')"
|
||||
pyproc.code = "print('{}')".format(message)
|
||||
pyproc.start()
|
||||
time.sleep(0.5) # to make sure the message is printed
|
||||
pyproc.wait_for(data="foobar")
|
||||
pyproc.wait_for(data=message)
|
||||
|
||||
def test_existing_message_previous_test(self, pyproc):
|
||||
"""Make sure the message of a previous test gets ignored."""
|
||||
@ -133,3 +135,36 @@ class TestWaitFor:
|
||||
pyproc.wait_for(data="foobar")
|
||||
with pytest.raises(testprocess.WaitForTimeout):
|
||||
pyproc.wait_for(data="foobar", timeout=100)
|
||||
|
||||
|
||||
class TestEnsureNotLogged:
|
||||
|
||||
@pytest.mark.parametrize('message, pattern', [
|
||||
('blacklisted', 'blacklisted'),
|
||||
('bl[a]cklisted', 'bl[a]cklisted'),
|
||||
('blacklisted', 'black*'),
|
||||
])
|
||||
def test_existing_message(self, pyproc, message, pattern):
|
||||
pyproc.code = "print('{}')".format(message)
|
||||
pyproc.start()
|
||||
with stopwatch(max_ms=1000):
|
||||
with pytest.raises(testprocess.BlacklistedMessageError):
|
||||
pyproc.ensure_not_logged(data=pattern, delay=2000)
|
||||
|
||||
def test_late_message(self, pyproc):
|
||||
pyproc.code = "time.sleep(0.5); print('blacklisted')"
|
||||
pyproc.start()
|
||||
with pytest.raises(testprocess.BlacklistedMessageError):
|
||||
pyproc.ensure_not_logged(data='blacklisted', delay=1000)
|
||||
|
||||
def test_no_matching_message(self, pyproc):
|
||||
pyproc.code = "print('blacklisted... nope!')"
|
||||
pyproc.start()
|
||||
pyproc.ensure_not_logged(data='blacklisted', delay=100)
|
||||
|
||||
def test_wait_for_and_blacklist(self, pyproc):
|
||||
pyproc.code = "print('blacklisted')"
|
||||
pyproc.start()
|
||||
pyproc.wait_for(data='blacklisted')
|
||||
with pytest.raises(testprocess.BlacklistedMessageError):
|
||||
pyproc.ensure_not_logged(data='blacklisted', delay=0)
|
||||
|
@ -46,5 +46,19 @@ def test_httpbin(httpbin, qtbot, path, content, expected):
|
||||
|
||||
data = response.read().decode('utf-8')
|
||||
|
||||
assert httpbin.get_requests() == [httpbin.Request('GET', path)]
|
||||
assert httpbin.get_requests() == [httpbin.ExpectedRequest('GET', path)]
|
||||
assert (content in data) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('line, verb, path, equal', [
|
||||
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
|
||||
'GET', '/', True),
|
||||
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
|
||||
'GET', '/foo', False),
|
||||
('127.0.0.1 - - [01/Jan/1990 00:00:00] "GET / HTTP/1.1" 200 -',
|
||||
'POST', '/foo', False),
|
||||
])
|
||||
def test_expected_request(httpbin, line, verb, path, equal):
|
||||
expected = httpbin.ExpectedRequest(verb, path)
|
||||
request = httpbin.Request(line)
|
||||
assert (expected == request) == equal
|
||||
|
@ -22,12 +22,13 @@
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
import fnmatch
|
||||
|
||||
import pytestqt.plugin # pylint: disable=import-error
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QProcess, QObject, QElapsedTimer
|
||||
from PyQt5.QtTest import QSignalSpy
|
||||
|
||||
from helpers import utils # pylint: disable=import-error
|
||||
|
||||
|
||||
class InvalidLine(Exception):
|
||||
|
||||
@ -48,6 +49,11 @@ class WaitForTimeout(Exception):
|
||||
"""Raised when wait_for didn't get the expected message."""
|
||||
|
||||
|
||||
class BlacklistedMessageError(Exception):
|
||||
|
||||
"""Raised when ensure_not_logged found a message."""
|
||||
|
||||
|
||||
class Line:
|
||||
|
||||
"""Container for a line of data the process emits.
|
||||
@ -78,6 +84,7 @@ class Process(QObject):
|
||||
|
||||
ready = pyqtSignal()
|
||||
new_data = pyqtSignal(object)
|
||||
KEYS = ['data']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@ -138,7 +145,8 @@ class Process(QObject):
|
||||
continue
|
||||
|
||||
if parsed is None:
|
||||
print("IGNORED: {}".format(line))
|
||||
if self._invalid:
|
||||
print("IGNORED: {}".format(line))
|
||||
else:
|
||||
self._data.append(parsed)
|
||||
self.new_data.emit(parsed)
|
||||
@ -198,7 +206,7 @@ class Process(QObject):
|
||||
|
||||
- If expected is None, the filter always matches.
|
||||
- If the value is a string or bytes object and the expected value is
|
||||
too, the pattern is treated as a fnmatch glob pattern.
|
||||
too, the pattern is treated as a glob pattern (with only * active).
|
||||
- If the value is a string or bytes object and the expected value is a
|
||||
compiled regex, it is used for matching.
|
||||
- If the value is any other type, == is used.
|
||||
@ -212,25 +220,34 @@ class Process(QObject):
|
||||
elif isinstance(expected, regex_type):
|
||||
return expected.match(value)
|
||||
elif isinstance(value, (bytes, str)):
|
||||
return fnmatch.fnmatchcase(value, expected)
|
||||
return utils.pattern_match(pattern=expected, value=value)
|
||||
else:
|
||||
return value == expected
|
||||
|
||||
def wait_for(self, timeout=None, **kwargs):
|
||||
def wait_for(self, timeout=None, *, override_waited_for=False, **kwargs):
|
||||
"""Wait until a given value is found in the data.
|
||||
|
||||
Keyword arguments to this function get interpreted as attributes of the
|
||||
searched data. Every given argument is treated as a pattern which
|
||||
the attribute has to match against.
|
||||
|
||||
Args:
|
||||
timeout: How long to wait for the message.
|
||||
override_waited_for: If set, gets triggered by previous messages
|
||||
again.
|
||||
|
||||
Return:
|
||||
The matched line.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if timeout is None:
|
||||
if 'CI' in os.environ:
|
||||
timeout = 15000
|
||||
else:
|
||||
timeout = 5000
|
||||
for key in kwargs:
|
||||
assert key in self.KEYS
|
||||
|
||||
# Search existing messages
|
||||
for line in self._data:
|
||||
matches = []
|
||||
@ -239,7 +256,7 @@ class Process(QObject):
|
||||
value = getattr(line, key)
|
||||
matches.append(self._match_data(value, expected))
|
||||
|
||||
if all(matches) and not line.waited_for:
|
||||
if all(matches) and (not line.waited_for or override_waited_for):
|
||||
# If we waited for this line, chances are we don't mean the
|
||||
# same thing the next time we use wait_for and it matches
|
||||
# this line again.
|
||||
@ -273,3 +290,18 @@ class Process(QObject):
|
||||
# this line again.
|
||||
line.waited_for = True
|
||||
return line
|
||||
|
||||
def ensure_not_logged(self, delay=500, **kwargs):
|
||||
"""Make sure the data matching the given arguments is not logged.
|
||||
|
||||
If nothing is found in the log, we wait for delay ms to make sure
|
||||
nothing arrives.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
try:
|
||||
line = self.wait_for(timeout=delay, override_waited_for=True,
|
||||
**kwargs)
|
||||
except WaitForTimeout:
|
||||
return
|
||||
else:
|
||||
raise BlacklistedMessageError(line)
|
||||
|
@ -26,7 +26,6 @@ import re
|
||||
import sys
|
||||
import socket
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
@ -34,25 +33,17 @@ from PyQt5.QtCore import pyqtSignal
|
||||
import testprocess # pylint: disable=import-error
|
||||
|
||||
|
||||
Request = collections.namedtuple('Request', 'verb, path')
|
||||
class Request(testprocess.Line):
|
||||
|
||||
"""A parsed line from the httpbin/flask log output.
|
||||
|
||||
class HTTPBin(testprocess.Process):
|
||||
|
||||
"""Abstraction over a running HTTPbin server process.
|
||||
|
||||
Reads the log from its stdout and parses it.
|
||||
Attributes:
|
||||
timestamp/verb/path/status: Parsed from the log output.
|
||||
|
||||
Class attributes:
|
||||
LOG_RE: Used to parse the CLF log which httpbin outputs.
|
||||
|
||||
Signals:
|
||||
new_request: Emitted when there's a new request received.
|
||||
"""
|
||||
|
||||
new_request = pyqtSignal(Request)
|
||||
Request = Request # So it can be used from the fixture easily.
|
||||
|
||||
LOG_RE = re.compile(r"""
|
||||
(?P<host>[^ ]*)
|
||||
\ ([^ ]*) # ignored
|
||||
@ -67,6 +58,69 @@ class HTTPBin(testprocess.Process):
|
||||
\ (?P<size>[^ ]*)
|
||||
""", re.VERBOSE)
|
||||
|
||||
def __init__(self, data):
|
||||
super().__init__(data)
|
||||
match = self.LOG_RE.match(data)
|
||||
if match is None:
|
||||
raise testprocess.InvalidLine(data)
|
||||
|
||||
assert match.group('host') == '127.0.0.1'
|
||||
assert match.group('user') == '-'
|
||||
self.timestamp = match.group('date')
|
||||
self.verb = match.group('verb')
|
||||
|
||||
# FIXME do we need to allow other options?
|
||||
assert match.group('protocol') == 'HTTP/1.1'
|
||||
assert self.verb == 'GET'
|
||||
|
||||
self.path = match.group('path')
|
||||
self.status = int(match.group('status'))
|
||||
|
||||
missing_paths = ['/favicon.ico', '/does-not-exist']
|
||||
|
||||
if self.path in missing_paths:
|
||||
assert self.status == 404
|
||||
else:
|
||||
assert self.status < 400
|
||||
|
||||
assert match.group('size') == '-'
|
||||
|
||||
def __eq__(self, other):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class ExpectedRequest:
|
||||
|
||||
"""Class to compare expected requests easily."""
|
||||
|
||||
def __init__(self, verb, path):
|
||||
self.verb = verb
|
||||
self.path = path
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, (Request, ExpectedRequest)):
|
||||
return (self.verb == other.verb and
|
||||
self.path == other.path)
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class HTTPBin(testprocess.Process):
|
||||
|
||||
"""Abstraction over a running HTTPbin server process.
|
||||
|
||||
Reads the log from its stdout and parses it.
|
||||
|
||||
Signals:
|
||||
new_request: Emitted when there's a new request received.
|
||||
"""
|
||||
|
||||
new_request = pyqtSignal(Request)
|
||||
Request = Request # So it can be used from the fixture easily.
|
||||
ExpectedRequest = ExpectedRequest
|
||||
|
||||
KEYS = ['verb', 'path']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.port = self._get_port()
|
||||
@ -91,14 +145,7 @@ class HTTPBin(testprocess.Process):
|
||||
'quit)'.format(self.port)):
|
||||
self.ready.emit()
|
||||
return None
|
||||
|
||||
match = self.LOG_RE.match(line)
|
||||
if match is None:
|
||||
raise testprocess.InvalidLine
|
||||
# FIXME do we need to allow other options?
|
||||
assert match.group('protocol') == 'HTTP/1.1'
|
||||
|
||||
return Request(verb=match.group('verb'), path=match.group('path'))
|
||||
return Request(line)
|
||||
|
||||
def _executable_args(self):
|
||||
if hasattr(sys, 'frozen'):
|
||||
|
@ -55,7 +55,7 @@ class HeaderChecker:
|
||||
"""Check if the passed header is ignored."""
|
||||
reply = self.stubs.FakeNetworkReply(
|
||||
headers={'Content-Disposition': header})
|
||||
with self.caplog.atLevel(logging.ERROR, 'rfc6266'):
|
||||
with self.caplog.at_level(logging.ERROR, 'rfc6266'):
|
||||
# with self.assertLogs(log.rfc6266, logging.ERROR):
|
||||
cd_inline, cd_filename = http.parse_content_disposition(reply)
|
||||
assert cd_filename == DEFAULT_NAME
|
||||
|
@ -41,7 +41,7 @@ def test_parse_content_disposition(caplog, template, stubs, s):
|
||||
"""Test parsing headers based on templates which hypothesis completes."""
|
||||
header = template.format(s)
|
||||
reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header})
|
||||
with caplog.atLevel(logging.ERROR, 'rfc6266'):
|
||||
with caplog.at_level(logging.ERROR, 'rfc6266'):
|
||||
http.parse_content_disposition(reply)
|
||||
|
||||
|
||||
|
@ -147,7 +147,7 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir):
|
||||
|
||||
disk_cache = cache.DiskCache(str(tmpdir))
|
||||
cache_file = disk_cache.fileMetaData("nosuchfile")
|
||||
assert cache_file.isValid() == False
|
||||
assert not cache_file.isValid()
|
||||
|
||||
|
||||
def test_cache_deactivated_metadata_file(config_stub, tmpdir):
|
||||
@ -207,7 +207,7 @@ def test_cache_deactivated_remove_data(config_stub, tmpdir):
|
||||
disk_cache = cache.DiskCache(str(tmpdir))
|
||||
|
||||
url = QUrl('http://www.example.com/')
|
||||
assert disk_cache.remove(url) == False
|
||||
assert not disk_cache.remove(url)
|
||||
|
||||
|
||||
def test_cache_insert_data(config_stub, tmpdir):
|
||||
|
@ -106,13 +106,12 @@ def test_logging(caplog, objects, tabbed_browser, index_of, verb):
|
||||
tabbed_browser.current_index = 0
|
||||
tabbed_browser.index_of = index_of
|
||||
|
||||
with caplog.atLevel(logging.DEBUG, logger='signals'):
|
||||
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||
objects.signaller.signal.emit('foo')
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert len(caplog.records) == 1
|
||||
expected_msg = "{}: filtered_signal('foo') (tab {})".format(verb, index_of)
|
||||
assert records[0].msg == expected_msg
|
||||
assert caplog.records[0].msg == expected_msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize('index_of', [0, 1])
|
||||
@ -120,10 +119,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
|
||||
tabbed_browser.current_index = 0
|
||||
tabbed_browser.index_of = index_of
|
||||
|
||||
with caplog.atLevel(logging.DEBUG, logger='signals'):
|
||||
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||
objects.signaller.statusbar_message.emit('foo')
|
||||
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
def test_runtime_error(objects, tabbed_browser):
|
||||
|
@ -42,3 +42,12 @@ class TestCommandRunner:
|
||||
else:
|
||||
with pytest.raises(cmdexc.NoSuchCommandError):
|
||||
list(cr.parse_all(cmdline_test.cmd, aliases=False))
|
||||
|
||||
def test_parse_with_count(self):
|
||||
"""Test parsing of commands with a count."""
|
||||
cr = runners.CommandRunner(0)
|
||||
result = cr.parse('20:scroll down', aliases=False)
|
||||
assert result.cmd.name == 'scroll'
|
||||
assert result.count == 20
|
||||
assert result.args == ['down']
|
||||
assert result.cmdline == ['scroll', 'down']
|
||||
|
@ -64,12 +64,11 @@ def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
|
||||
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
|
||||
obj = Obj("{{ font['foo'] }}")
|
||||
|
||||
with caplog.atLevel(9): # VDEBUG
|
||||
with caplog.at_level(9): # VDEBUG
|
||||
style.set_register_stylesheet(obj)
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert records[0].message == 'stylesheet for Obj: bar'
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == 'stylesheet for Obj: bar'
|
||||
|
||||
assert obj.rendered_stylesheet == 'bar'
|
||||
|
||||
@ -104,11 +103,10 @@ class TestColorDict:
|
||||
|
||||
def test_key_error(self, caplog):
|
||||
d = style.ColorDict()
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
d['foo'] # pylint: disable=pointless-statement
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert records[0].message == 'No color defined for foo!'
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == 'No color defined for foo!'
|
||||
|
||||
def test_qcolor(self):
|
||||
d = style.ColorDict()
|
||||
|
@ -74,14 +74,14 @@ class TestDebugLog:
|
||||
|
||||
def test_log(self, keyparser, caplog):
|
||||
keyparser._debug_log('foo')
|
||||
assert len(caplog.records()) == 1
|
||||
record = caplog.records()[0]
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.message == 'foo'
|
||||
|
||||
def test_no_log(self, keyparser, caplog):
|
||||
keyparser.do_log = False
|
||||
keyparser._debug_log('foo')
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
@pytest.mark.parametrize('input_key, supports_count, expected', [
|
||||
@ -161,10 +161,10 @@ class TestReadConfig:
|
||||
0, supports_count=False, supports_chains=False)
|
||||
kp._warn_on_keychains = warn_on_keychains
|
||||
|
||||
with caplog.atLevel(logging.WARNING):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
kp.read_config('normal')
|
||||
|
||||
assert bool(caplog.records()) == warn_on_keychains
|
||||
assert bool(caplog.records) == warn_on_keychains
|
||||
|
||||
|
||||
class TestSpecialKeys:
|
||||
|
@ -174,16 +174,16 @@ def test_start_logging(fake_proc, caplog):
|
||||
"""Make sure that starting logs the executed commandline."""
|
||||
cmd = 'does_not_exist'
|
||||
args = ['arg', 'arg with spaces']
|
||||
with caplog.atLevel(logging.DEBUG):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
fake_proc.start(cmd, args)
|
||||
msgs = [e.msg for e in caplog.records()]
|
||||
msgs = [e.msg for e in caplog.records]
|
||||
assert msgs == ["Starting process.",
|
||||
"Executing: does_not_exist arg 'arg with spaces'"]
|
||||
|
||||
|
||||
def test_error(qtbot, proc, caplog, guiprocess_message_mock):
|
||||
"""Test the process emitting an error."""
|
||||
with caplog.atLevel(logging.ERROR, 'message'):
|
||||
with caplog.at_level(logging.ERROR, 'message'):
|
||||
with qtbot.waitSignal(proc.error, raising=True, timeout=5000):
|
||||
proc.start('this_does_not_exist_either', [])
|
||||
|
||||
|
@ -358,11 +358,10 @@ class TestListen:
|
||||
|
||||
@pytest.mark.posix
|
||||
def test_atime_update_no_name(self, qtbot, caplog, ipc_server):
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
ipc_server.update_atime()
|
||||
|
||||
records = caplog.records()
|
||||
assert records[-1].msg == "In update_atime with no server path!"
|
||||
assert caplog.records[-1].msg == "In update_atime with no server path!"
|
||||
|
||||
@pytest.mark.posix
|
||||
def test_atime_shutdown_typeerror(self, qtbot, ipc_server):
|
||||
@ -408,22 +407,21 @@ class TestHandleConnection:
|
||||
|
||||
def test_no_connection(self, ipc_server, caplog):
|
||||
ipc_server.handle_connection()
|
||||
record = caplog.records()[-1]
|
||||
assert record.message == "No new connection to handle."
|
||||
assert caplog.records[-1].message == "No new connection to handle."
|
||||
|
||||
def test_double_connection(self, qlocalsocket, ipc_server, caplog):
|
||||
ipc_server._socket = qlocalsocket
|
||||
ipc_server.handle_connection()
|
||||
message = ("Got new connection but ignoring it because we're still "
|
||||
"handling another one.")
|
||||
assert message in [rec.message for rec in caplog.records()]
|
||||
assert message in [rec.message for rec in caplog.records]
|
||||
|
||||
def test_disconnected_immediately(self, ipc_server, caplog):
|
||||
socket = FakeSocket(state=QLocalSocket.UnconnectedState)
|
||||
ipc_server._server = FakeServer(socket)
|
||||
ipc_server.handle_connection()
|
||||
msg = "Socket was disconnected immediately."
|
||||
all_msgs = [r.message for r in caplog.records()]
|
||||
all_msgs = [r.message for r in caplog.records]
|
||||
assert msg in all_msgs
|
||||
|
||||
def test_error_immediately(self, ipc_server, caplog):
|
||||
@ -436,7 +434,7 @@ class TestHandleConnection:
|
||||
exc_msg = 'Error while handling IPC connection: Error string (error 7)'
|
||||
assert str(excinfo.value) == exc_msg
|
||||
msg = "We got an error immediately."
|
||||
all_msgs = [r.message for r in caplog.records()]
|
||||
all_msgs = [r.message for r in caplog.records]
|
||||
assert msg in all_msgs
|
||||
|
||||
def test_read_line_immediately(self, qtbot, ipc_server, caplog):
|
||||
@ -454,7 +452,7 @@ class TestHandleConnection:
|
||||
assert spy[0][0] == ['foo']
|
||||
assert spy[0][1] == 'tab'
|
||||
|
||||
all_msgs = [r.message for r in caplog.records()]
|
||||
all_msgs = [r.message for r in caplog.records]
|
||||
assert "We can read a line immediately." in all_msgs
|
||||
|
||||
|
||||
@ -505,11 +503,11 @@ def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg):
|
||||
got_args_spy = QSignalSpy(ipc_server.got_args)
|
||||
|
||||
signals = [ipc_server.got_invalid_data, connected_socket.disconnected]
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with qtbot.waitSignals(signals, raising=True):
|
||||
connected_socket.write(data)
|
||||
|
||||
messages = [r.message for r in caplog.records()]
|
||||
messages = [r.message for r in caplog.records]
|
||||
assert messages[-1] == 'Ignoring invalid IPC data.'
|
||||
assert messages[-2].startswith(msg)
|
||||
assert not got_args_spy
|
||||
@ -542,11 +540,11 @@ class TestSendToRunningInstance:
|
||||
def test_no_server(self, caplog):
|
||||
sent = ipc.send_to_running_instance('qute-test', [], None)
|
||||
assert not sent
|
||||
msg = caplog.records()[-1].message
|
||||
msg = caplog.records[-1].message
|
||||
assert msg == "No existing instance present (error 2)"
|
||||
|
||||
@pytest.mark.parametrize('has_cwd', [True, False])
|
||||
@pytest.mark.posix(reason="Causes random trouble on Windows")
|
||||
@pytest.mark.linux(reason="Causes random trouble on Windows and OS X")
|
||||
def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd):
|
||||
ipc_server.listen()
|
||||
spy = QSignalSpy(ipc_server.got_args)
|
||||
@ -610,12 +608,12 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
|
||||
with qtbot.waitSignal(ipc_server._server.newConnection, raising=True):
|
||||
qlocalsocket.connectToServer('qute-test')
|
||||
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with qtbot.waitSignal(qlocalsocket.disconnected, raising=True,
|
||||
timeout=5000):
|
||||
pass
|
||||
|
||||
assert caplog.records()[-1].message == "IPC connection timed out."
|
||||
assert caplog.records[-1].message == "IPC connection timed out."
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method, args, is_warning', [
|
||||
@ -628,13 +626,13 @@ def test_ipcserver_socket_none(ipc_server, caplog, method, args, is_warning):
|
||||
assert ipc_server._socket is None
|
||||
|
||||
if is_warning:
|
||||
with caplog.atLevel(logging.WARNING):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
func(*args)
|
||||
else:
|
||||
func(*args)
|
||||
|
||||
msg = "In {} with None socket!".format(method)
|
||||
assert msg in [r.message for r in caplog.records()]
|
||||
assert msg in [r.message for r in caplog.records]
|
||||
|
||||
|
||||
class TestSendOrListen:
|
||||
@ -683,7 +681,7 @@ class TestSendOrListen:
|
||||
def test_normal_connection(self, caplog, qtbot, args):
|
||||
ret_server = ipc.send_or_listen(args)
|
||||
assert isinstance(ret_server, ipc.IPCServer)
|
||||
msgs = [e.message for e in caplog.records()]
|
||||
msgs = [e.message for e in caplog.records]
|
||||
assert "Starting IPC server..." in msgs
|
||||
objreg_server = objreg.get('ipc-server')
|
||||
assert objreg_server is ret_server
|
||||
@ -698,7 +696,7 @@ class TestSendOrListen:
|
||||
with qtbot.waitSignal(legacy_server.got_args, raising=True):
|
||||
ret = ipc.send_or_listen(args)
|
||||
assert ret is None
|
||||
msgs = [e.message for e in caplog.records()]
|
||||
msgs = [e.message for e in caplog.records]
|
||||
assert "Connecting to {}".format(legacy_server._socketname) in msgs
|
||||
|
||||
@pytest.mark.posix(reason="Unneeded on Windows")
|
||||
@ -775,7 +773,7 @@ class TestSendOrListen:
|
||||
|
||||
ret = ipc.send_or_listen(args)
|
||||
assert ret is None
|
||||
msgs = [e.message for e in caplog.records()]
|
||||
msgs = [e.message for e in caplog.records]
|
||||
assert "Got AddressInUseError, trying again." in msgs
|
||||
|
||||
@pytest.mark.parametrize('has_error, exc_name, exc_msg', [
|
||||
@ -812,12 +810,11 @@ class TestSendOrListen:
|
||||
QLocalSocket.ConnectionRefusedError, # error() gets called twice
|
||||
]
|
||||
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with pytest.raises(ipc.Error):
|
||||
ipc.send_or_listen(args)
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert len(caplog.records) == 1
|
||||
|
||||
error_msgs = [
|
||||
'Handling fatal misc.ipc.{} with --no-err-windows!'.format(
|
||||
@ -828,7 +825,7 @@ class TestSendOrListen:
|
||||
'post_text: Maybe another instance is running but frozen?',
|
||||
'exception text: {}'.format(exc_msg),
|
||||
]
|
||||
assert records[0].msg == '\n'.join(error_msgs)
|
||||
assert caplog.records[0].msg == '\n'.join(error_msgs)
|
||||
|
||||
@pytest.mark.posix(reason="Flaky on Windows")
|
||||
def test_error_while_listening(self, qlocalserver_mock, caplog, args):
|
||||
@ -837,12 +834,11 @@ class TestSendOrListen:
|
||||
err = QAbstractSocket.SocketResourceError
|
||||
qlocalserver_mock().serverError.return_value = err
|
||||
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
with pytest.raises(ipc.Error):
|
||||
ipc.send_or_listen(args)
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert len(caplog.records) == 1
|
||||
|
||||
error_msgs = [
|
||||
'Handling fatal misc.ipc.ListenError with --no-err-windows!',
|
||||
@ -853,7 +849,7 @@ class TestSendOrListen:
|
||||
'exception text: Error while listening to IPC server: Error '
|
||||
'string (error 4)',
|
||||
]
|
||||
assert records[0].msg == '\n'.join(error_msgs)
|
||||
assert caplog.records[0].msg == '\n'.join(error_msgs)
|
||||
|
||||
|
||||
@pytest.mark.windows
|
||||
|
@ -821,10 +821,9 @@ class TestSessionDelete:
|
||||
tmpdir.chmod(0o555) # unwritable
|
||||
|
||||
with pytest.raises(cmdexc.CommandError) as excinfo:
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
sess_man.session_delete('foo')
|
||||
|
||||
assert str(excinfo.value).startswith('Error while deleting session: ')
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert records[0].message == 'Error while deleting session!'
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == 'Error while deleting session!'
|
||||
|
@ -51,7 +51,8 @@ class CovtestHelper:
|
||||
coveragerc = str(self._testdir.tmpdir / 'coveragerc')
|
||||
return self._testdir.runpytest('--cov=module',
|
||||
'--cov-config={}'.format(coveragerc),
|
||||
'--cov-report=xml')
|
||||
'--cov-report=xml',
|
||||
plugins=['no:faulthandler'])
|
||||
|
||||
def check(self, perfect_files=None):
|
||||
"""Run check_coverage.py and run its return value."""
|
||||
@ -165,6 +166,7 @@ def test_tested_unlisted(covtest):
|
||||
@pytest.mark.parametrize('args, reason', [
|
||||
(['-k', 'foo'], "because -k is given."),
|
||||
(['-m', 'foo'], "because -m is given."),
|
||||
(['--lf'], "because --lf is given."),
|
||||
(['blah', '-m', 'foo'], "because -m is given."),
|
||||
(['tests/foo'], "because there is nothing to check."),
|
||||
])
|
||||
|
@ -41,9 +41,8 @@ def test_log_events(qapp, caplog):
|
||||
obj = EventObject()
|
||||
qapp.postEvent(obj, QEvent(QEvent.User))
|
||||
qapp.processEvents()
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert records[0].msg == 'Event in test_debug.EventObject: User'
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].msg == 'Event in test_debug.EventObject: User'
|
||||
|
||||
|
||||
class SignalObject(QObject):
|
||||
@ -75,10 +74,9 @@ def test_log_signals(caplog, signal_obj):
|
||||
signal_obj.signal1.emit()
|
||||
signal_obj.signal2.emit('foo', 'bar')
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 2
|
||||
assert records[0].msg == 'Signal in <repr>: signal1()'
|
||||
assert records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
|
||||
assert len(caplog.records) == 2
|
||||
assert caplog.records[0].msg == 'Signal in <repr>: signal1()'
|
||||
assert caplog.records[1].msg == "Signal in <repr>: signal2('foo', 'bar')"
|
||||
|
||||
|
||||
class TestLogTime:
|
||||
@ -86,15 +84,14 @@ class TestLogTime:
|
||||
def test_duration(self, caplog):
|
||||
logger_name = 'qt-tests'
|
||||
|
||||
with caplog.atLevel(logging.DEBUG, logger_name):
|
||||
with caplog.at_level(logging.DEBUG, logger_name):
|
||||
with debug.log_time(logger_name, action='foobar'):
|
||||
time.sleep(0.1)
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert len(caplog.records) == 1
|
||||
|
||||
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
|
||||
match = pattern.match(records[0].msg)
|
||||
match = pattern.match(caplog.records[0].msg)
|
||||
assert match
|
||||
|
||||
duration = float(match.group(1))
|
||||
@ -104,11 +101,11 @@ class TestLogTime:
|
||||
"""Test with an explicit logger instead of a name."""
|
||||
logger_name = 'qt-tests'
|
||||
|
||||
with caplog.atLevel(logging.DEBUG, logger_name):
|
||||
with caplog.at_level(logging.DEBUG, logger_name):
|
||||
with debug.log_time(logging.getLogger(logger_name)):
|
||||
pass
|
||||
|
||||
assert len(caplog.records()) == 1
|
||||
assert len(caplog.records) == 1
|
||||
|
||||
def test_decorator(self, caplog):
|
||||
logger_name = 'qt-tests'
|
||||
@ -118,12 +115,11 @@ class TestLogTime:
|
||||
assert arg == 1
|
||||
assert kwarg == 2
|
||||
|
||||
with caplog.atLevel(logging.DEBUG, logger_name):
|
||||
with caplog.at_level(logging.DEBUG, logger_name):
|
||||
func(1, kwarg=2)
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert records[0].msg.startswith('Foo took')
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].msg.startswith('Foo took')
|
||||
|
||||
|
||||
class TestQEnumKey:
|
||||
|
@ -52,12 +52,11 @@ def test_no_err_windows(caplog, exc, name, exc_text):
|
||||
try:
|
||||
raise exc
|
||||
except Exception as e:
|
||||
with caplog.atLevel(logging.ERROR):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
error.handle_fatal_exc(e, Args(no_err_windows=True), 'title',
|
||||
pre_text='pre', post_text='post')
|
||||
|
||||
records = caplog.records()
|
||||
assert len(records) == 1
|
||||
assert len(caplog.records) == 1
|
||||
|
||||
expected = [
|
||||
'Handling fatal {} with --no-err-windows!'.format(name),
|
||||
@ -67,7 +66,7 @@ def test_no_err_windows(caplog, exc, name, exc_text):
|
||||
'post_text: post',
|
||||
'exception text: {}'.format(exc_text),
|
||||
]
|
||||
assert records[0].msg == '\n'.join(expected)
|
||||
assert caplog.records[0].msg == '\n'.join(expected)
|
||||
|
||||
|
||||
# This happens on Xvfb for some reason
|
||||
|
@ -25,7 +25,7 @@ import itertools
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import pytest_capturelog # pylint: disable=import-error
|
||||
import pytest_catchlog # pylint: disable=import-error
|
||||
|
||||
from qutebrowser.utils import log
|
||||
|
||||
@ -60,10 +60,11 @@ def restore_loggers():
|
||||
while root_logger.handlers:
|
||||
h = root_logger.handlers[0]
|
||||
root_logger.removeHandler(h)
|
||||
h.close()
|
||||
if not isinstance(h, pytest_catchlog.LogCaptureHandler):
|
||||
h.close()
|
||||
root_logger.setLevel(original_logging_level)
|
||||
for h in root_handlers:
|
||||
if not isinstance(h, pytest_capturelog.CaptureLogHandler):
|
||||
if not isinstance(h, pytest_catchlog.LogCaptureHandler):
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/856
|
||||
root_logger.addHandler(h)
|
||||
logging._acquireLock()
|
||||
@ -238,30 +239,30 @@ class TestHideQtWarning:
|
||||
def test_unfiltered(self, logger, caplog):
|
||||
"""Test a message which is not filtered."""
|
||||
with log.hide_qt_warning("World", 'qt-tests'):
|
||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
||||
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||
logger.warning("Hello World")
|
||||
assert len(caplog.records()) == 1
|
||||
record = caplog.records()[0]
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == 'WARNING'
|
||||
assert record.message == "Hello World"
|
||||
|
||||
def test_filtered_exact(self, logger, caplog):
|
||||
"""Test a message which is filtered (exact match)."""
|
||||
with log.hide_qt_warning("Hello", 'qt-tests'):
|
||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
||||
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||
logger.warning("Hello")
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
||||
def test_filtered_start(self, logger, caplog):
|
||||
"""Test a message which is filtered (match at line start)."""
|
||||
with log.hide_qt_warning("Hello", 'qt-tests'):
|
||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
||||
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||
logger.warning("Hello World")
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
||||
def test_filtered_whitespace(self, logger, caplog):
|
||||
"""Test a message which is filtered (match with whitespace)."""
|
||||
with log.hide_qt_warning("Hello", 'qt-tests'):
|
||||
with caplog.atLevel(logging.WARNING, 'qt-tests'):
|
||||
with caplog.at_level(logging.WARNING, 'qt-tests'):
|
||||
logger.warning(" Hello World ")
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
@ -263,10 +263,10 @@ class TestInitCacheDirTag:
|
||||
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
|
||||
lambda: str(tmpdir))
|
||||
mocker.patch('builtins.open', side_effect=OSError)
|
||||
with caplog.atLevel(logging.ERROR, 'init'):
|
||||
with caplog.at_level(logging.ERROR, 'init'):
|
||||
standarddir._init_cachedir_tag()
|
||||
assert len(caplog.records()) == 1
|
||||
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG'
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == 'Failed to create CACHEDIR.TAG'
|
||||
assert not tmpdir.listdir()
|
||||
|
||||
|
||||
|
@ -241,11 +241,11 @@ class TestActuteWarning:
|
||||
mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError,
|
||||
create=True)
|
||||
|
||||
with caplog.atLevel(logging.ERROR, 'init'):
|
||||
with caplog.at_level(logging.ERROR, 'init'):
|
||||
utils.actute_warning()
|
||||
|
||||
assert len(caplog.records()) == 1
|
||||
assert caplog.records()[0].message == 'Failed to read Compose file'
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == 'Failed to read Compose file'
|
||||
out, _err = capsys.readouterr()
|
||||
assert not out
|
||||
|
||||
@ -427,8 +427,7 @@ class TestFormatSize:
|
||||
|
||||
class TestKeyToString:
|
||||
|
||||
KEYS = [(k, v) for k, v in sorted(vars(Qt).items())
|
||||
if isinstance(v, Qt.Key)]
|
||||
"""Test key_to_string."""
|
||||
|
||||
@pytest.mark.parametrize('key, expected', [
|
||||
(Qt.Key_Blue, 'Blue'),
|
||||
@ -449,13 +448,15 @@ class TestKeyToString:
|
||||
# want to know if the mapping still behaves properly.
|
||||
assert utils.key_to_string(Qt.Key_A) == 'A'
|
||||
|
||||
@pytest.mark.parametrize('key', [e[1] for e in KEYS],
|
||||
ids=[e[0] for e in KEYS])
|
||||
def test_all(self, key):
|
||||
def test_all(self):
|
||||
"""Make sure there's some sensible output for all keys."""
|
||||
string = utils.key_to_string(key)
|
||||
assert string
|
||||
string.encode('utf-8') # make sure it's encodable
|
||||
for name, value in sorted(vars(Qt).items()):
|
||||
if not isinstance(value, Qt.Key):
|
||||
continue
|
||||
print(name)
|
||||
string = utils.key_to_string(value)
|
||||
assert string
|
||||
string.encode('utf-8') # make sure it's encodable
|
||||
|
||||
|
||||
class TestKeyEventToString:
|
||||
@ -669,12 +670,12 @@ class TestPreventExceptions:
|
||||
|
||||
def test_raising(self, caplog):
|
||||
"""Test with a raising function."""
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
ret = self.func_raising()
|
||||
assert ret == 42
|
||||
assert len(caplog.records()) == 1
|
||||
assert len(caplog.records) == 1
|
||||
expected = 'Error in test_utils.TestPreventExceptions.func_raising'
|
||||
actual = caplog.records()[0].message
|
||||
actual = caplog.records[0].message
|
||||
assert actual == expected
|
||||
|
||||
@utils.prevent_exceptions(42)
|
||||
@ -683,10 +684,10 @@ class TestPreventExceptions:
|
||||
|
||||
def test_not_raising(self, caplog):
|
||||
"""Test with a non-raising function."""
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
ret = self.func_not_raising()
|
||||
assert ret == 23
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
||||
@utils.prevent_exceptions(42, True)
|
||||
def func_predicate_true(self):
|
||||
@ -694,10 +695,10 @@ class TestPreventExceptions:
|
||||
|
||||
def test_predicate_true(self, caplog):
|
||||
"""Test with a True predicate."""
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
ret = self.func_predicate_true()
|
||||
assert ret == 42
|
||||
assert len(caplog.records()) == 1
|
||||
assert len(caplog.records) == 1
|
||||
|
||||
@utils.prevent_exceptions(42, False)
|
||||
def func_predicate_false(self):
|
||||
@ -705,10 +706,10 @@ class TestPreventExceptions:
|
||||
|
||||
def test_predicate_false(self, caplog):
|
||||
"""Test with a False predicate."""
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
with pytest.raises(Exception):
|
||||
self.func_predicate_false()
|
||||
assert not caplog.records()
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
class Obj:
|
||||
|
@ -108,7 +108,7 @@ class TestGitStr:
|
||||
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
|
||||
raising=False)
|
||||
commit_file_mock.side_effect = OSError
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
assert version._git_str() is None
|
||||
|
||||
@pytest.mark.not_frozen
|
||||
@ -136,7 +136,7 @@ class TestGitStr:
|
||||
m.path.join.side_effect = OSError
|
||||
mocker.patch('qutebrowser.utils.version.utils.read_file',
|
||||
side_effect=OSError)
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
assert version._git_str() is None
|
||||
|
||||
@pytest.mark.not_frozen
|
||||
@ -145,10 +145,10 @@ class TestGitStr:
|
||||
"""Test with undefined __file__ but available git-commit-id."""
|
||||
monkeypatch.delattr('qutebrowser.utils.version.__file__')
|
||||
commit_file_mock.return_value = '0deadcode'
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
assert version._git_str() == '0deadcode'
|
||||
assert len(caplog.records()) == 1
|
||||
assert caplog.records()[0].message == "Error while getting git path"
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == "Error while getting git path"
|
||||
|
||||
|
||||
def _has_git():
|
||||
@ -294,11 +294,11 @@ def test_release_info(files, expected, caplog, monkeypatch):
|
||||
fake = ReleaseInfoFake(files)
|
||||
monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake)
|
||||
monkeypatch.setattr(version, 'open', fake.open_fake, raising=False)
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
assert version._release_info() == expected
|
||||
if files is None:
|
||||
assert len(caplog.records()) == 1
|
||||
assert caplog.records()[0].message == "Error while reading fake-file."
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].message == "Error while reading fake-file."
|
||||
|
||||
|
||||
class ImportFake:
|
||||
|
@ -86,6 +86,6 @@ def test_abort_typeerror(question, qtbot, mocker, caplog):
|
||||
"""Test Question.abort() with .emit() raising a TypeError."""
|
||||
signal_mock = mocker.patch('qutebrowser.utils.usertypes.Question.aborted')
|
||||
signal_mock.emit.side_effect = TypeError
|
||||
with caplog.atLevel(logging.ERROR, 'misc'):
|
||||
with caplog.at_level(logging.ERROR, 'misc'):
|
||||
question.abort()
|
||||
assert caplog.records()[0].message == 'Error while aborting question'
|
||||
assert caplog.records[0].message == 'Error while aborting question'
|
||||
|
13
tox.ini
13
tox.ini
@ -26,11 +26,11 @@ deps =
|
||||
parse==1.6.6
|
||||
parse-type==0.3.4
|
||||
py==1.4.30
|
||||
pytest==2.7.3 # rq.filter: <2.8.0
|
||||
pytest==2.8.2
|
||||
pytest-bdd==2.15.0
|
||||
pytest-capturelog==0.7
|
||||
pytest-catchlog==1.2.0
|
||||
pytest-cov==2.2.0
|
||||
pytest-faulthandler==1.0.1
|
||||
pytest-faulthandler==1.1.0
|
||||
pytest-html==1.7
|
||||
pytest-mock==0.8.1
|
||||
pytest-qt==1.9.0
|
||||
@ -39,12 +39,12 @@ deps =
|
||||
six==1.10.0
|
||||
termcolor==1.1.0
|
||||
vulture==0.8.1
|
||||
Werkzeug==0.11.1
|
||||
Werkzeug==0.11.2
|
||||
wheel==0.26.0
|
||||
xvfbwrapper==0.2.5
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} -m py.test --strict -rfEsw --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||
{envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||
{envpython} scripts/dev/check_coverage.py {posargs}
|
||||
|
||||
[testenv:mkvenv]
|
||||
@ -105,6 +105,7 @@ deps =
|
||||
astroid==1.3.8
|
||||
pylint==1.4.4
|
||||
logilab-common==1.1.0
|
||||
requests==2.8.1
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
|
||||
@ -204,7 +205,7 @@ basepython = python3
|
||||
skip_install = true
|
||||
passenv =
|
||||
deps =
|
||||
check-manifest==0.27
|
||||
check-manifest==0.28
|
||||
commands =
|
||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||
|
||||
|
17
www/header.asciidoc
Normal file
17
www/header.asciidoc
Normal file
@ -0,0 +1,17 @@
|
||||
+++
|
||||
<div id="headline">
|
||||
<img class="qutebrowser-logo" src="icons/qutebrowser.svg" />
|
||||
<div class="text">
|
||||
<h1>qutebrowser</h1>
|
||||
A keyboard-driven browser.
|
||||
</div>
|
||||
</div>
|
||||
<div id="menu">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="FAQ.html">FAQ</a>
|
||||
<a href="INSTALL.html">Install</a>
|
||||
<a href="CHANGELOG.html">Changelog</a>
|
||||
<a href="CONTRIBUTING.html">Contributing</a>
|
||||
<a href="https://www.github.com/The-Compiler/qutebrowser">GitHub</a>
|
||||
</div>
|
||||
+++
|
BIN
www/media/OpenSans-Bold.woff2
Normal file
BIN
www/media/OpenSans-Bold.woff2
Normal file
Binary file not shown.
255
www/media/OpenSans-License.asciidoc
Normal file
255
www/media/OpenSans-License.asciidoc
Normal file
@ -0,0 +1,255 @@
|
||||
License notice for both OpenSans font files
|
||||
===========================================
|
||||
|
||||
Open Sans font by https://www.google.com/fonts/specimen/Open+Sans[Google], licensed under the http://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0] license.
|
||||
|
||||
[options="header"]
|
||||
|=================================================================================
|
||||
|File |Copyright
|
||||
|`OpenSans-Regular.woff2` |Digitized data copyright 2010-2011, Google Corporation.
|
||||
|`OpenSans-Bold.woff2` |Digitized data copyright 2010-2011, Google Corporation.
|
||||
|=================================================================================
|
||||
|
||||
|
||||
MANIFEST.json
|
||||
-------------
|
||||
|
||||
Here's an excerpt of the https://github.com/google/fonts/blob/master/apache/opensans/METADATA.json[MANIFEST.json] file
|
||||
from https://github.com/google/fonts/blob/master/apache/opensans/[the offical repository]:
|
||||
|
||||
----
|
||||
{
|
||||
"name": "Open Sans",
|
||||
"designer": "Steve Matteson",
|
||||
"license": "Apache2",
|
||||
"visibility": "External",
|
||||
"category": "Sans Serif",
|
||||
"size": 113987,
|
||||
"fonts": [
|
||||
{
|
||||
"name": "Open Sans",
|
||||
"style": "normal",
|
||||
"weight": 400,
|
||||
"filename": "OpenSans-Regular.ttf",
|
||||
"postScriptName": "OpenSans",
|
||||
"fullName": "Open Sans",
|
||||
"copyright": "Digitized data copyright 2010-2011, Google Corporation."
|
||||
},
|
||||
"name": "Open Sans",
|
||||
"style": "normal",
|
||||
"weight": 700,
|
||||
"filename": "OpenSans-Bold.ttf",
|
||||
"postScriptName": "OpenSans-Bold",
|
||||
"fullName": "Open Sans Bold",
|
||||
"copyright": "Digitized data copyright 2010-2011, Google Corporation."
|
||||
}
|
||||
]
|
||||
}
|
||||
----
|
||||
|
||||
The Apache 2.0 License text
|
||||
---------------------------
|
||||
|
||||
----
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
----
|
BIN
www/media/OpenSans-Regular.woff2
Normal file
BIN
www/media/OpenSans-Regular.woff2
Normal file
Binary file not shown.
BIN
www/media/favicon.png
Normal file
BIN
www/media/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
11
www/media/font.css
Normal file
11
www/media/font.css
Normal file
@ -0,0 +1,11 @@
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
font-weight: normal;
|
||||
src: url(OpenSans-Regular.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-weight: bold;
|
||||
src: url(OpenSans-Bold.woff2) format("woff2");
|
||||
}
|
211
www/media/qutebrowser.svg
Normal file
211
www/media/qutebrowser.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 21 KiB |
181
www/qute.css
Normal file
181
www/qute.css
Normal file
@ -0,0 +1,181 @@
|
||||
* {
|
||||
margin: 0px 0px;
|
||||
padding: 0px 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-text-size-adjust: none;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
#header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#headline {
|
||||
background-color: #333333;
|
||||
padding: 20px 20px;
|
||||
overflow: auto;
|
||||
color: #888;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
#headline .qutebrowser-logo {
|
||||
display: block;
|
||||
height: 70px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#headline .text {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#headline .text h1 {
|
||||
color: #1e89c6;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#headline .text {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: 0px 20px;
|
||||
background-color: #555555;
|
||||
color: #CCC;
|
||||
overflow: auto;
|
||||
margin-bottom: 10px;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
#menu a {
|
||||
color: #CCC;
|
||||
text-decoration: none;
|
||||
background-color: #555555;
|
||||
padding: 10px 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#menu a:hover {
|
||||
background-color: #1e89c6;
|
||||
}
|
||||
|
||||
.sect1 {
|
||||
padding: 10px 40px;
|
||||
}
|
||||
|
||||
.sect2 {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
|
||||
div.footnote {
|
||||
padding: 10px 40px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0px 40px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #0A396E;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
}
|
||||
|
||||
.ulist {
|
||||
padding-left: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding: 20px 40px;
|
||||
border-top: 1px solid #CCCCCC;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1e89c6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hdlist1 {
|
||||
color: #0A396E;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #DDDDDD;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.listingblock {
|
||||
padding: 10px 10px;
|
||||
background-color: #DDDDDD;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
#headline .qutebrowser-logo {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
width: 30%;
|
||||
height: auto;
|
||||
float: none;
|
||||
}
|
||||
|
||||
#headline .text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: 0px 0px;
|
||||
background-color: #555555;
|
||||
color: #CCC;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#menu a {
|
||||
color: #CCC;
|
||||
text-decoration: none;
|
||||
background-color: #555555;
|
||||
width: 100%;
|
||||
padding: 10px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;">
|
||||
<link href='media/font.css' rel='stylesheet' type='text/css'>
|
||||
<link rel="icon" href="media/favicon.png" type="image/png">
|
||||
<style>
|
Loading…
Reference in New Issue
Block a user