Repository: qutebrowser/qutebrowser

Issue:
Insert mode on twitter not triggered (content editable field)
**Version info**:

qutebrowser v1.10.1
Git commit: 
Backend: QtWebEngine (Chromium 73.0.3683.105)
Qt: 5.13.2

https://paste.the-compiler.org/view/430dbcd9
https://github.com/andys8/dotfiles/blob/master/qutebrowser/config.py

(I've the same behavior on a different machine with arch instead of ubuntu and newer qtwebengine)

**Does the bug happen if you start with `--temp-basedir`?**: Yes

**Description**

Some websites do not use input fields as they were meant to be used. Instead they mark html notes as content-editable.

```html
<div contenteditable="true">
  This text can be edited by the user.
</div>
```

https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content

This doesn't trigger insert mode in qutebrowser. It is used on some websites, for example twitter.com. For comparison: vimium in chrome is working fine with these kind of input fields.

![image](https://user-images.githubusercontent.com/13085980/79071356-918f6f80-7cdb-11ea-93c3-dfe232ebb772.png)


For me it's a large painpoint, because you can't trust qutebrowser. I click on a text field and start typing. It has the focus, but if qutebrowser didn't start the insert mode, the typed characters will be represented as normal mode keybindings, and changes are high you close the website or jump somewhere else accidentally.


**How to reproduce**

1. Open https://twitter.com
2. Login
3. Try to click into field to write a new tweet
4. Insert mode will not be triggered

![qutebrowser_twitter_bad](https://user-images.githubusercontent.com/13085980/79071042-ae2aa800-7cd9-11ea-8d22-6111304547ea.gif)

**Fix**

I tested #4722 with https://github.com/jgkamat/qutebrowser/tree/integration to verify it'll fix the behavior with twitter.com (because it's a large example). The insert mode indicator (magenta) is triggered. It's the case for other examples like the minimal one, too.

![qutebrowser_twitter_ok](https://user-images.githubusercontent.com/13085980/79071040-a965f400-7cd9-11ea-9e30-61074bcbc242.gif)

It would be awesome if you @The-Compiler could have another look at #4722 and consider merging it or solving the issue in another way. Thanks a lot!



Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py
--- a/qutebrowser/browser/webelem.py
+++ b/qutebrowser/browser/webelem.py
@@ -173,6 +173,10 @@ def is_content_editable(self) -> bool:
         except KeyError:
             return False
 
+    def is_content_editable_prop(self) -> bool:
+        """Get the value of this element's isContentEditable property."""
+        raise NotImplementedError
+
     def _is_editable_object(self) -> bool:
         """Check if an object-element is editable."""
         if 'type' not in self:
@@ -250,6 +254,9 @@ def is_editable(self, strict: bool = False) -> bool:
         elif tag in ['embed', 'applet']:
             # Flash/Java/...
             return config.val.input.insert_mode.plugins and not strict
+        elif (not strict and self.is_content_editable_prop() and
+              self.is_writable()):
+            return True
         elif tag == 'object':
             return self._is_editable_object() and not strict
         elif tag in ['div', 'pre']:
diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py
--- a/qutebrowser/browser/webengine/webengineelem.py
+++ b/qutebrowser/browser/webengine/webengineelem.py
@@ -54,6 +54,7 @@ def __init__(self, js_dict: typing.Dict[str, typing.Any],
             'class_name': str,
             'rects': list,
             'attributes': dict,
+            'is_content_editable': bool,
             'caret_position': (int, type(None)),
         }  # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
         assert set(js_dict.keys()).issubset(js_dict_types.keys())
@@ -78,6 +79,7 @@ def __init__(self, js_dict: typing.Dict[str, typing.Any],
                                     "{}".format(value, type(value), js_dict))
 
         self._id = js_dict['id']
+        self._is_content_editable = js_dict['is_content_editable']
         self._js_dict = js_dict
 
     def __str__(self) -> str:
@@ -137,6 +139,9 @@ def outer_xml(self) -> str:
         """Get the full HTML representation of this element."""
         return self._js_dict['outer_xml']
 
+    def is_content_editable_prop(self) -> bool:
+        return self._is_content_editable
+
     def value(self) -> webelem.JsValueType:
         return self._js_dict.get('value', None)
 
diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py
--- a/qutebrowser/browser/webkit/webkitelem.py
+++ b/qutebrowser/browser/webkit/webkitelem.py
@@ -116,6 +116,11 @@ def outer_xml(self) -> str:
         self._check_vanished()
         return self._elem.toOuterXml()
 
+    def is_content_editable_prop(self) -> bool:
+        val = self._elem.evaluateJavaScript('this.isContentEditable || false')
+        assert isinstance(val, bool)
+        return val
+
     def value(self) -> webelem.JsValueType:
         self._check_vanished()
         val = self._elem.evaluateJavaScript('this.value')


Test patch:
diff --git a/tests/end2end/data/hints/input.html b/tests/end2end/data/hints/input.html
--- a/tests/end2end/data/hints/input.html
+++ b/tests/end2end/data/hints/input.html
@@ -20,5 +20,7 @@
         <form><input type="text" style="padding-left: 20px;"></input></form>
         With existing text (logs to JS)::
         <form><input id="qute-input-existing" value="existing"></input></form>
+        Contenteditable attributes
+        <p contenteditable="true">laythe</p>
     </body>
 </html>
diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature
--- a/tests/end2end/features/hints.feature
+++ b/tests/end2end/features/hints.feature
@@ -570,6 +570,14 @@ Feature: Using hints
         And I run :leave-mode
         Then the javascript message "true" should be logged
 
+    Scenario: Hinting contenteditable inputs
+        When I open data/hints/input.html
+        And I hint with args "inputs" and follow f
+        And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
+        And I run :leave-mode
+        # The actual check is already done above
+        Then no crash should happen
+
     # Delete hint target
     Scenario: Deleting a simple target
         When I open data/hints/html/simple.html
diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py
--- a/tests/unit/browser/webkit/test_webkitelem.py
+++ b/tests/unit/browser/webkit/test_webkitelem.py
@@ -781,6 +781,13 @@ class TestIsEditable:
 
     """Tests for is_editable."""
 
+    @pytest.fixture(autouse=True)
+    def not_is_contenteditable_prop(self, mocker):
+        """Short-circut is_contenteditable_prop for TestIsEditable."""
+        with mocker.patch('qutebrowser.browser.webkit.webkitelem.WebKitElement'
+                          '.is_content_editable_prop', return_value=False):
+            yield
+
     @pytest.mark.parametrize('tagname, attributes, editable', [
         ('input', {}, True),
         ('input', {'type': 'text'}, True),