Improve webelement API

This commit is contained in:
Florian Bruhin 2014-09-04 08:00:05 +02:00
parent becc4490bc
commit b856bf3a47
8 changed files with 520 additions and 438 deletions

View File

@ -51,3 +51,6 @@ defining-attr-methods=__init__,__new__,setUp
[DESIGN] [DESIGN]
max-args=10 max-args=10
[TYPECHECK]
ignored-classes=WebElementWrapper

View File

@ -127,8 +127,6 @@ style
- Always use a list as argument for namedtuple? - Always use a list as argument for namedtuple?
- Refactor enum() so it uses a list as second argument (like python - Refactor enum() so it uses a list as second argument (like python
enum/namedtuple). enum/namedtuple).
- utils.webelem could be a wrapper class over QWebElement instead of a
collection of functions.
dwb keybindings to possibly implement dwb keybindings to possibly implement
===================================== =====================================

View File

@ -763,13 +763,14 @@ class CommandDispatcher:
and do everything async. and do everything async.
""" """
frame = self._current_widget().page().currentFrame() frame = self._current_widget().page().currentFrame()
try:
elem = webelem.focus_elem(frame) elem = webelem.focus_elem(frame)
if elem.isNull(): except webelem.IsNullError:
raise cmdexc.CommandError("No element focused!") 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!") raise cmdexc.CommandError("Focused element is not editable!")
if webelem.is_content_editable(elem): if elem.is_content_editable():
text = elem.toPlainText() text = str(elem)
else: else:
text = elem.evaluateJavaScript('this.value') text = elem.evaluateJavaScript('this.value')
self._editor = editor.ExternalEditor(self._tabs) self._editor = editor.ExternalEditor(self._tabs)
@ -783,17 +784,18 @@ class CommandDispatcher:
Callback for QProcess when the editor was closed. Callback for QProcess when the editor was closed.
Args: Args:
elem: The QWebElement which was modified. elem: The WebElementWrapper which was modified.
text: The new text to insert. text: The new text to insert.
""" """
if elem.isNull(): try:
raise cmdexc.CommandError("Element vanished while editing!") if elem.is_content_editable():
if webelem.is_content_editable(elem):
log.misc.debug("Filling element {} via setPlainText.".format( log.misc.debug("Filling element {} via setPlainText.".format(
webelem.debug_text(elem))) elem.debug_text()))
elem.setPlainText(text) elem.setPlainText(text)
else: else:
log.misc.debug("Filling element {} via javascript.".format( log.misc.debug("Filling element {} via javascript.".format(
webelem.debug_text(elem))) elem.debug_text()))
text = webelem.javascript_escape(text) text = webelem.javascript_escape(text)
elem.evaluateJavaScript("this.value='{}'".format(text)) elem.evaluateJavaScript("this.value='{}'".format(text))
except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")

View File

