Repository: qutebrowser/qutebrowser

Issue:
Setting the default zoom != 100% doesn't work consistently with WebEngine
1. Set `ui default-zoom` to a value different than `100%` (I use 120%).
2. Browse the internet, open new tabs!
3. Sometimes the zoom will actually be 100%, and not the expected 120%!

It will happen reasonably reguarly. Maybe once every ~10 new tabs.

Also reported by someone else on IRC (using 140%). This far, I've only noticed this with WebEngine, not WebKit-ng.


Discussion:
I don't know of a good solution to this - currently QtWebEngine has its own default zoom handling, and qutebrowser just writes its zoom when loading a page is finished. Maybe the best thing to do here is to add a setting to Qt (i.e. in C++) to set a default zoom.
I've been sporadically experiencing this problem and I think I'm pretty close to figuring out exactly what is triggering it. 

First of all I've noticed that at any point if you navigate away from a page before it's finished loading then qutebrowser will have not had a chance to change the zoom level to the user defined default and anything done afterwards in that tab will be stuck at 100% zoom level. I'm guessing that qutebrowser remembers the zoom level based on the current zoom level of the tab whenever a new navigation action is triggered to navigate away from the current page. It's very easy to reproduce this problem by navigating away from a slow loading page (gmail is a great example) before the load has finished.

I've also noticed one other strange bit of behavior that may be a different side of the same problem, I'm not 100% sure. However the following scenario results in a 100% zoom level:

1. Load github.com in new tab
2. Load gmail.com (logged into mailbox)
3. Wait for page to load and zoom to adjust to user defined setting
3. Navigate back in history to github.com

You will notice in the above scenario that the zoom level is now stuck at 100% in the tab. However, if you were to open the github url using `:open` the zoom level would be fine. It's possible that this is a separate bug that only affects the history navigation.

I'm hopeful that now that I have a reasonable understanding of what's happening and the conditions that exhibit this loss of user-defined zoom, that I can look into a way to work around the problem, and at least mitigate it without needing to alter Qt.
One option, that's much more of a workaround than a fix, would be to add a configuration setting that forces a reset to the default zoom level anytime a page change happens. Something like `force-default-zoom: true`. This would effectively overwrite the zoom level for a tab anytime a new page is loaded (or is navigated to using `:back` or `:forward`).
Quick update ... I've been able to entirely eliminate the problem with the following change:

    modified   qutebrowser/browser/browsertab.py
    @@ -742,6 +742,7 @@ class AbstractTab(QWidget):
             if not self.title():
                 self.title_changed.emit(self.url().toDisplayString())
             self._handle_auto_insert_mode(ok)
    +        self.zoom.set_default()

This is basically calling `zoom.set_default()` at the tail end of `_on_load_finished` in `browsertab`. This is my best estimation of what a `force-default-zoom: true` would look like. 

I'll keep digging and see if I can improve the way that manually changed zoom levels are tracked vs. the default, and see if I can fix this without resorting to a workaround.


Resolving patch:
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -250,6 +250,7 @@ def __init__(self, win_id, parent=None):
         self._default_zoom_changed = False
         self._init_neighborlist()
         config.instance.changed.connect(self._on_config_changed)
+        self._zoom_factor = float(config.val.zoom.default) / 100
 
         # # FIXME:qtwebengine is this needed?
         # # For some reason, this signal doesn't get disconnected automatically
@@ -263,8 +264,7 @@ def _on_config_changed(self, option):
         if option in ['zoom.levels', 'zoom.default']:
             if not self._default_zoom_changed:
                 factor = float(config.val.zoom.default) / 100
-                self._set_factor_internal(factor)
-            self._default_zoom_changed = False
+                self.set_factor(factor)
             self._init_neighborlist()
 
     def _init_neighborlist(self):
@@ -301,15 +301,22 @@ def set_factor(self, factor, *, fuzzyval=True):
             self._neighborlist.fuzzyval = int(factor * 100)
         if factor < 0:
             raise ValueError("Can't zoom to factor {}!".format(factor))
-        self._default_zoom_changed = True
+
+        default_zoom_factor = float(config.val.zoom.default) / 100
+        self._default_zoom_changed = (factor != default_zoom_factor)
+
+        self._zoom_factor = factor
         self._set_factor_internal(factor)
 
     def factor(self):
-        raise NotImplementedError
+        return self._zoom_factor
 
     def set_default(self):
         self._set_factor_internal(float(config.val.zoom.default) / 100)
 
+    def set_current(self):
+        self._set_factor_internal(self._zoom_factor)
+
 
 class AbstractCaret(QObject):
 
