Merge branch 'dirbrowser-issue-1334' of https://github.com/Kingdread/qutebrowser into Kingdread-dirbrowser-issue-1334

This commit is contained in:
Florian Bruhin 2016-03-29 22:38:04 +02:00
commit 6c7e2492e9
8 changed files with 384 additions and 19 deletions

View File

@ -57,9 +57,29 @@ def is_root(directory):
Return:
Whether the directory is a root directory or not.
"""
# If you're curious as why this works:
# dirname('/') = '/'
# dirname('/home') = '/'
# dirname('/home/') = '/home'
# dirname('/home/foo') = '/home'
# basically, for files (no trailing slash) it removes the file part, and
# for directories, it removes the trailing slash, so the only way for this
# to be equal is if the directory is the root directory.
return os.path.dirname(directory) == directory
def parent_dir(directory):
"""Return the parent directory for the given directory.
Args:
directory: The path to the directory.
Return:
The path to the parent directory.
"""
return os.path.normpath(os.path.join(directory, os.pardir))
def dirbrowser_html(path):
"""Get the directory browser web page.
@ -74,14 +94,14 @@ def dirbrowser_html(path):
if is_root(path):
parent = None
else:
parent = os.path.dirname(path)
parent = parent_dir(path)
try:
all_files = os.listdir(path)
except OSError as e:
html = jinja.render('error.html',
title="Error while reading directory",
url='file://{}'.format(path), error=str(e),
url='file:///{}'.format(path), error=str(e),
icon='')
return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -46,21 +46,21 @@ ul.files > li {
<p id="dirbrowserTitleText">Browse directory: {{url}}</p>
</div>
{% if parent %}
{% if parent is not none %}
<ul class="parent">
<li><a href="{{parent}}">..</a></li>
<li><a href="{{ file_url(parent) }}">..</a></li>
</ul>
{% endif %}
<ul class="folders">
{% for item in directories %}
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
{% endfor %}
</ul>
<ul class="files">
{% for item in files %}
<li><a href="file://{{item.absname}}">{{item.name}}</a></li>
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li>
{% endfor %}
</ul>
</div>

View File

@ -74,6 +74,15 @@ def resource_url(path):
return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
def file_url(path):
"""Return a file:// url (as string) to the given local path.
Arguments:
path: The absolute path to the local file
"""
return QUrl.fromLocalFile(path).toString(QUrl.FullyEncoded)
def render(template, **kwargs):
"""Render the given template and pass the given arguments to it."""
try:
@ -85,5 +94,7 @@ def render(template, **kwargs):
tb = traceback.format_exc()
return err_template.format(pagename=template, traceback=tb)
_env = jinja2.Environment(loader=Loader('html'), autoescape=_guess_autoescape)
_env.globals['resource_url'] = resource_url
_env.globals['file_url'] = file_url

View File

@ -0,0 +1,11 @@
<html>
<head>
<title>quteprocess.click_element test</title>
</head>
<body>
<span onclick='console.log("click_element clicked")'>Test Element</span>
<span onclick='console.log("click_element special chars")'>"Don't", he shouted</span>
<span>Duplicate</span>
<span>Duplicate</span>
</body>
</html>

View File

@ -37,6 +37,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl
import testprocess
from qutebrowser.misc import ipc
from qutebrowser.utils import log, utils
from qutebrowser.browser import webelem
from helpers import utils as testutils
@ -253,9 +254,14 @@ class QuteProc(testprocess.Process):
path if path != '/' else '')
def wait_for_js(self, message):
"""Wait for the given javascript console message."""
self.wait_for(category='js', function='javaScriptConsoleMessage',
message='[*] {}'.format(message))
"""Wait for the given javascript console message.
Return:
The LogLine.
"""
return self.wait_for(category='js',
function='javaScriptConsoleMessage',
message='[*] {}'.format(message))
def _is_error_logline(self, msg):
"""Check if the given LogLine is some kind of error message."""
@ -348,10 +354,14 @@ class QuteProc(testprocess.Process):
def open_path(self, path, *, new_tab=False, new_window=False, port=None,
https=False):
"""Open the given path on the local webserver in qutebrowser."""
url = self.path_to_url(path, port=port, https=https)
self.open_url(url, new_tab=new_tab, new_window=new_window)
def open_url(self, url, *, new_tab=False, new_window=False):
"""Open the given url in qutebrowser."""
if new_tab and new_window:
raise ValueError("new_tab and new_window given!")
url = self.path_to_url(path, port=port, https=https)
if new_tab:
self.send_cmd(':open -t ' + url)
elif new_window:
@ -418,6 +428,57 @@ class QuteProc(testprocess.Process):
"""Press the given keys using :fake-key."""
self.send_cmd(':fake-key -g "{}"'.format(keys))
def click_element(self, text):
"""Click the element with the given text."""
# Use Javascript and XPath to find the right element, use console.log
# to return an error (no element found, ambiguous element)
script = (
'var _es = document.evaluate(\'//*[text()={text}]\', document, '
'null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);'
'if (_es.snapshotLength == 0) {{ console.log("qute:no elems"); }} '
'else if (_es.snapshotLength > 1) {{ console.log("qute:ambiguous '
'elems") }} '
'else {{ console.log("qute:okay"); _es.snapshotItem(0).click() }}'
).format(text=webelem.javascript_escape(_xpath_escape(text)))
self.send_cmd(':jseval ' + script)
message = self.wait_for_js('qute:*').message
if message.endswith('qute:no elems'):
raise ValueError('No element with {!r} found'.format(text))
elif message.endswith('qute:ambiguous elems'):
raise ValueError('Element with {!r} is not unique'.format(text))
elif not message.endswith('qute:okay'):
raise ValueError('Invalid response from qutebrowser: {}'
.format(message))
def _xpath_escape(text):
"""Escape a string to be used in an XPath expression.
The resulting string should still be escaped with javascript_escape, to
prevent javascript from interpreting the quotes.
This function is needed because XPath does not provide any character
escaping mechanisms, so to get the string
"I'm back", he said
you have to use concat like
concat('"I', "'m back", '", he said')
Args:
text: Text to escape
Return:
The string "escaped" as a concat() call.
"""
# Shortcut if at most a single quoting style is used
if "'" not in text or '"' not in text:
return repr(text)
parts = re.split('([\'"])', text)
# Python's repr() of strings will automatically choose the right quote
# type. Since each part only contains one "type" of quote, no escaping
# should be necessary.
parts = [repr(part) for part in parts if part]
return 'concat({})'.format(', '.join(parts))
@pytest.yield_fixture(scope='module')
def quteproc_process(qapp, httpbin, request):