@ -155,8 +155,10 @@ class HintManager(QObject):
def _cleanup(self): def _cleanup(self):
"""Clean up after hinting.""" """Clean up after hinting."""
for elem in self._context.elems.values(): for elem in self._context.elems.values():
if not elem.label.isNull(): try:
elem.label.removeFromDocument() elem.label.removeFromDocument()
except webelem.IsNullError:
pass
text = self.HINT_TEXTS[self._context.target] text = self.HINT_TEXTS[self._context.target]
message.instance().maybe_reset_text(text) message.instance().maybe_reset_text(text)
self._context = None self._context = None
@ -263,7 +265,7 @@ class HintManager(QObject):
Return: Return:
The CSS to set as a string. 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' display = 'inline'
else: else:
display = 'none' display = 'none'
@ -290,7 +292,9 @@ class HintManager(QObject):
# See: http://stackoverflow.com/q/7364852/2085149 # See: http://stackoverflow.com/q/7364852/2085149
doc.appendInside('<span class="qutehint" style="{}">{}</span>'.format( doc.appendInside('<span class="qutehint" style="{}">{}</span>'.format(
css, string)) css, string))
return doc.lastChild() elem = webelem.WebElementWrapper(doc.lastChild())
elem['hidden'] = 'false'
return elem
def _click(self, elem): def _click(self, elem):
"""Click an element. """Click an element.
@ -306,9 +310,9 @@ class HintManager(QObject):
# FIXME Instead of clicking the center, we could have nicer heuristics. # FIXME Instead of clicking the center, we could have nicer heuristics.
# e.g. parse (-webkit-)border-radius correctly and click text fields at # e.g. parse (-webkit-)border-radius correctly and click text fields at
# the bottom right, and everything else on the top left or so. # the bottom right, and everything else on the top left or so.
pos = webelem.rect_on_view(elem).center() pos = elem.rect_on_view().center()
log.hints.debug("Clicking on '{}' at {}/{}".format(elem.toPlainText(), log.hints.debug("Clicking on '{}' at {}/{}".format(
pos.x(), pos.y())) elem, pos.x(), pos.y()))
events = ( events = (
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier), Qt.NoModifier),
@ -384,7 +388,7 @@ class HintManager(QObject):
Return: Return:
A QUrl with the absolute URL, or None. A QUrl with the absolute URL, or None.
""" """
text = elem.attribute('href') text = elem['href']
if not text: if not text:
return None return None
if baseurl is None: if baseurl is None:
@ -402,12 +406,14 @@ class HintManager(QObject):
webelem.SELECTORS[webelem.Group.links]) webelem.SELECTORS[webelem.Group.links])
rel_values = ('prev', 'previous') if prev else ('next') rel_values = ('prev', 'previous') if prev else ('next')
for e in elems: for e in elems:
if not e.hasAttribute('rel'): e = webelem.WebElementWrapper(e)
try:
rel_attr = e['rel']
except KeyError:
continue continue
rel_attr = e.attribute('rel')
if rel_attr in rel_values: if rel_attr in rel_values:
log.hints.debug("Found '{}' with rel={}".format( log.hints.debug("Found '{}' with rel={}".format(
webelem.debug_text(e), rel_attr)) e.debug_text(), rel_attr))
return e return e
# Then check for regular links/buttons. # Then check for regular links/buttons.
elems = frame.findAllElements( elems = frame.findAllElements(
@ -418,7 +424,8 @@ class HintManager(QObject):
for regex in config.get('hints', option): for regex in config.get('hints', option):
log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern)) log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern))
for e in elems: for e in elems:
text = e.toPlainText() e = webelem.WebElementWrapper(e)
text = str(e)
if not text: if not text:
continue continue
if regex.search(text): if regex.search(text):
@ -498,10 +505,11 @@ class HintManager(QObject):
ctx = HintContext() ctx = HintContext()
ctx.frames = webelem.get_child_frames(mainframe) ctx.frames = webelem.get_child_frames(mainframe)
for f in ctx.frames: 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) filterfunc = webelem.FILTERS.get(group, lambda e: True)
visible_elems = [e for e in elems if filterfunc(e) and visible_elems = [e for e in elems if filterfunc(e) and
webelem.is_visible(e, mainframe)] e.is_visible(mainframe)]
if not visible_elems: if not visible_elems:
raise cmdexc.CommandError("No elements found.") raise cmdexc.CommandError("No elements found.")
ctx.target = target ctx.target = target
@ -529,34 +537,34 @@ class HintManager(QObject):
rest = string[len(keystr):] rest = string[len(keystr):]
elems.label.setInnerXml('<font color="{}">{}</font>{}'.format( elems.label.setInnerXml('<font color="{}">{}</font>{}'.format(
config.get('colors', 'hints.fg.match'), matched, rest)) 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 # 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) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label['style'] = css
else: else:
# element doesn't match anymore -> hide it # 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) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label['style'] = css
def filter_hints(self, filterstr): def filter_hints(self, filterstr):
"""Filter displayed hints according to a text.""" """Filter displayed hints according to a text."""
for elems in self._context.elems.values(): for elems in self._context.elems.values():
if elems.elem.toPlainText().lower().startswith(filterstr): if str(elems.elem).lower().startswith(filterstr):
if elems.label.attribute('hidden') == 'true': if elems.label['hidden'] == 'true':
# hidden element which matches again -> unhide it # 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) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label['style'] = css
else: else:
# element doesn't match anymore -> hide it # 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) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label['style'] = css
visible = {} visible = {}
for k, e in self._context.elems.items(): for k, e in self._context.elems.items():
if e.label.attribute('hidden') != 'true': if e.label['hidden'] != 'true':
visible[k] = e visible[k] = e
if not visible: if not visible:
# Whoops, filtered all hints # Whoops, filtered all hints
@ -625,7 +633,7 @@ class HintManager(QObject):
log.hints.debug("Contents size changed...!") log.hints.debug("Contents size changed...!")
for elems in self._context.elems.values(): for elems in self._context.elems.values():
css = self._get_hint_css(elems.elem, elems.label) css = self._get_hint_css(elems.elem, elems.label)
elems.label.setAttribute('style', css) elems.label['style'] = css
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):

View File

@ -24,7 +24,6 @@
from unittest import mock from unittest import mock
from PyQt5.QtCore import QPoint, QProcess from PyQt5.QtCore import QPoint, QProcess
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtNetwork import QNetworkRequest from PyQt5.QtNetwork import QNetworkRequest
@ -78,84 +77,6 @@ class FakeKeyEvent:
self.modifiers = mock.Mock(return_value=modifiers) 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 '<fakeelem>'
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: class FakeWebFrame:
"""A stub for QWebFrame.""" """A stub for QWebFrame."""

View File

@ -17,16 +17,86 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for the webelement utils.""" """Tests for the webelement utils."""
import unittest import unittest
import unittest.mock
import collections.abc
from PyQt5.QtCore import QRect, QPoint from PyQt5.QtCore import QRect, QPoint
from PyQt5.QtWebKit import QWebElement
from qutebrowser.utils import webelem from qutebrowser.utils import webelem
from qutebrowser.test import stubs 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 = '<fakeelem/>'
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): class IsVisibleInvalidTests(unittest.TestCase):
"""Tests for is_visible with invalid elements. """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 geometry() and webFrame() should not be called, and ValueError should
be raised. be raised.
""" """
elem = stubs.FakeWebElement(null=True) elem = get_webelem()
with self.assertRaises(ValueError): elem._elem.isNull.return_value = True
webelem.is_visible(elem, self.frame) with self.assertRaises(webelem.IsNullError):
elem.isNull.assert_called_once_with() elem.is_visible(self.frame)
self.assertFalse(elem.geometry.called)
self.assertFalse(elem.webFrame.called)
def test_invalid_invisible(self): def test_invalid_invisible(self):
"""Test elements with an invalid geometry which are invisible.""" """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.assertFalse(elem.geometry().isValid())
self.assertEqual(elem.geometry().x(), 0) 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): def test_invalid_visible(self):
"""Test elements with an invalid geometry which are visible. """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 This seems to happen sometimes in the real world, with real elements
which *are* visible, but don't have a valid geometry. 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.assertFalse(elem.geometry().isValid())
self.assertTrue(webelem.is_visible(elem, self.frame)) self.assertTrue(elem.is_visible(self.frame))
class IsVisibleScrollTests(unittest.TestCase): class IsVisibleScrollTests(unittest.TestCase):
@ -83,13 +151,13 @@ class IsVisibleScrollTests(unittest.TestCase):
def test_invisible(self): def test_invisible(self):
"""Test elements which should be invisible due to scrolling.""" """Test elements which should be invisible due to scrolling."""
elem = stubs.FakeWebElement(QRect(5, 5, 4, 4), self.frame) elem = get_webelem(QRect(5, 5, 4, 4), self.frame)
self.assertFalse(webelem.is_visible(elem, self.frame)) self.assertFalse(elem.is_visible(self.frame))
def test_visible(self): def test_visible(self):
"""Test elements which still should be visible after scrolling.""" """Test elements which still should be visible after scrolling."""
elem = stubs.FakeWebElement(QRect(10, 10, 1, 1), self.frame) elem = get_webelem(QRect(10, 10, 1, 1), self.frame)
self.assertTrue(webelem.is_visible(elem, self.frame)) self.assertTrue(elem.is_visible(self.frame))
class IsVisibleCssTests(unittest.TestCase): class IsVisibleCssTests(unittest.TestCase):
@ -105,27 +173,25 @@ class IsVisibleCssTests(unittest.TestCase):
def test_visibility_visible(self): def test_visibility_visible(self):
"""Check that elements with "visibility = visible" are visible.""" """Check that elements with "visibility = visible" are visible."""
elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, elem = get_webelem(QRect(0, 0, 10, 10), self.frame,
visibility='visible') visibility='visible')
self.assertTrue(webelem.is_visible(elem, self.frame)) self.assertTrue(elem.is_visible(self.frame))
def test_visibility_hidden(self): def test_visibility_hidden(self):
"""Check that elements with "visibility = hidden" are not visible.""" """Check that elements with "visibility = hidden" are not visible."""
elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, elem = get_webelem(QRect(0, 0, 10, 10), self.frame,
visibility='hidden') visibility='hidden')
self.assertFalse(webelem.is_visible(elem, self.frame)) self.assertFalse(elem.is_visible(self.frame))
def test_display_inline(self): def test_display_inline(self):
"""Check that elements with "display = inline" are visible.""" """Check that elements with "display = inline" are visible."""
elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, elem = get_webelem(QRect(0, 0, 10, 10), self.frame, display='inline')
display='inline') self.assertTrue(elem.is_visible(self.frame))
self.assertTrue(webelem.is_visible(elem, self.frame))
def test_display_none(self): def test_display_none(self):
"""Check that elements with "display = none" are not visible.""" """Check that elements with "display = none" are not visible."""
elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, elem = get_webelem(QRect(0, 0, 10, 10), self.frame, display='none')
display='none') self.assertFalse(elem.is_visible(self.frame))
self.assertFalse(webelem.is_visible(elem, self.frame))
class IsVisibleIframeTests(unittest.TestCase): class IsVisibleIframeTests(unittest.TestCase):
@ -162,26 +228,26 @@ class IsVisibleIframeTests(unittest.TestCase):
self.frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300)) self.frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300))
self.iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), self.iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100),
parent=self.frame) parent=self.frame)
self.elem1 = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.iframe) self.elem1 = get_webelem(QRect(0, 0, 10, 10), self.iframe)
self.elem2 = stubs.FakeWebElement(QRect(20, 90, 10, 10), self.iframe) self.elem2 = get_webelem(QRect(20, 90, 10, 10), self.iframe)
self.elem3 = stubs.FakeWebElement(QRect(20, 150, 10, 10), self.iframe) self.elem3 = get_webelem(QRect(20, 150, 10, 10), self.iframe)
self.elem4 = stubs.FakeWebElement(QRect(30, 180, 10, 10), self.frame) self.elem4 = get_webelem(QRect(30, 180, 10, 10), self.frame)
def test_not_scrolled(self): def test_not_scrolled(self):
"""Test base situation.""" """Test base situation."""
self.assertTrue(self.frame.geometry().contains(self.iframe.geometry())) self.assertTrue(self.frame.geometry().contains(self.iframe.geometry()))
self.assertTrue(webelem.is_visible(self.elem1, self.frame)) self.assertTrue(self.elem1.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem2, self.frame)) self.assertTrue(self.elem2.is_visible(self.frame))
self.assertFalse(webelem.is_visible(self.elem3, self.frame)) self.assertFalse(self.elem3.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem4, self.frame)) self.assertTrue(self.elem4.is_visible(self.frame))
def test_iframe_scrolled(self): def test_iframe_scrolled(self):
"""Scroll iframe down so elem3 gets visible and elem1/elem2 not.""" """Scroll iframe down so elem3 gets visible and elem1/elem2 not."""
self.iframe.scrollPosition.return_value = QPoint(0, 100) self.iframe.scrollPosition.return_value = QPoint(0, 100)
self.assertFalse(webelem.is_visible(self.elem1, self.frame)) self.assertFalse(self.elem1.is_visible(self.frame))
self.assertFalse(webelem.is_visible(self.elem2, self.frame)) self.assertFalse(self.elem2.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem3, self.frame)) self.assertTrue(self.elem3.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem4, self.frame)) self.assertTrue(self.elem4.is_visible(self.frame))
def test_mainframe_scrolled_iframe_visible(self): def test_mainframe_scrolled_iframe_visible(self):
"""Scroll mainframe down so iframe is partly visible but elem1 not.""" """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()) geom = self.frame.geometry().translated(self.frame.scrollPosition())
self.assertFalse(geom.contains(self.iframe.geometry())) self.assertFalse(geom.contains(self.iframe.geometry()))
self.assertTrue(geom.intersects(self.iframe.geometry())) self.assertTrue(geom.intersects(self.iframe.geometry()))
self.assertFalse(webelem.is_visible(self.elem1, self.frame)) self.assertFalse(self.elem1.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem2, self.frame)) self.assertTrue(self.elem2.is_visible(self.frame))
self.assertFalse(webelem.is_visible(self.elem3, self.frame)) self.assertFalse(self.elem3.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem4, self.frame)) self.assertTrue(self.elem4.is_visible(self.frame))
def test_mainframe_scrolled_iframe_invisible(self): def test_mainframe_scrolled_iframe_invisible(self):
"""Scroll mainframe down so iframe is invisible.""" """Scroll mainframe down so iframe is invisible."""
@ -200,10 +266,10 @@ class IsVisibleIframeTests(unittest.TestCase):
geom = self.frame.geometry().translated(self.frame.scrollPosition()) geom = self.frame.geometry().translated(self.frame.scrollPosition())
self.assertFalse(geom.contains(self.iframe.geometry())) self.assertFalse(geom.contains(self.iframe.geometry()))
self.assertFalse(geom.intersects(self.iframe.geometry())) self.assertFalse(geom.intersects(self.iframe.geometry()))
self.assertFalse(webelem.is_visible(self.elem1, self.frame)) self.assertFalse(self.elem1.is_visible(self.frame))
self.assertFalse(webelem.is_visible(self.elem2, self.frame)) self.assertFalse(self.elem2.is_visible(self.frame))
self.assertFalse(webelem.is_visible(self.elem3, self.frame)) self.assertFalse(self.elem3.is_visible(self.frame))
self.assertTrue(webelem.is_visible(self.elem4, self.frame)) self.assertTrue(self.elem4.is_visible(self.frame))
class IsWritableTests(unittest.TestCase): class IsWritableTests(unittest.TestCase):
@ -212,18 +278,18 @@ class IsWritableTests(unittest.TestCase):
def test_writable(self): def test_writable(self):
"""Test a normal element.""" """Test a normal element."""
elem = stubs.FakeWebElement() elem = get_webelem()
self.assertTrue(webelem.is_writable(elem)) self.assertTrue(elem.is_writable())
def test_disabled(self): def test_disabled(self):
"""Test a disabled element.""" """Test a disabled element."""
elem = stubs.FakeWebElement(attributes=['disabled']) elem = get_webelem(attributes=['disabled'])
self.assertFalse(webelem.is_writable(elem)) self.assertFalse(elem.is_writable())
def test_readonly(self): def test_readonly(self):
"""Test a readonly element.""" """Test a readonly element."""
elem = stubs.FakeWebElement(attributes=['readonly']) elem = get_webelem(attributes=['readonly'])
self.assertFalse(webelem.is_writable(elem)) self.assertFalse(elem.is_writable())
class JavascriptEscapeTests(unittest.TestCase): class JavascriptEscapeTests(unittest.TestCase):
@ -309,204 +375,186 @@ class IsEditableTests(unittest.TestCase):
def test_input_plain(self): def test_input_plain(self):
"""Test with plain input element.""" """Test with plain input element."""
elem = stubs.FakeWebElement(tagname='input') elem = get_webelem(tagname='input')
self.assertTrue(webelem.is_editable(elem)) self.assertTrue(elem.is_editable())
def test_input_text(self): def test_input_text(self):
"""Test with text input element.""" """Test with text input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'text'})
attributes={'type': 'text'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_text_caps(self): def test_input_text_caps(self):
"""Test with text input element with caps attributes.""" """Test with text input element with caps attributes."""
elem = stubs.FakeWebElement(tagname='INPUT', elem = get_webelem(tagname='INPUT', attributes={'TYPE': 'TEXT'})
attributes={'TYPE': 'TEXT'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_email(self): def test_input_email(self):
"""Test with email input element.""" """Test with email input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'email'})
attributes={'type': 'email'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_url(self): def test_input_url(self):
"""Test with url input element.""" """Test with url input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'url'})
attributes={'type': 'url'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_tel(self): def test_input_tel(self):
"""Test with tel input element.""" """Test with tel input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'tel'})
attributes={'type': 'tel'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_number(self): def test_input_number(self):
"""Test with number input element.""" """Test with number input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'number'})
attributes={'type': 'number'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_password(self): def test_input_password(self):
"""Test with password input element.""" """Test with password input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'password'})
attributes={'type': 'password'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_search(self): def test_input_search(self):
"""Test with search input element.""" """Test with search input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'search'})
attributes={'type': 'search'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_input_button(self): def test_input_button(self):
"""Button should not be editable.""" """Button should not be editable."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'button'})
attributes={'type': 'button'}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_input_checkbox(self): def test_input_checkbox(self):
"""Checkbox should not be editable.""" """Checkbox should not be editable."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'type': 'checkbox'})
attributes={'type': 'checkbox'}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_textarea(self): def test_textarea(self):
"""Test textarea element.""" """Test textarea element."""
elem = stubs.FakeWebElement(tagname='textarea') elem = get_webelem(tagname='textarea')
self.assertTrue(webelem.is_editable(elem)) self.assertTrue(elem.is_editable())
def test_select(self): def test_select(self):
"""Test selectbox.""" """Test selectbox."""
elem = stubs.FakeWebElement(tagname='select') elem = get_webelem(tagname='select')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_input_disabled(self): def test_input_disabled(self):
"""Test disabled input element.""" """Test disabled input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'disabled': None})
attributes={'disabled': None}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_input_readonly(self): def test_input_readonly(self):
"""Test readonly input element.""" """Test readonly input element."""
elem = stubs.FakeWebElement(tagname='input', elem = get_webelem(tagname='input', attributes={'readonly': None})
attributes={'readonly': None}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_textarea_disabled(self): def test_textarea_disabled(self):
"""Test disabled textarea element.""" """Test disabled textarea element."""
elem = stubs.FakeWebElement(tagname='textarea', elem = get_webelem(tagname='textarea', attributes={'disabled': None})
attributes={'disabled': None}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_textarea_readonly(self): def test_textarea_readonly(self):
"""Test readonly textarea element.""" """Test readonly textarea element."""
elem = stubs.FakeWebElement(tagname='textarea', elem = get_webelem(tagname='textarea', attributes={'readonly': None})
attributes={'readonly': None}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_embed_true(self): def test_embed_true(self):
"""Test embed-element with insert-mode-on-plugins true.""" """Test embed-element with insert-mode-on-plugins true."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}}) {'insert-mode-on-plugins': True}})
elem = stubs.FakeWebElement(tagname='embed') elem = get_webelem(tagname='embed')
self.assertTrue(webelem.is_editable(elem)) self.assertTrue(elem.is_editable())
def test_applet_true(self): def test_applet_true(self):
"""Test applet-element with insert-mode-on-plugins true.""" """Test applet-element with insert-mode-on-plugins true."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}}) {'insert-mode-on-plugins': True}})
elem = stubs.FakeWebElement(tagname='applet') elem = get_webelem(tagname='applet')
self.assertTrue(webelem.is_editable(elem)) self.assertTrue(elem.is_editable())
def test_embed_false(self): def test_embed_false(self):
"""Test embed-element with insert-mode-on-plugins false.""" """Test embed-element with insert-mode-on-plugins false."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}}) {'insert-mode-on-plugins': False}})
elem = stubs.FakeWebElement(tagname='embed') elem = get_webelem(tagname='embed')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_applet_false(self): def test_applet_false(self):
"""Test applet-element with insert-mode-on-plugins false.""" """Test applet-element with insert-mode-on-plugins false."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}}) {'insert-mode-on-plugins': False}})
elem = stubs.FakeWebElement(tagname='applet') elem = get_webelem(tagname='applet')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_object_no_type(self): def test_object_no_type(self):
"""Test object-element without type.""" """Test object-element without type."""
elem = stubs.FakeWebElement(tagname='object') elem = get_webelem(tagname='object')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_object_image(self): def test_object_image(self):
"""Test object-element with image type.""" """Test object-element with image type."""
elem = stubs.FakeWebElement(tagname='object', elem = get_webelem(tagname='object', attributes={'type': 'image/gif'})
attributes={'type': 'image/gif'}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_object_application(self): def test_object_application(self):
"""Test object-element with application type.""" """Test object-element with application type."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}}) {'insert-mode-on-plugins': True}})
elem = stubs.FakeWebElement(tagname='object', elem = get_webelem(tagname='object',
attributes={'type': 'application/foo'}) attributes={'type': 'application/foo'})
self.assertTrue(webelem.is_editable(elem)) self.assertTrue(elem.is_editable())
def test_object_application_false(self): def test_object_application_false(self):
"""Test object-element with application type but not ...-on-plugins.""" """Test object-element with application type but not ...-on-plugins."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}}) {'insert-mode-on-plugins': False}})
elem = stubs.FakeWebElement(tagname='object', elem = get_webelem(tagname='object',
attributes={'type': 'application/foo'}) attributes={'type': 'application/foo'})
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_object_classid(self): def test_object_classid(self):
"""Test object-element with classid.""" """Test object-element with classid."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': True}}) {'insert-mode-on-plugins': True}})
elem = stubs.FakeWebElement(tagname='object', elem = get_webelem(tagname='object',
attributes={'type': 'foo', attributes={'type': 'foo', 'classid': 'foo'})
'classid': 'foo'}) self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
def test_object_classid_false(self): def test_object_classid_false(self):
"""Test object-element with classid but not insert-mode-on-plugins.""" """Test object-element with classid but not insert-mode-on-plugins."""
webelem.config = stubs.ConfigStub({'input': webelem.config = stubs.ConfigStub({'input':
{'insert-mode-on-plugins': False}}) {'insert-mode-on-plugins': False}})
elem = stubs.FakeWebElement(tagname='object', elem = get_webelem(tagname='object',
attributes={'type': 'foo', attributes={'type': 'foo', 'classid': 'foo'})
'classid': 'foo'}) self.assertFalse(elem.is_editable())
self.assertFalse(webelem.is_editable(elem))
def test_div_empty(self): def test_div_empty(self):
"""Test div-element without class.""" """Test div-element without class."""
elem = stubs.FakeWebElement(tagname='div') elem = get_webelem(tagname='div')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_div_noneditable(self): def test_div_noneditable(self):
"""Test div-element with non-editableclass.""" """Test div-element with non-editableclass."""
elem = stubs.FakeWebElement(tagname='div', classes='foo-kix-bar') elem = get_webelem(tagname='div', classes='foo-kix-bar')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_div_xik(self): def test_div_xik(self):
"""Test div-element with xik class.""" """Test div-element with xik class."""
elem = stubs.FakeWebElement(tagname='div', classes='foo kix-foo') elem = get_webelem(tagname='div', classes='foo kix-foo')
self.assertTrue(webelem.is_editable(elem)) self.assertTrue(elem.is_editable())
def test_div_xik_caps(self): def test_div_xik_caps(self):
"""Test div-element with xik class in caps. """Test div-element with xik class in caps.
This tests if classes are case sensitive as they should. This tests if classes are case sensitive as they should.
""" """
elem = stubs.FakeWebElement(tagname='div', classes='KIX-FOO') elem = get_webelem(tagname='div', classes='KIX-FOO')
self.assertFalse(webelem.is_editable(elem)) self.assertFalse(elem.is_editable())
def test_div_codemirror(self): def test_div_codemirror(self):
"""Test div-element with codemirror class.""" """Test div-element with codemirror class."""
elem = stubs.FakeWebElement(tagname='div', elem = get_webelem(tagname='div', classes='foo CodeMirror-foo')
classes='foo CodeMirror-foo') self.assertTrue(elem.is_editable())
self.assertTrue(webelem.is_editable(elem))
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -27,6 +27,9 @@ Module attributes:
without "href". without "href".
""" """
import collections.abc
import functools
from PyQt5.QtCore import QRect, QUrl from PyQt5.QtCore import QRect, QUrl
from PyQt5.QtWebKit import QWebElement from PyQt5.QtWebKit import QWebElement
@ -50,45 +53,141 @@ SELECTORS = {
} }
FILTERS = { FILTERS = {
Group.links: (lambda e: e.hasAttribute('href') and Group.links: (lambda e: 'href' in e and
QUrl(e.attribute('href')).scheme() != 'javascript'), QUrl(e['href']).scheme() != 'javascript'),
} }
def is_visible(elem, mainframe): class IsNullError(Exception):
"""Gets raised by WebElementWrapper if an element is null."""
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 "<WebElementWrapper '{}'>".format(self.debug_text())
except IsNullError:
return "<WebElementWrapper null>"
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. """Check whether the element is currently visible on the screen.
Args: Args:
elem: The QWebElement to check.
mainframe: The main QWebFrame. mainframe: The main QWebFrame.
Return: Return:
True if the element is visible, False otherwise. True if the element is visible, False otherwise.
""" """
self._check_vanished()
# CSS attributes which hide an element # CSS attributes which hide an element
hidden_attributes = { hidden_attributes = {
'visibility': 'hidden', 'visibility': 'hidden',
'display': 'none', 'display': 'none',
} }
if elem.isNull():
raise ValueError("Element is a null-element!")
for k, v in hidden_attributes.items(): for k, v in hidden_attributes.items():
if elem.styleProperty(k, QWebElement.ComputedStyle) == v: if self._elem.styleProperty(k, QWebElement.ComputedStyle) == v:
return False return False
if (not elem.geometry().isValid()) and elem.geometry().x() == 0: geometry = self._elem.geometry()
if not geometry.isValid() and geometry.x() == 0:
# Most likely an invisible link # Most likely an invisible link
return False return False
# First check if the element is visible on screen # First check if the element is visible on screen
elem_rect = rect_on_view(elem) elem_rect = self.rect_on_view()
if elem_rect.isValid(): if elem_rect.isValid():
visible_on_screen = mainframe.geometry().intersects(elem_rect) visible_on_screen = mainframe.geometry().intersects(elem_rect)
else: else:
# We got an invalid rectangle (width/height 0/0 probably), but this can # We got an invalid rectangle (width/height 0/0 probably), but this
# still be a valid link. # can still be a valid link.
visible_on_screen = mainframe.geometry().contains(elem_rect.topLeft()) visible_on_screen = mainframe.geometry().contains(
# Then check if it's visible in its frame if it's not in the main frame. elem_rect.topLeft())
elem_frame = elem.webFrame() # Then check if it's visible in its frame if it's not in the main
elem_rect = elem.geometry() # frame.
elem_frame = self._elem.webFrame()
elem_rect = self._elem.geometry()
framegeom = QRect(elem_frame.geometry()) framegeom = QRect(elem_frame.geometry())
if not framegeom.isValid(): if not framegeom.isValid():
visible_in_frame = False visible_in_frame = False
@ -98,32 +197,134 @@ def is_visible(elem, mainframe):
if elem_rect.isValid(): if elem_rect.isValid():
visible_in_frame = framegeom.intersects(elem_rect) visible_in_frame = framegeom.intersects(elem_rect)
else: else:
# We got an invalid rectangle (width/height 0/0 probably), but this # We got an invalid rectangle (width/height 0/0 probably), but
# can still be a valid link. # this can still be a valid link.
visible_in_frame = framegeom.contains(elem_rect.topLeft()) visible_in_frame = framegeom.contains(elem_rect.topLeft())
else: else:
visible_in_frame = visible_on_screen visible_in_frame = visible_on_screen
return all([visible_on_screen, visible_in_frame]) return all([visible_on_screen, visible_in_frame])
def rect_on_view(self):
def rect_on_view(elem):
"""Get the geometry of the element relative to the webview.""" """Get the geometry of the element relative to the webview."""
frame = elem.webFrame() self._check_vanished()
rect = QRect(elem.geometry()) frame = self._elem.webFrame()
rect = QRect(self._elem.geometry())
while frame is not None: while frame is not None:
rect.translate(frame.geometry().topLeft()) rect.translate(frame.geometry().topLeft())
rect.translate(frame.scrollPosition() * -1) rect.translate(frame.scrollPosition() * -1)
frame = frame.parentFrame() frame = frame.parentFrame()
return rect 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_writable(elem): def is_content_editable(self):
"""Check wheter an element is writable. """Check if an element has a contenteditable attribute.
FIXME: Add tests.
Args: Args:
elem: The QWebElement to check. elem: The QWebElement to check.
Return:
True if the element has a contenteditable attribute,
False otherwise.
""" """
return not (elem.hasAttribute('disabled') or elem.hasAttribute('readonly')) 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("<object> without type clicked...")
return False
objtype = self['type'].lower()
if objtype.startswith('application/') or 'classid' in self:
# Let's hope flash/java stuff has an application/* mimetype OR
# at least a classid attribute. Oh, and let's hope images/...
# DON'T have a classid attribute. HTML sucks.
log.webview.debug("<object type='{}'> clicked.".format(objtype))
return config.get('input', 'insert-mode-on-plugins')
else:
# Image/Audio/...
return False
def _is_editable_input(self):
"""Check if an input-element is editable.
Return:
True if the element is editable, False otherwise.
"""
try:
objtype = self['type'].lower()
except KeyError:
return self.is_writable()
else:
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
'search']:
return self.is_writable()
else:
return False
def _is_editable_div(self):
"""Check if a div-element is editable.
Return:
True if the element is editable, False otherwise.
"""
# Beginnings of div-classes which are actually some kind of editor.
div_classes = ('CodeMirror', # Javascript editor over a textarea
'kix-', # Google Docs editor
'ace_') # http://ace.c9.io/
for klass in self._elem.classes():
if any([klass.startswith(e) for e in div_classes]):
return True
def is_editable(self, strict=False):
"""Check whether we should switch to insert mode for this element.
FIXME: add tests
Args:
strict: Whether to do stricter checking so only fields where we can
get the value match, for use with the :editor command.
Return:
True if we should switch to insert mode, False otherwise.
"""
# pylint: disable=too-many-return-statements
self._check_vanished()
roles = ('combobox', 'textbox')
log.misc.debug("Checking if element is editable: {}".format(
self.debug_text()))
tag = self._elem.tagName().lower()
if self.is_content_editable() and self.is_writable():
return True
elif self.get('role', None) in roles:
return True
elif tag == 'input':
return self._is_editable_input()
elif tag == 'textarea':
return self.is_writable()
elif tag in ('embed', 'applet'):
# Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict
elif tag == 'object':
return self._is_editable_object() and not strict
elif tag == 'div':
return self._is_editable_div() and not strict
else:
return False
def debug_text(self):
"""Get a text based on an element suitable for debug output."""
self._check_vanished()
return utils.compact_text(repr(self._elem), 500)
def javascript_escape(text): def javascript_escape(text):
@ -168,108 +369,6 @@ def get_child_frames(startframe):
return results return results
def is_content_editable(elem):
"""Check if an element hsa a contenteditable attribute.
FIXME: Add tests.
Args:
elem: The QWebElement to check.
Return:
True if the element has a contenteditable attribute, False otherwise.
"""
return (elem.hasAttribute('contenteditable') and
elem.attribute('contenteditable') not in ('false', 'inherit'))
def _is_editable_object(elem):
"""Check if an object-element is editable."""
if not elem.hasAttribute('type'):
log.webview.debug("<object> without type clicked...")
return False
objtype = elem.attribute('type').lower()
if objtype.startswith('application/') or elem.hasAttribute('classid'):
# Let's hope flash/java stuff has an application/* mimetype OR
# at least a classid attribute. Oh, and let's hope images/...
# DON'T have a classid attribute. HTML sucks.
log.webview.debug("<object type='{}'> clicked.".format(objtype))
return config.get('input', 'insert-mode-on-plugins')
else:
# Image/Audio/...
return False
def _is_editable_input(elem):
"""Check if an input-element is editable.
Args:
elem: The QWebElement to check.
Return:
True if the element is editable, False otherwise.
"""
objtype = elem.attribute('type').lower()
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
'search', '']:
return is_writable(elem)
def _is_editable_div(elem):
"""Check if a div-element is editable.
Args:
elem: The QWebElement to check.
Return:
True if the element is editable, False otherwise.
"""
# Beginnings of div-classes which are actually some kind of editor.
div_classes = ('CodeMirror', # Javascript editor over a textarea
'kix-', # Google Docs editor
'ace_') # http://ace.c9.io/
for klass in elem.classes():
if any([klass.startswith(e) for e in div_classes]):
return True
def is_editable(elem, strict=False):
"""Check whether we should switch to insert mode for this element.
FIXME: add tests
Args:
elem: The QWebElement to check.
strict: Whether to do stricter checking so only fields where we can get
the value match, for use with the :editor command.
Return:
True if we should switch to insert mode, False otherwise.
"""
# pylint: disable=too-many-return-statements
roles = ('combobox', 'textbox')
log.misc.debug("Checking if element is editable: {}".format(
debug_text(elem)))
tag = elem.tagName().lower()
if is_content_editable(elem) and is_writable(elem):
return True
elif elem.hasAttribute('role') and elem.attribute('role') in roles:
return True
elif tag == 'input':
return _is_editable_input(elem)
elif tag == 'textarea':
return is_writable(elem)
elif tag in ('embed', 'applet'):
# Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict
elif tag == 'object':
return _is_editable_object(elem) and not strict
elif tag == 'div':
return _is_editable_div(elem) and not strict
else:
return False
def focus_elem(frame): def focus_elem(frame):
"""Get the focused element in a webframe. """Get the focused element in a webframe.
@ -278,9 +377,5 @@ def focus_elem(frame):
Args: Args:
frame: The QWebFrame to search in. frame: The QWebFrame to search in.
""" """
return frame.findFirstElement(SELECTORS[Group.focus]) elem = frame.findFirstElement(SELECTORS[Group.focus])
return WebElementWrapper(elem)
def debug_text(elem):
"""Get a text based on an element suitable for debug output."""
return utils.compact_text(elem.toOuterXml(), 500)

View File

@ -177,8 +177,9 @@ class WebView(QWebView):
log.mouse.debug("Hitresult is null!") log.mouse.debug("Hitresult is null!")
self._check_insertmode = True self._check_insertmode = True
return return
elem = hitresult.element() try:
if elem.isNull(): elem = webelem.WebElementWrapper(hitresult.element())
except webelem.IsNullError:
# For some reason, the hitresult element can be a null element # For some reason, the hitresult element can be a null element
# sometimes (e.g. when clicking the timetable fields on # sometimes (e.g. when clicking the timetable fields on
# http://www.sbb.ch/ ). If this is the case, we schedule a check # http://www.sbb.ch/ ). If this is the case, we schedule a check
@ -186,8 +187,8 @@ class WebView(QWebView):
log.mouse.debug("Hitresult element is null!") log.mouse.debug("Hitresult element is null!")
self._check_insertmode = True self._check_insertmode = True
return return
elif ((hitresult.isContentEditable() and webelem.is_writable(elem)) or if ((hitresult.isContentEditable() and elem.is_writable()) or
webelem.is_editable(elem)): elem.is_editable(elem)):
log.mouse.debug("Clicked editable element!") log.mouse.debug("Clicked editable element!")
modeman.maybe_enter(usertypes.KeyMode.insert, 'click') modeman.maybe_enter(usertypes.KeyMode.insert, 'click')
else: else:
@ -200,8 +201,12 @@ class WebView(QWebView):
if not self._check_insertmode: if not self._check_insertmode:
return return
self._check_insertmode = False self._check_insertmode = False
try:
elem = webelem.focus_elem(self.page().currentFrame()) elem = webelem.focus_elem(self.page().currentFrame())
if webelem.is_editable(elem): except webelem.IsNullError:
log.mouse.warning("Element vanished!")
return
if elem.is_editable():
log.mouse.debug("Clicked editable element (delayed)!") log.mouse.debug("Clicked editable element (delayed)!")
modeman.maybe_enter(usertypes.KeyMode.insert, 'click-delayed') modeman.maybe_enter(usertypes.KeyMode.insert, 'click-delayed')
else: else:
@ -353,11 +358,13 @@ class WebView(QWebView):
if modeman.instance().mode() == usertypes.KeyMode.insert or not ok: if modeman.instance().mode() == usertypes.KeyMode.insert or not ok:
return return
frame = self.page().currentFrame() frame = self.page().currentFrame()
elem = frame.findFirstElement(':focus') try:
log.modes.debug("focus element: {}".format(elem.toOuterXml())) elem = webelem.WebElementWrapper(frame.findFirstElement(':focus'))
if elem.isNull(): except webelem.IsNullError:
log.webview.debug("Focused element is null!") log.webview.debug("Focused element is null!")
elif webelem.is_editable(elem): return
log.modes.debug("focus element: {}".format(repr(elem)))
if elem.is_editable():
modeman.maybe_enter(usertypes.KeyMode.insert, 'load finished') modeman.maybe_enter(usertypes.KeyMode.insert, 'load finished')
@pyqtSlot(str) @pyqtSlot(str)