@@ -754,6 +761,8 @@ def _on_load_finished(self, ok):
         if not self.title():
             self.title_changed.emit(self.url().toDisplayString())
 
+        self.zoom.set_current()
+
     @pyqtSlot()
     def _on_history_trigger(self):
         """Emit add_history_item when triggered by backend-specific signal."""
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -445,9 +445,6 @@ class WebEngineZoom(browsertab.AbstractZoom):
     def _set_factor_internal(self, factor):
         self._widget.setZoomFactor(factor)
 
-    def factor(self):
-        return self._widget.zoomFactor()
-
 
 class WebEngineElements(browsertab.AbstractElements):
 
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -378,9 +378,6 @@ class WebKitZoom(browsertab.AbstractZoom):
     def _set_factor_internal(self, factor):
         self._widget.setZoomFactor(factor)
 
-    def factor(self):
-        return self._widget.zoomFactor()
-
 
 class WebKitScroller(browsertab.AbstractScroller):
 


Test patch:
diff --git a/tests/end2end/data/long_load.html b/tests/end2end/data/long_load.html
new file mode 100644
--- /dev/null
+++ b/tests/end2end/data/long_load.html
@@ -0,0 +1,9 @@
+<script type="text/javascript">
+    var date = new Date();
+    var curDate = new Date();
+    while(curDate-date < 2000) {
+      curDate = new Date();
+    };
+
+    document.write("<html><h1>hello</h1></html>");
+</script>
diff --git a/tests/end2end/features/test_zoom_bdd.py b/tests/end2end/features/test_zoom_bdd.py
--- a/tests/end2end/features/test_zoom_bdd.py
+++ b/tests/end2end/features/test_zoom_bdd.py
@@ -24,5 +24,6 @@
 @bdd.then(bdd.parsers.parse("the zoom should be {zoom}%"))
 def check_zoom(quteproc, zoom):
     data = quteproc.get_session()
-    value = data['windows'][0]['tabs'][0]['history'][0]['zoom'] * 100
+    histories = data['windows'][0]['tabs'][0]['history']
+    value = next(h for h in histories if 'zoom' in h)['zoom'] * 100
     assert abs(value - float(zoom)) < 0.0001
diff --git a/tests/end2end/features/zoom.feature b/tests/end2end/features/zoom.feature
--- a/tests/end2end/features/zoom.feature
+++ b/tests/end2end/features/zoom.feature
@@ -81,6 +81,17 @@ Feature: Zooming in and out
         Then the message "Zoom level: 60%" should be shown
         And the zoom should be 60%
 
+    # https://github.com/qutebrowser/qutebrowser/issues/2507
+    # Using 127.0.0.1 because separate domain is required to reproduce
+    Scenario: Qutebrowser enforces correct zoom level
+        When I run :zoom 150%
+        And I open data/search.html
+        And I run :open http://127.0.0.1:(port)/data/long_load.html
+        And I wait until http://127.0.0.1:(port)/data/long_load.html is loaded
+        And I run :back
+        And I wait until data/search.html is loaded
+        Then the zoom should be 150%
+
     # Fixed in QtWebEngine branch
     @xfail
     Scenario: Zooming in with cloned tab
diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py
--- a/tests/end2end/fixtures/quteprocess.py
+++ b/tests/end2end/fixtures/quteprocess.py
@@ -411,14 +411,17 @@ def path_to_url(self, path, *, port=None, https=False):
         verbatim.
         """
         special_schemes = ['about:', 'qute:', 'chrome:', 'view-source:',
-                           'data:']
+                           'data:', 'http:', 'https:']
+        server = self.request.getfixturevalue('server')
+        server_port = server.port if port is None else port
+
         if any(path.startswith(scheme) for scheme in special_schemes):
+            path = path.replace('(port)', str(server_port))
             return path
         else:
-            server = self.request.getfixturevalue('server')
             return '{}://localhost:{}/{}'.format(
                 'https' if https else 'http',
-                server.port if port is None else port,
+                server_port,
                 path if path != '/' else '')
 
     def wait_for_js(self, message):
diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py
--- a/tests/end2end/fixtures/webserver_sub.py
+++ b/tests/end2end/fixtures/webserver_sub.py
@@ -73,7 +73,7 @@ def redirect_later():
     If delay is -1, wait until a request on redirect-later-continue is done.
     """
     global _redirect_later_event
-    delay = int(flask.request.args.get('delay', '1'))
+    delay = float(flask.request.args.get('delay', '1'))
     if delay == -1:
         _redirect_later_event = threading.Event()
         ok = _redirect_later_event.wait(timeout=30 * 1000)