View File

@ -0,0 +1,189 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2016 Daniel Schadt
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Test the built-in directory browser."""
import os
import bs4
import collections
import pytest
from PyQt5.QtCore import QUrl
from qutebrowser.utils import jinja
class DirLayout:
"""Provide a fake directory layout to test dirbrowser."""
LAYOUT = [
'folder0/file00',
'folder0/file01',
'folder1/folder10/file100',
'folder1/file10',
'folder1/file11',
'file0',
'file1',
]
@classmethod
def layout_folders(cls):
"""Return all folders in the root directory of the layout."""
folders = set()
for path in cls.LAYOUT:
parts = path.split('/')
if len(parts) > 1:
folders.add(parts[0])
folders = list(folders)
folders.sort()
return folders
@classmethod
def get_folder_content(cls, name):
"""Return (folders, files) for the given folder in the root dir."""
folders = set()
files = set()
for path in cls.LAYOUT:
if not path.startswith(name + '/'):
continue
parts = path.split('/')
if len(parts) == 2:
files.add(parts[1])
else:
folders.add(parts[1])
folders = list(folders)
folders.sort()
files = list(files)
files.sort()
return (folders, files)
def __init__(self, factory):
self._factory = factory
self.base = factory.getbasetemp()
self.layout = factory.mktemp('layout')
self._mklayout()
def _mklayout(self):
for filename in self.LAYOUT:
self.layout.ensure(filename)
def file_url(self):
"""Return a file:// link to the directory."""
return jinja.file_url(str(self.layout))
def path(self, *parts):
"""Return the path to the given file inside the layout folder."""
return os.path.normpath(str(self.layout.join(*parts)))
def base_path(self):
"""Return the path of the base temporary folder."""
return os.path.normpath(str(self.base))
Parsed = collections.namedtuple('Parsed', 'path, parent, folders, files')
Item = collections.namedtuple('Item', 'path, link, text')
def parse(quteproc):
"""Parse the dirbrowser content from the given quteproc.
Args:
quteproc: The quteproc fixture.
"""
html = quteproc.get_content(plain=False)
soup = bs4.BeautifulSoup(html, 'html.parser')
print(soup.prettify())
title_prefix = 'Browse directory: '
# Strip off the title prefix to obtain the path of the folder that
# we're browsing
path = soup.title.string[len(title_prefix):]
path = os.path.normpath(path)
container = soup('div', id='dirbrowserContainer')[0]
parent_elem = container('ul', class_='parent')
if not parent_elem:
parent = None
else:
parent = QUrl(parent_elem[0].li.a['href']).toLocalFile()
parent = os.path.normpath(parent)
folders = []
files = []
for css_class, list_ in [('folders', folders), ('files', files)]:
for li in container('ul', class_=css_class)[0]('li'):
item_path = QUrl(li.a['href']).toLocalFile()
item_path = os.path.normpath(item_path)
list_.append(Item(path=item_path, link=li.a['href'],
text=str(li.a.string)))
return Parsed(path=path, parent=parent, folders=folders,
files=files)
@pytest.fixture(scope='module')
def dir_layout(tmpdir_factory):
return DirLayout(tmpdir_factory)
def test_parent_folder(dir_layout, quteproc):
quteproc.open_url(dir_layout.file_url())
page = parse(quteproc)
assert page.parent == dir_layout.base_path()
def test_parent_with_slash(dir_layout, quteproc):
"""Test the parent link with an URL that has a trailing slash."""
quteproc.open_url(dir_layout.file_url() + '/')
page = parse(quteproc)
assert page.parent == dir_layout.base_path()
def test_parent_in_root_dir(dir_layout, quteproc):
# This actually works on windows
root_path = os.path.realpath('/')
urlstr = QUrl.fromLocalFile(root_path).toString(QUrl.FullyEncoded)
quteproc.open_url(urlstr)
page = parse(quteproc)
assert page.parent is None
def test_enter_folder_smoke(dir_layout, quteproc):
quteproc.open_url(dir_layout.file_url())
quteproc.send_cmd(':hint all normal')
# a is the parent link, s is the first listed folder/file
quteproc.send_cmd(':follow-hint s')
page = parse(quteproc)
assert page.path == dir_layout.path('folder0')
@pytest.mark.parametrize('folder', DirLayout.layout_folders())
def test_enter_folder(dir_layout, quteproc, folder):
quteproc.open_url(dir_layout.file_url())
quteproc.click_element(text=folder)
page = parse(quteproc)
assert page.path == dir_layout.path(folder)
assert page.parent == dir_layout.path()
folders, files = DirLayout.get_folder_content(folder)
foldernames = [item.text for item in page.folders]
assert foldernames == folders
filenames = [item.text for item in page.files]
assert filenames == files

