Improve webelement API
This commit is contained in:
parent
becc4490bc
commit
b856bf3a47
@ -51,3 +51,6 @@ defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
[DESIGN]
|
||||
max-args=10
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-classes=WebElementWrapper
|
||||
|
2
doc/TODO
2
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
|
||||
=====================================
|
||||
|
@ -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))
|
||||
|
@ -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('<span class="qutehint" style="{}">{}</span>'.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('<font color="{}">{}</font>{}'.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):
|
||||
|
@ -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 '<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:
|
||||
|
||||
"""A stub for QWebFrame."""
|
||||
|
@ -17,16 +17,86 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# 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 = '<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):
|
||||
|
||||
"""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__':
|
||||
|
@ -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 "<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.
|
||||
|
||||
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("<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):
|
||||
@ -168,108 +369,6 @@ def get_child_frames(startframe):
|
||||
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):
|
||||
"""Get the focused element in a webframe.
|
||||
|
||||
@ -278,9 +377,5 @@ def focus_elem(frame):
|
||||
Args:
|
||||
frame: The QWebFrame to search in.
|
||||
"""
|
||||
return frame.findFirstElement(SELECTORS[Group.focus])
|
||||
|
||||
|
||||
def debug_text(elem):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
return utils.compact_text(elem.toOuterXml(), 500)
|
||||
elem = frame.findFirstElement(SELECTORS[Group.focus])
|
||||
return WebElementWrapper(elem)
|
||||
|
@ -177,8 +177,9 @@ class WebView(QWebView):
|
||||
log.mouse.debug("Hitresult is null!")
|
||||
self._check_insertmode = True
|
||||
return
|
||||
elem = hitresult.element()
|
||||
if elem.isNull():
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(hitresult.element())
|
||||
except webelem.IsNullError:
|
||||
# For some reason, the hitresult element can be a null element
|
||||
# sometimes (e.g. when clicking the timetable fields on
|
||||
# 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!")
|
||||
self._check_insertmode = True
|
||||
return
|
||||
elif ((hitresult.isContentEditable() and webelem.is_writable(elem)) or
|
||||
webelem.is_editable(elem)):
|
||||
if ((hitresult.isContentEditable() and elem.is_writable()) or
|
||||
elem.is_editable(elem)):
|
||||
log.mouse.debug("Clicked editable element!")
|
||||
modeman.maybe_enter(usertypes.KeyMode.insert, 'click')
|
||||
else:
|
||||
@ -200,8 +201,12 @@ class WebView(QWebView):
|
||||
if not self._check_insertmode:
|
||||
return
|
||||
self._check_insertmode = False
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
if webelem.is_editable(elem):
|
||||
try:
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
except webelem.IsNullError:
|
||||
log.mouse.warning("Element vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element (delayed)!")
|
||||
modeman.maybe_enter(usertypes.KeyMode.insert, 'click-delayed')
|
||||
else:
|
||||
@ -353,11 +358,13 @@ class WebView(QWebView):
|
||||
if modeman.instance().mode() == usertypes.KeyMode.insert or not ok:
|
||||
return
|
||||
frame = self.page().currentFrame()
|
||||
elem = frame.findFirstElement(':focus')
|
||||
log.modes.debug("focus element: {}".format(elem.toOuterXml()))
|
||||
if elem.isNull():
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(frame.findFirstElement(':focus'))
|
||||
except webelem.IsNullError:
|
||||
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')
|
||||
|
||||
@pyqtSlot(str)
|
||||
|
Loading…
Reference in New Issue
Block a user