diff --git a/.pylintrc b/.pylintrc
index 5c1717e92..83ffc5fc1 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -51,3 +51,6 @@ defining-attr-methods=__init__,__new__,setUp
[DESIGN]
max-args=10
+
+[TYPECHECK]
+ignored-classes=WebElementWrapper
diff --git a/doc/TODO b/doc/TODO
index 9215849f3..f829ac6cd 100644
--- a/doc/TODO
+++ b/doc/TODO
@@ -127,8 +127,6 @@ style
- Always use a list as argument for namedtuple?
- Refactor enum() so it uses a list as second argument (like python
enum/namedtuple).
-- utils.webelem could be a wrapper class over QWebElement instead of a
- collection of functions.
dwb keybindings to possibly implement
=====================================
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index 9a9835cdc..13173c674 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -763,13 +763,14 @@ class CommandDispatcher:
and do everything async.
"""
frame = self._current_widget().page().currentFrame()
- elem = webelem.focus_elem(frame)
- if elem.isNull():
+ try:
+ elem = webelem.focus_elem(frame)
+ except webelem.IsNullError:
raise cmdexc.CommandError("No element focused!")
- if not webelem.is_editable(elem, strict=True):
+ if not elem.is_editable(strict=True):
raise cmdexc.CommandError("Focused element is not editable!")
- if webelem.is_content_editable(elem):
- text = elem.toPlainText()
+ if elem.is_content_editable():
+ text = str(elem)
else:
text = elem.evaluateJavaScript('this.value')
self._editor = editor.ExternalEditor(self._tabs)
@@ -783,17 +784,18 @@ class CommandDispatcher:
Callback for QProcess when the editor was closed.
Args:
- elem: The QWebElement which was modified.
+ elem: The WebElementWrapper which was modified.
text: The new text to insert.
"""
- if elem.isNull():
+ try:
+ if elem.is_content_editable():
+ log.misc.debug("Filling element {} via setPlainText.".format(
+ elem.debug_text()))
+ elem.setPlainText(text)
+ else:
+ log.misc.debug("Filling element {} via javascript.".format(
+ elem.debug_text()))
+ text = webelem.javascript_escape(text)
+ elem.evaluateJavaScript("this.value='{}'".format(text))
+ except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")
- if webelem.is_content_editable(elem):
- log.misc.debug("Filling element {} via setPlainText.".format(
- webelem.debug_text(elem)))
- elem.setPlainText(text)
- else:
- log.misc.debug("Filling element {} via javascript.".format(
- webelem.debug_text(elem)))
- text = webelem.javascript_escape(text)
- elem.evaluateJavaScript("this.value='{}'".format(text))
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 0ea1977d7..1583fa331 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -155,8 +155,10 @@ class HintManager(QObject):
def _cleanup(self):
"""Clean up after hinting."""
for elem in self._context.elems.values():
- if not elem.label.isNull():
+ try:
elem.label.removeFromDocument()
+ except webelem.IsNullError:
+ pass
text = self.HINT_TEXTS[self._context.target]
message.instance().maybe_reset_text(text)
self._context = None
@@ -263,7 +265,7 @@ class HintManager(QObject):
Return:
The CSS to set as a string.
"""
- if label is None or label.attribute('hidden') != 'true':
+ if label is None or label['hidden'] != 'true':
display = 'inline'
else:
display = 'none'
@@ -290,7 +292,9 @@ class HintManager(QObject):
# See: http://stackoverflow.com/q/7364852/2085149
doc.appendInside('{}'.format(
css, string))
- return doc.lastChild()
+ elem = webelem.WebElementWrapper(doc.lastChild())
+ elem['hidden'] = 'false'
+ return elem
def _click(self, elem):
"""Click an element.
@@ -306,9 +310,9 @@ class HintManager(QObject):
# FIXME Instead of clicking the center, we could have nicer heuristics.
# e.g. parse (-webkit-)border-radius correctly and click text fields at
# the bottom right, and everything else on the top left or so.
- pos = webelem.rect_on_view(elem).center()
- log.hints.debug("Clicking on '{}' at {}/{}".format(elem.toPlainText(),
- pos.x(), pos.y()))
+ pos = elem.rect_on_view().center()
+ log.hints.debug("Clicking on '{}' at {}/{}".format(
+ elem, pos.x(), pos.y()))
events = (
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
@@ -384,7 +388,7 @@ class HintManager(QObject):
Return:
A QUrl with the absolute URL, or None.
"""
- text = elem.attribute('href')
+ text = elem['href']
if not text:
return None
if baseurl is None:
@@ -402,12 +406,14 @@ class HintManager(QObject):
webelem.SELECTORS[webelem.Group.links])
rel_values = ('prev', 'previous') if prev else ('next')
for e in elems:
- if not e.hasAttribute('rel'):
+ e = webelem.WebElementWrapper(e)
+ try:
+ rel_attr = e['rel']
+ except KeyError:
continue
- rel_attr = e.attribute('rel')
if rel_attr in rel_values:
log.hints.debug("Found '{}' with rel={}".format(
- webelem.debug_text(e), rel_attr))
+ e.debug_text(), rel_attr))
return e
# Then check for regular links/buttons.
elems = frame.findAllElements(
@@ -418,7 +424,8 @@ class HintManager(QObject):
for regex in config.get('hints', option):
log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern))
for e in elems:
- text = e.toPlainText()
+ e = webelem.WebElementWrapper(e)
+ text = str(e)
if not text:
continue
if regex.search(text):
@@ -498,10 +505,11 @@ class HintManager(QObject):
ctx = HintContext()
ctx.frames = webelem.get_child_frames(mainframe)
for f in ctx.frames:
- elems += f.findAllElements(webelem.SELECTORS[group])
+ for e in f.findAllElements(webelem.SELECTORS[group]):
+ elems.append(webelem.WebElementWrapper(e))
filterfunc = webelem.FILTERS.get(group, lambda e: True)
visible_elems = [e for e in elems if filterfunc(e) and
- webelem.is_visible(e, mainframe)]
+ e.is_visible(mainframe)]
if not visible_elems:
raise cmdexc.CommandError("No elements found.")
ctx.target = target
@@ -529,34 +537,34 @@ class HintManager(QObject):
rest = string[len(keystr):]
elems.label.setInnerXml('{}{}'.format(
config.get('colors', 'hints.fg.match'), matched, rest))
- if elems.label.attribute('hidden') == 'true':
+ if elems.label['hidden'] == 'true':
# hidden element which matches again -> unhide it
- elems.label.setAttribute('hidden', 'false')
+ elems.label['hidden'] = 'false'
css = self._get_hint_css(elems.elem, elems.label)
- elems.label.setAttribute('style', css)
+ elems.label['style'] = css
else:
# element doesn't match anymore -> hide it
- elems.label.setAttribute('hidden', 'true')
+ elems.label['hidden'] = 'true'
css = self._get_hint_css(elems.elem, elems.label)
- elems.label.setAttribute('style', css)
+ elems.label['style'] = css
def filter_hints(self, filterstr):
"""Filter displayed hints according to a text."""
for elems in self._context.elems.values():
- if elems.elem.toPlainText().lower().startswith(filterstr):
- if elems.label.attribute('hidden') == 'true':
+ if str(elems.elem).lower().startswith(filterstr):
+ if elems.label['hidden'] == 'true':
# hidden element which matches again -> unhide it
- elems.label.setAttribute('hidden', 'false')
+ elems.label['hidden'] = 'false'
css = self._get_hint_css(elems.elem, elems.label)
- elems.label.setAttribute('style', css)
+ elems.label['style'] = css
else:
# element doesn't match anymore -> hide it
- elems.label.setAttribute('hidden', 'true')
+ elems.label['hidden'] = 'true'
css = self._get_hint_css(elems.elem, elems.label)
- elems.label.setAttribute('style', css)
+ elems.label['style'] = css
visible = {}
for k, e in self._context.elems.items():
- if e.label.attribute('hidden') != 'true':
+ if e.label['hidden'] != 'true':
visible[k] = e
if not visible:
# Whoops, filtered all hints
@@ -625,7 +633,7 @@ class HintManager(QObject):
log.hints.debug("Contents size changed...!")
for elems in self._context.elems.values():
css = self._get_hint_css(elems.elem, elems.label)
- elems.label.setAttribute('style', css)
+ elems.label['style'] = css
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
diff --git a/qutebrowser/test/stubs.py b/qutebrowser/test/stubs.py
index 2625ba909..a45cd4e12 100644
--- a/qutebrowser/test/stubs.py
+++ b/qutebrowser/test/stubs.py
@@ -24,7 +24,6 @@
from unittest import mock
from PyQt5.QtCore import QPoint, QProcess
-from PyQt5.QtWebKit import QWebElement
from PyQt5.QtNetwork import QNetworkRequest
@@ -78,84 +77,6 @@ class FakeKeyEvent:
self.modifiers = mock.Mock(return_value=modifiers)
-class FakeWebElement:
-
- """A stub for QWebElement."""
-
- def __init__(self, geometry=None, frame=None, null=False, visibility='',
- display='', attributes=None, tagname=None, classes=None):
- """Constructor.
-
- Args:
- geometry: The geometry of the QWebElement as QRect.
- frame: The QWebFrame the element is in.
- null: Whether the element is null or not.
- visibility: The CSS visibility style property calue.
- display: The CSS display style property calue.
- attributes: Boolean HTML attributes to be added.
- tagname: The tag name.
- classes: HTML classes to be added.
-
- Raise:
- ValueError if element is not null and geometry/frame are not given.
- """
- self.geometry = mock.Mock(return_value=geometry)
- self.webFrame = mock.Mock(return_value=frame)
- self.isNull = mock.Mock(return_value=null)
- self.tagName = mock.Mock(return_value=tagname)
- self._visibility = visibility
- self._display = display
- self._attributes = attributes
- self._classes = classes
-
- def toOuterXml(self):
- """Imitate toOuterXml."""
- return ''
-
- def styleProperty(self, name, strategy):
- """Return the CSS style property named name.
-
- Only display/visibility and ComputedStyle are simulated.
-
- Raise:
- ValueError if strategy is not ComputedStyle or name is not
- visibility/display.
- """
- if strategy != QWebElement.ComputedStyle:
- raise ValueError("styleProperty called with strategy != "
- "ComputedStyle ({})!".format(strategy))
- if name == 'visibility':
- return self._visibility
- elif name == 'display':
- return self._display
- else:
- raise ValueError("styleProperty called with unknown name "
- "'{}'".format(name))
-
- def hasAttribute(self, name):
- """Check if the element has an attribute named name."""
- if self._attributes is None:
- return False
- else:
- return name in self._attributes
-
- def attribute(self, name):
- """Get the attribute named name."""
- if self._attributes is None:
- return ''
- try:
- return self._attributes[name]
- except KeyError:
- return ''
-
- def classes(self):
- """Get the classes of the object."""
- if self._classes is not None:
- return self._classes.split(' ')
- else:
- return []
-
-
class FakeWebFrame:
"""A stub for QWebFrame."""
diff --git a/qutebrowser/test/utils/test_webelem.py b/qutebrowser/test/utils/test_webelem.py
index 978076b6a..c9d422b60 100644
--- a/qutebrowser/test/utils/test_webelem.py
+++ b/qutebrowser/test/utils/test_webelem.py
@@ -17,16 +17,86 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see .
+# pylint: disable=protected-access
+
"""Tests for the webelement utils."""
import unittest
+import unittest.mock
+import collections.abc
from PyQt5.QtCore import QRect, QPoint
+from PyQt5.QtWebKit import QWebElement
from qutebrowser.utils import webelem
from qutebrowser.test import stubs
+def get_webelem(geometry=None, frame=None, null=False, visibility='',
+ display='', attributes=None, tagname=None, classes=None):
+ """Factory for WebElementWrapper objects based on a mock.
+
+ Args:
+ geometry: The geometry of the QWebElement as QRect.
+ frame: The QWebFrame the element is in.
+ null: Whether the element is null or not.
+ visibility: The CSS visibility style property calue.
+ display: The CSS display style property calue.
+ attributes: Boolean HTML attributes to be added.
+ tagname: The tag name.
+ classes: HTML classes to be added.
+ """
+ elem = unittest.mock.Mock()
+ elem.isNull.return_value = null
+ elem.geometry.return_value = geometry
+ elem.webFrame.return_value = frame
+ elem.tagName.return_value = tagname
+ elem.toOuterXml.return_value = ''
+ if attributes is not None:
+ if not isinstance(attributes, collections.abc.Mapping):
+ attributes = {e: None for e in attributes}
+ elem.hasAttribute.side_effect = lambda k: k in attributes
+ elem.attribute.side_effect = lambda k: attributes.get(k, '')
+ elem.attributeNames.return_value = list(attributes)
+ else:
+ elem.hasAttribute.return_value = False
+ elem.attribute.return_value = ''
+ elem.attributeNames.return_value = []
+ if classes is not None:
+ elem.classes.return_value = classes.split(' ')
+ else:
+ elem.classes.return_value = []
+
+ def _style_property(name, strategy):
+ """Helper function to act as styleProperty method."""
+ if strategy != QWebElement.ComputedStyle:
+ raise ValueError("styleProperty called with strategy != "
+ "ComputedStyle ({})!".format(strategy))
+ if name == 'visibility':
+ return visibility
+ elif name == 'display':
+ return display
+ else:
+ raise ValueError("styleProperty called with unknown name "
+ "'{}'".format(name))
+
+ elem.styleProperty.side_effect = _style_property
+ wrapped = webelem.WebElementWrapper(elem)
+ if attributes is not None:
+ wrapped.update(attributes)
+ return wrapped
+
+
+class WebElementWrapperTests(unittest.TestCase):
+
+ """Test WebElementWrapper."""
+
+ def test_nullelem(self):
+ """Test __init__ with a null element."""
+ with self.assertRaises(webelem.IsNullError):
+ get_webelem(null=True)
+
+
class IsVisibleInvalidTests(unittest.TestCase):
"""Tests for is_visible with invalid elements.
@@ -44,19 +114,17 @@ class IsVisibleInvalidTests(unittest.TestCase):
geometry() and webFrame() should not be called, and ValueError should
be raised.
"""
- elem = stubs.FakeWebElement(null=True)
- with self.assertRaises(ValueError):
- webelem.is_visible(elem, self.frame)
- elem.isNull.assert_called_once_with()
- self.assertFalse(elem.geometry.called)
- self.assertFalse(elem.webFrame.called)
+ elem = get_webelem()
+ elem._elem.isNull.return_value = True
+ with self.assertRaises(webelem.IsNullError):
+ elem.is_visible(self.frame)
def test_invalid_invisible(self):
"""Test elements with an invalid geometry which are invisible."""
- elem = stubs.FakeWebElement(QRect(0, 0, 0, 0), self.frame)
+ elem = get_webelem(QRect(0, 0, 0, 0), self.frame)
self.assertFalse(elem.geometry().isValid())
self.assertEqual(elem.geometry().x(), 0)
- self.assertFalse(webelem.is_visible(elem, self.frame))
+ self.assertFalse(elem.is_visible(self.frame))
def test_invalid_visible(self):
"""Test elements with an invalid geometry which are visible.
@@ -64,9 +132,9 @@ class IsVisibleInvalidTests(unittest.TestCase):
This seems to happen sometimes in the real world, with real elements
which *are* visible, but don't have a valid geometry.
"""
- elem = stubs.FakeWebElement(QRect(10, 10, 0, 0), self.frame)
+ elem = get_webelem(QRect(10, 10, 0, 0), self.frame)
self.assertFalse(elem.geometry().isValid())
- self.assertTrue(webelem.is_visible(elem, self.frame))
+ self.assertTrue(elem.is_visible(self.frame))
class IsVisibleScrollTests(unittest.TestCase):
@@ -83,13 +151,13 @@ class IsVisibleScrollTests(unittest.TestCase):
def test_invisible(self):
"""Test elements which should be invisible due to scrolling."""
- elem = stubs.FakeWebElement(QRect(5, 5, 4, 4), self.frame)
- self.assertFalse(webelem.is_visible(elem, self.frame))
+ elem = get_webelem(QRect(5, 5, 4, 4), self.frame)
+ self.assertFalse(elem.is_visible(self.frame))
def test_visible(self):
"""Test elements which still should be visible after scrolling."""
- elem = stubs.FakeWebElement(QRect(10, 10, 1, 1), self.frame)
- self.assertTrue(webelem.is_visible(elem, self.frame))
+ elem = get_webelem(QRect(10, 10, 1, 1), self.frame)
+ self.assertTrue(elem.is_visible(self.frame))
class IsVisibleCssTests(unittest.TestCase):
@@ -105,27 +173,25 @@ class IsVisibleCssTests(unittest.TestCase):
def test_visibility_visible(self):
"""Check that elements with "visibility = visible" are visible."""
- elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame,
- visibility='visible')
- self.assertTrue(webelem.is_visible(elem, self.frame))
+ elem = get_webelem(QRect(0, 0, 10, 10), self.frame,
+ visibility='visible')
+ self.assertTrue(elem.is_visible(self.frame))
def test_visibility_hidden(self):
"""Check that elements with "visibility = hidden" are not visible."""
- elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame,
- visibility='hidden')
- self.assertFalse(webelem.is_visible(elem, self.frame))
+ elem = get_webelem(QRect(0, 0, 10, 10), self.frame,
+ visibility='hidden')
+ self.assertFalse(elem.is_visible(self.frame))
def test_display_inline(self):
"""Check that elements with "display = inline" are visible."""
- elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame,
- display='inline')
- self.assertTrue(webelem.is_visible(elem, self.frame))
+ elem = get_webelem(QRect(0, 0, 10, 10), self.frame, display='inline')
+ self.assertTrue(elem.is_visible(self.frame))
def test_display_none(self):
"""Check that elements with "display = none" are not visible."""
- elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame,
- display='none')
- self.assertFalse(webelem.is_visible(elem, self.frame))
+ elem = get_webelem(QRect(0, 0, 10, 10), self.frame, display='none')
+ self.assertFalse(elem.is_visible(self.frame))
class IsVisibleIframeTests(unittest.TestCase):
@@ -162,26 +228,26 @@ class IsVisibleIframeTests(unittest.TestCase):
self.frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
self.iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100),
parent=self.frame)
- self.elem1 = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.iframe)
- self.elem2 = stubs.FakeWebElement(QRect(20, 90, 10, 10), self.iframe)
- self.elem3 = stubs.FakeWebElement(QRect(20, 150, 10, 10), self.iframe)
- self.elem4 = stubs.FakeWebElement(QRect(30, 180, 10, 10), self.frame)
+ self.elem1 = get_webelem(QRect(0, 0, 10, 10), self.iframe)
+ self.elem2 = get_webelem(QRect(20, 90, 10, 10), self.iframe)
+ self.elem3 = get_webelem(QRect(20, 150, 10, 10), self.iframe)
+ self.elem4 = get_webelem(QRect(30, 180, 10, 10), self.frame)
def test_not_scrolled(self):
"""Test base situation."""
self.assertTrue(self.frame.geometry().contains(self.iframe.geometry()))
- self.assertTrue(webelem.is_visible(self.elem1, self.frame))
- self.assertTrue(webelem.is_visible(self.elem2, self.frame))
- self.assertFalse(webelem.is_visible(self.elem3, self.frame))
- self.assertTrue(webelem.is_visible(self.elem4, self.frame))
+ self.assertTrue(self.elem1.is_visible(self.frame))
+ self.assertTrue(self.elem2.is_visible(self.frame))
+ self.assertFalse(self.elem3.is_visible(self.frame))
+ self.assertTrue(self.elem4.is_visible(self.frame))
def test_iframe_scrolled(self):
"""Scroll iframe down so elem3 gets visible and elem1/elem2 not."""
self.iframe.scrollPosition.return_value = QPoint(0, 100)
- self.assertFalse(webelem.is_visible(self.elem1, self.frame))
- self.assertFalse(webelem.is_visible(self.elem2, self.frame))
- self.assertTrue(webelem.is_visible(self.elem3, self.frame))
- self.assertTrue(webelem.is_visible(self.elem4, self.frame))
+ self.assertFalse(self.elem1.is_visible(self.frame))
+ self.assertFalse(self.elem2.is_visible(self.frame))
+ self.assertTrue(self.elem3.is_visible(self.frame))
+ self.assertTrue(self.elem4.is_visible(self.frame))
def test_mainframe_scrolled_iframe_visible(self):
"""Scroll mainframe down so iframe is partly visible but elem1 not."""
@@ -189,10 +255,10 @@ class IsVisibleIframeTests(unittest.TestCase):
geom = self.frame.geometry().translated(self.frame.scrollPosition())
self.assertFalse(geom.contains(self.iframe.geometry()))
self.assertTrue(geom.intersects(self.iframe.geometry()))
- self.assertFalse(webelem.is_visible(self.elem1, self.frame))
- self.assertTrue(webelem.is_visible(self.elem2, self.frame))
- self.assertFalse(webelem.is_visible(self.elem3, self.frame))
- self.assertTrue(webelem.is_visible(self.elem4, self.frame))
+ self.assertFalse(self.elem1.is_visible(self.frame))
+ self.assertTrue(self.elem2.is_visible(self.frame))
+ self.assertFalse(self.elem3.is_visible(self.frame))
+ self.assertTrue(self.elem4.is_visible(self.frame))
def test_mainframe_scrolled_iframe_invisible(self):
"""Scroll mainframe down so iframe is invisible."""
@@ -200,10 +266,10 @@ class IsVisibleIframeTests(unittest.TestCase):
geom = self.frame.geometry().translated(self.frame.scrollPosition())
self.assertFalse(geom.contains(self.iframe.geometry()))
self.assertFalse(geom.intersects(self.iframe.geometry()))
- self.assertFalse(webelem.is_visible(self.elem1, self.frame))
- self.assertFalse(webelem.is_visible(self.elem2, self.frame))
- self.assertFalse(webelem.is_visible(self.elem3, self.frame))
- self.assertTrue(webelem.is_visible(self.elem4, self.frame))
+ self.assertFalse(self.elem1.is_visible(self.frame))
+ self.assertFalse(self.elem2.is_visible(self.frame))
+ self.assertFalse(self.elem3.is_visible(self.frame))
+ self.assertTrue(self.elem4.is_visible(self.frame))
class IsWritableTests(unittest.TestCase):
@@ -212,18 +278,18 @@ class IsWritableTests(unittest.TestCase):
def test_writable(self):
"""Test a normal element."""
- elem = stubs.FakeWebElement()
- self.assertTrue(webelem.is_writable(elem))
+ elem = get_webelem()
+ self.assertTrue(elem.is_writable())
def test_disabled(self):
"""Test a disabled element."""
- elem = stubs.FakeWebElement(attributes=['disabled'])
- self.assertFalse(webelem.is_writable(elem))
+ elem = get_webelem(attributes=['disabled'])
+ self.assertFalse(elem.is_writable())
def test_readonly(self):
"""Test a readonly element."""
- elem = stubs.FakeWebElement(attributes=['readonly'])
- self.assertFalse(webelem.is_writable(elem))
+ elem = get_webelem(attributes=['readonly'])
+ self.assertFalse(elem.is_writable())
class JavascriptEscapeTests(unittest.TestCase):
@@ -309,204 +375,186 @@ class IsEditableTests(unittest.TestCase):
def test_input_plain(self):
"""Test with plain input element."""
- elem = stubs.FakeWebElement(tagname='input')
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input')
+ self.assertTrue(elem.is_editable())
def test_input_text(self):
"""Test with text input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'text'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'text'})
+ self.assertTrue(elem.is_editable())
def test_input_text_caps(self):
"""Test with text input element with caps attributes."""
- elem = stubs.FakeWebElement(tagname='INPUT',
- attributes={'TYPE': 'TEXT'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='INPUT', attributes={'TYPE': 'TEXT'})
+ self.assertTrue(elem.is_editable())
def test_input_email(self):
"""Test with email input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'email'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'email'})
+ self.assertTrue(elem.is_editable())
def test_input_url(self):
"""Test with url input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'url'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'url'})
+ self.assertTrue(elem.is_editable())
def test_input_tel(self):
"""Test with tel input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'tel'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'tel'})
+ self.assertTrue(elem.is_editable())
def test_input_number(self):
"""Test with number input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'number'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'number'})
+ self.assertTrue(elem.is_editable())
def test_input_password(self):
"""Test with password input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'password'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'password'})
+ self.assertTrue(elem.is_editable())
def test_input_search(self):
"""Test with search input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'search'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'search'})
+ self.assertTrue(elem.is_editable())
def test_input_button(self):
"""Button should not be editable."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'button'})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'button'})
+ self.assertFalse(elem.is_editable())
def test_input_checkbox(self):
"""Checkbox should not be editable."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'type': 'checkbox'})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'type': 'checkbox'})
+ self.assertFalse(elem.is_editable())
def test_textarea(self):
"""Test textarea element."""
- elem = stubs.FakeWebElement(tagname='textarea')
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='textarea')
+ self.assertTrue(elem.is_editable())
def test_select(self):
"""Test selectbox."""
- elem = stubs.FakeWebElement(tagname='select')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='select')
+ self.assertFalse(elem.is_editable())
def test_input_disabled(self):
"""Test disabled input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'disabled': None})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'disabled': None})
+ self.assertFalse(elem.is_editable())
def test_input_readonly(self):
"""Test readonly input element."""
- elem = stubs.FakeWebElement(tagname='input',
- attributes={'readonly': None})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='input', attributes={'readonly': None})
+ self.assertFalse(elem.is_editable())
def test_textarea_disabled(self):
"""Test disabled textarea element."""
- elem = stubs.FakeWebElement(tagname='textarea',
- attributes={'disabled': None})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='textarea', attributes={'disabled': None})
+ self.assertFalse(elem.is_editable())
def test_textarea_readonly(self):
"""Test readonly textarea element."""
- elem = stubs.FakeWebElement(tagname='textarea',
- attributes={'readonly': None})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='textarea', attributes={'readonly': None})
+ self.assertFalse(elem.is_editable())
def test_embed_true(self):
"""Test embed-element with insert-mode-on-plugins true."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}})
- elem = stubs.FakeWebElement(tagname='embed')
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='embed')
+ self.assertTrue(elem.is_editable())
def test_applet_true(self):
"""Test applet-element with insert-mode-on-plugins true."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}})
- elem = stubs.FakeWebElement(tagname='applet')
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='applet')
+ self.assertTrue(elem.is_editable())
def test_embed_false(self):
"""Test embed-element with insert-mode-on-plugins false."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}})
- elem = stubs.FakeWebElement(tagname='embed')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='embed')
+ self.assertFalse(elem.is_editable())
def test_applet_false(self):
"""Test applet-element with insert-mode-on-plugins false."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}})
- elem = stubs.FakeWebElement(tagname='applet')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='applet')
+ self.assertFalse(elem.is_editable())
def test_object_no_type(self):
"""Test object-element without type."""
- elem = stubs.FakeWebElement(tagname='object')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='object')
+ self.assertFalse(elem.is_editable())
def test_object_image(self):
"""Test object-element with image type."""
- elem = stubs.FakeWebElement(tagname='object',
- attributes={'type': 'image/gif'})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='object', attributes={'type': 'image/gif'})
+ self.assertFalse(elem.is_editable())
def test_object_application(self):
"""Test object-element with application type."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}})
- elem = stubs.FakeWebElement(tagname='object',
- attributes={'type': 'application/foo'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='object',
+ attributes={'type': 'application/foo'})
+ self.assertTrue(elem.is_editable())
def test_object_application_false(self):
"""Test object-element with application type but not ...-on-plugins."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}})
- elem = stubs.FakeWebElement(tagname='object',
- attributes={'type': 'application/foo'})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='object',
+ attributes={'type': 'application/foo'})
+ self.assertFalse(elem.is_editable())
def test_object_classid(self):
"""Test object-element with classid."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}})
- elem = stubs.FakeWebElement(tagname='object',
- attributes={'type': 'foo',
- 'classid': 'foo'})
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='object',
+ attributes={'type': 'foo', 'classid': 'foo'})
+ self.assertTrue(elem.is_editable())
def test_object_classid_false(self):
"""Test object-element with classid but not insert-mode-on-plugins."""
webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}})
- elem = stubs.FakeWebElement(tagname='object',
- attributes={'type': 'foo',
- 'classid': 'foo'})
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='object',
+ attributes={'type': 'foo', 'classid': 'foo'})
+ self.assertFalse(elem.is_editable())
def test_div_empty(self):
"""Test div-element without class."""
- elem = stubs.FakeWebElement(tagname='div')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='div')
+ self.assertFalse(elem.is_editable())
def test_div_noneditable(self):
"""Test div-element with non-editableclass."""
- elem = stubs.FakeWebElement(tagname='div', classes='foo-kix-bar')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='div', classes='foo-kix-bar')
+ self.assertFalse(elem.is_editable())
def test_div_xik(self):
"""Test div-element with xik class."""
- elem = stubs.FakeWebElement(tagname='div', classes='foo kix-foo')
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='div', classes='foo kix-foo')
+ self.assertTrue(elem.is_editable())
def test_div_xik_caps(self):
"""Test div-element with xik class in caps.
This tests if classes are case sensitive as they should.
"""
- elem = stubs.FakeWebElement(tagname='div', classes='KIX-FOO')
- self.assertFalse(webelem.is_editable(elem))
+ elem = get_webelem(tagname='div', classes='KIX-FOO')
+ self.assertFalse(elem.is_editable())
def test_div_codemirror(self):
"""Test div-element with codemirror class."""
- elem = stubs.FakeWebElement(tagname='div',
- classes='foo CodeMirror-foo')
- self.assertTrue(webelem.is_editable(elem))
+ elem = get_webelem(tagname='div', classes='foo CodeMirror-foo')
+ self.assertTrue(elem.is_editable())
if __name__ == '__main__':
diff --git a/qutebrowser/utils/webelem.py b/qutebrowser/utils/webelem.py
index dcc2b7276..b0f637909 100644
--- a/qutebrowser/utils/webelem.py
+++ b/qutebrowser/utils/webelem.py
@@ -27,6 +27,9 @@ Module attributes:
without "href".
"""
+import collections.abc
+import functools
+
from PyQt5.QtCore import QRect, QUrl
from PyQt5.QtWebKit import QWebElement
@@ -50,80 +53,278 @@ SELECTORS = {
}
FILTERS = {
- Group.links: (lambda e: e.hasAttribute('href') and
- QUrl(e.attribute('href')).scheme() != 'javascript'),
+ Group.links: (lambda e: 'href' in e and
+ QUrl(e['href']).scheme() != 'javascript'),
}
-def is_visible(elem, mainframe):
- """Check whether the element is currently visible on the screen.
+class IsNullError(Exception):
- Args:
- elem: The QWebElement to check.
- mainframe: The main QWebFrame.
+ """Gets raised by WebElementWrapper if an element is null."""
- Return:
- True if the element is visible, False otherwise.
- """
- # CSS attributes which hide an element
- hidden_attributes = {
- 'visibility': 'hidden',
- 'display': 'none',
- }
- if elem.isNull():
- raise ValueError("Element is a null-element!")
- for k, v in hidden_attributes.items():
- if elem.styleProperty(k, QWebElement.ComputedStyle) == v:
+ pass
+
+
+class WebElementWrapper(collections.abc.MutableMapping):
+
+ """A wrapper around QWebElement to make it more intelligent."""
+
+ def __init__(self, elem):
+ if elem.isNull():
+ raise IsNullError('{} is a null element!'.format(elem))
+ self._elem = elem
+ for name in ('addClass', 'appendInside', 'appendOutside',
+ 'attributeNS', 'classes', 'clone', 'document',
+ 'encloseContentsWith', 'encloseWith',
+ 'evaluateJavaScript', 'findAll', 'findFirst',
+ 'firstChild', 'geometry', 'hasAttributeNS',
+ 'hasAttributes', 'hasClass', 'hasFocus', 'lastChild',
+ 'localName', 'namespaceUri', 'nextSibling', 'parent',
+ 'prefix', 'prependInside', 'prependOutside',
+ 'previousSibling', 'removeAllChildren',
+ 'removeAttributeNS', 'removeClass', 'removeFromDocument',
+ 'render', 'replace', 'setAttributeNS', 'setFocus',
+ 'setInnerXml', 'setOuterXml', 'setPlainText',
+ 'setStyleProperty', 'styleProperty', 'tagName',
+ 'takeFromDocument', 'toInnerXml', 'toOuterXml',
+ 'toggleClass', 'webFrame', '__eq__', '__ne__'):
+ # We don't wrap some methods for which we have better alternatives:
+ # - Mapping access for attributeNames/hasAttribute/setAttribute/
+ # attribute/removeAttribute.
+ # - isNull is checked automagically.
+ # - str(...) instead of toPlainText
+ # For the rest, we create a wrapper which checks if the element is
+ # null.
+
+ method = getattr(self._elem, name)
+
+ @functools.wraps(method)
+ def _wrapper(meth, *args, **kwargs):
+ # pylint: disable=missing-docstring
+ self._check_vanished()
+ return meth(*args, **kwargs)
+
+ wrapper = functools.partial(_wrapper, method)
+ functools.update_wrapper(wrapper, method)
+
+ setattr(self, name, wrapper)
+
+ def __str__(self):
+ self._check_vanished()
+ return self._elem.toPlainText()
+
+ def __repr__(self):
+ try:
+ return "".format(self.debug_text())
+ except IsNullError:
+ return ""
+
+ def __getitem__(self, key):
+ self._check_vanished()
+ if key not in self:
+ raise KeyError(key)
+ return self._elem.attribute(key)
+
+ def __setitem__(self, key, val):
+ self._check_vanished()
+ self._elem.setAttribute(key, val)
+
+ def __delitem__(self, key):
+ self._check_vanished()
+ if key not in self:
+ raise KeyError(key)
+ self.removeAttribute(key)
+
+ def __contains__(self, key):
+ self._check_vanished()
+ return self._elem.hasAttribute(key)
+
+ def __iter__(self):
+ self._check_vanished()
+ yield from self._elem.attributeNames()
+
+ def __len__(self):
+ self._check_vanished()
+ return len(self._elem.attributeNames())
+
+ def _check_vanished(self):
+ """Raise an exception if the element vanished (is null)."""
+ if self._elem.isNull():
+ raise IsNullError('Element {} vanished!'.format(
+ self._elem))
+
+ def is_visible(self, mainframe):
+ """Check whether the element is currently visible on the screen.
+
+ Args:
+ mainframe: The main QWebFrame.
+
+ Return:
+ True if the element is visible, False otherwise.
+ """
+ self._check_vanished()
+ # CSS attributes which hide an element
+ hidden_attributes = {
+ 'visibility': 'hidden',
+ 'display': 'none',
+ }
+ for k, v in hidden_attributes.items():
+ if self._elem.styleProperty(k, QWebElement.ComputedStyle) == v:
+ return False
+ geometry = self._elem.geometry()
+ if not geometry.isValid() and geometry.x() == 0:
+ # Most likely an invisible link
return False
- if (not elem.geometry().isValid()) and elem.geometry().x() == 0:
- # Most likely an invisible link
- return False
- # First check if the element is visible on screen
- elem_rect = rect_on_view(elem)
- if elem_rect.isValid():
- visible_on_screen = mainframe.geometry().intersects(elem_rect)
- else:
- # We got an invalid rectangle (width/height 0/0 probably), but this can
- # still be a valid link.
- visible_on_screen = mainframe.geometry().contains(elem_rect.topLeft())
- # Then check if it's visible in its frame if it's not in the main frame.
- elem_frame = elem.webFrame()
- elem_rect = elem.geometry()
- framegeom = QRect(elem_frame.geometry())
- if not framegeom.isValid():
- visible_in_frame = False
- elif elem_frame.parentFrame() is not None:
- framegeom.moveTo(0, 0)
- framegeom.translate(elem_frame.scrollPosition())
+ # First check if the element is visible on screen
+ elem_rect = self.rect_on_view()
if elem_rect.isValid():
- visible_in_frame = framegeom.intersects(elem_rect)
+ visible_on_screen = mainframe.geometry().intersects(elem_rect)
else:
# We got an invalid rectangle (width/height 0/0 probably), but this
# can still be a valid link.
- visible_in_frame = framegeom.contains(elem_rect.topLeft())
- else:
- visible_in_frame = visible_on_screen
- return all([visible_on_screen, visible_in_frame])
+ visible_on_screen = mainframe.geometry().contains(
+ elem_rect.topLeft())
+ # Then check if it's visible in its frame if it's not in the main
+ # frame.
+ elem_frame = self._elem.webFrame()
+ elem_rect = self._elem.geometry()
+ framegeom = QRect(elem_frame.geometry())
+ if not framegeom.isValid():
+ visible_in_frame = False
+ elif elem_frame.parentFrame() is not None:
+ framegeom.moveTo(0, 0)
+ framegeom.translate(elem_frame.scrollPosition())
+ if elem_rect.isValid():
+ visible_in_frame = framegeom.intersects(elem_rect)
+ else:
+ # We got an invalid rectangle (width/height 0/0 probably), but
+ # this can still be a valid link.
+ visible_in_frame = framegeom.contains(elem_rect.topLeft())
+ else:
+ visible_in_frame = visible_on_screen
+ return all([visible_on_screen, visible_in_frame])
+ def rect_on_view(self):
+ """Get the geometry of the element relative to the webview."""
+ self._check_vanished()
+ frame = self._elem.webFrame()
+ rect = QRect(self._elem.geometry())
+ while frame is not None:
+ rect.translate(frame.geometry().topLeft())
+ rect.translate(frame.scrollPosition() * -1)
+ frame = frame.parentFrame()
+ return rect
-def rect_on_view(elem):
- """Get the geometry of the element relative to the webview."""
- frame = elem.webFrame()
- rect = QRect(elem.geometry())
- while frame is not None:
- rect.translate(frame.geometry().topLeft())
- rect.translate(frame.scrollPosition() * -1)
- frame = frame.parentFrame()
- return rect
+ def is_writable(self):
+ """Check whether an element is writable."""
+ self._check_vanished()
+ return not ('disabled' in self or 'readonly' in self)
+ def is_content_editable(self):
+ """Check if an element has a contenteditable attribute.
-def is_writable(elem):
- """Check wheter an element is writable.
+ FIXME: Add tests.
- Args:
- elem: The QWebElement to check.
- """
- return not (elem.hasAttribute('disabled') or elem.hasAttribute('readonly'))
+ Args:
+ elem: The QWebElement to check.
+
+ Return:
+ True if the element has a contenteditable attribute,
+ False otherwise.
+ """
+ self._check_vanished()
+ try:
+ return self['contenteditable'].lower() not in ('false', 'inherit')
+ except KeyError:
+ return False
+
+ def _is_editable_object(self):
+ """Check if an object-element is editable."""
+ if 'type' not in self:
+ log.webview.debug("