View File

@ -158,3 +158,40 @@ def test_log_line_parse(data, attrs):
def test_log_line_no_match():
with pytest.raises(testprocess.InvalidLine):
quteprocess.LogLine("Hello World!")
class TestClickElement:
@pytest.fixture(autouse=True)
def open_page(self, quteproc):
quteproc.open_path('data/click_element.html')
quteproc.wait_for_load_finished('data/click_element.html')
def test_click_element(self, quteproc):
quteproc.click_element('Test Element')
quteproc.wait_for_js('click_element clicked')
def test_click_special_chars(self, quteproc):
quteproc.click_element('"Don\'t", he shouted')
quteproc.wait_for_js('click_element special chars')
def test_duplicate(self, quteproc):
with pytest.raises(ValueError) as excinfo:
quteproc.click_element('Duplicate')
assert 'not unique' in str(excinfo.value)
def test_nonexistent(self, quteproc):
with pytest.raises(ValueError) as excinfo:
quteproc.click_element('no element exists with this text')
assert 'No element' in str(excinfo.value)
@pytest.mark.parametrize('string, expected', [
('Test', "'Test'"),
("Don't", '"Don\'t"'),
# This is some serious string escaping madness
('"Don\'t", he said',
"concat('\"', 'Don', \"'\", 't', '\"', ', he said')"),
])
def test_xpath_escape(string, expected):
assert quteprocess._xpath_escape(string) == expected

View File

@ -28,6 +28,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkRequest
from qutebrowser.browser.network import filescheme
from qutebrowser.utils import jinja
@pytest.mark.parametrize('create_file, create_dir, filterfunc, expected', [
@ -57,6 +58,8 @@ class TestIsRoot:
@pytest.mark.windows
@pytest.mark.parametrize('directory, is_root', [
('C:\\foo\\bar', False),
('C:\\foo\\', False),
('C:\\foo', False),
('C:\\', True)
])
@ -65,6 +68,8 @@ class TestIsRoot:
@pytest.mark.posix
@pytest.mark.parametrize('directory, is_root', [
('/foo/bar', False),
('/foo/', False),
('/foo', False),
('/', True)
])
@ -72,6 +77,38 @@ class TestIsRoot:
assert filescheme.is_root(directory) == is_root
class TestParentDir:
@pytest.mark.windows
@pytest.mark.parametrize('directory, parent', [
('C:\\foo\\bar', 'C:\\foo'),
('C:\\foo', 'C:\\'),
('C:\\foo\\', 'C:\\'),
('C:\\', 'C:\\'),
])
def test_windows(self, directory, parent):
assert filescheme.parent_dir(directory) == parent
@pytest.mark.posix
@pytest.mark.parametrize('directory, parent', [
('/home/foo', '/home'),
('/home', '/'),
('/home/', '/'),
('/', '/'),
])
def test_posix(self, directory, parent):
assert filescheme.parent_dir(directory) == parent
def _file_url(path):
"""Return a file:// url (as string) for the given LocalPath.
Arguments:
path: The filepath as LocalPath (as handled by py.path)
"""
return jinja.file_url(str(path))
class TestDirbrowserHtml:
Parsed = collections.namedtuple('Parsed', 'parent, folders, files')
@ -87,7 +124,7 @@ class TestDirbrowserHtml:
container = soup('div', id='dirbrowserContainer')[0]
parent_elem = container('ul', class_='parent')
if len(parent_elem) == 0:
if not parent_elem:
parent = None
else:
parent = parent_elem[0].li.a.string
@ -145,8 +182,8 @@ class TestDirbrowserHtml:
parsed = parser(str(tmpdir))
assert parsed.parent
assert not parsed.folders
foo_item = self.Item('file://' + str(foo_file), foo_file.relto(tmpdir))
bar_item = self.Item('file://' + str(bar_file), bar_file.relto(tmpdir))
foo_item = self.Item(_file_url(foo_file), foo_file.relto(tmpdir))
bar_item = self.Item(_file_url(bar_file), bar_file.relto(tmpdir))
assert parsed.files == [bar_item, foo_item]
def test_html_special_chars(self, tmpdir, parser):
@ -154,8 +191,7 @@ class TestDirbrowserHtml:
special_file.ensure()
parsed = parser(str(tmpdir))
item = self.Item('file://' + str(special_file),
special_file.relto(tmpdir))
item = self.Item(_file_url(special_file), special_file.relto(tmpdir))
assert parsed.files == [item]
def test_dirs(self, tmpdir, parser):
@ -167,8 +203,8 @@ class TestDirbrowserHtml:
parsed = parser(str(tmpdir))
assert parsed.parent
assert not parsed.files
foo_item = self.Item('file://' + str(foo_dir), foo_dir.relto(tmpdir))
bar_item = self.Item('file://' + str(bar_dir), bar_dir.relto(tmpdir))
foo_item = self.Item(_file_url(foo_dir), foo_dir.relto(tmpdir))
bar_item = self.Item(_file_url(bar_dir), bar_dir.relto(tmpdir))
assert parsed.folders == [bar_item, foo_item]
def test_mixed(self, tmpdir, parser):
@ -178,8 +214,8 @@ class TestDirbrowserHtml:
bar_dir.ensure(dir=True)
parsed = parser(str(tmpdir))
foo_item = self.Item('file://' + str(foo_file), foo_file.relto(tmpdir))
bar_item = self.Item('file://' + str(bar_dir), bar_dir.relto(tmpdir))
foo_item = self.Item(_file_url(foo_file), foo_file.relto(tmpdir))
bar_item = self.Item(_file_url(bar_dir), bar_dir.relto(tmpdir))
assert parsed.parent
assert parsed.files == [foo_item]
assert parsed.folders == [bar_item]