Merge lp:~osomon/webbrowser-app/historyUpdateOnLoadCommitted into lp:webbrowser-app

Proposed by Olivier Tilloy
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 1029
Merged at revision: 1372
Proposed branch: lp:~osomon/webbrowser-app/historyUpdateOnLoadCommitted
Merge into: lp:webbrowser-app
Diff against target: 391 lines (+229/-40)
6 files modified
src/app/webbrowser/Browser.qml (+29/-2)
src/app/webbrowser/history-model.cpp (+37/-1)
src/app/webbrowser/history-model.h (+2/-1)
tests/autopilot/webbrowser_app/tests/http_server.py (+32/-1)
tests/autopilot/webbrowser_app/tests/test_history.py (+80/-34)
tests/unittests/history-model/tst_HistoryModelTests.cpp (+49/-1)
To merge this branch: bzr merge lp:~osomon/webbrowser-app/historyUpdateOnLoadCommitted
Reviewer Review Type Date Requested Status
Riccardo Padovani (community) Approve
PS Jenkins bot continuous-integration Needs Fixing
Ubuntu Phablet Team Pending
Review via email: mp+259361@code.launchpad.net

Commit message

Store entries in the history database on load committed, not load succeeded.
This ensures that content-initiated navigations are also stored.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Riccardo Padovani (rpadovani) wrote :

+ browser.historyModel.add(webviewInternal.storedUrl, webviewInternal.storedTitle, icon)

Why this and not

browser.historyModel.add(webviewInternal.storedUrl, title, icon)

?

I mean, we're updating the database, it's better to do it with last data we have, isn't it?

review: Needs Information
Revision history for this message
Olivier Tilloy (osomon) wrote :

> + browser.historyModel.add(webviewInternal.storedUrl,
> webviewInternal.storedTitle, icon)
>
> Why this and not
>
> browser.historyModel.add(webviewInternal.storedUrl, title, icon)
>
> ?
>
> I mean, we're updating the database, it's better to do it with last data we
> have, isn't it?

Some pages dynamically update the title to convey live information (like the ugly "scrolling title" effect, see e.g. http://htmlmarquee.com/title.html). In that case we don’t want to update the history DB everytime the title changes. And if the title has changed when we get an icon, we don’t want to update it either, because it’s most likely a dynamic change.
This is consistent with how chromium on desktop behaves.

Revision history for this message
Riccardo Padovani (rpadovani) wrote :

Looks good to me now :-)

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1021. By Olivier Tilloy

Merge the latest changes from trunk and resolve conflicts.

1022. By Olivier Tilloy

Add an update(…) method to HistoryModel, to allow updating the title and icon of an entry without incrementing the corresponding number of visits.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1023. By Olivier Tilloy

Simplify code a bit.
Do not prevent dynamic favicon updates from updating the history database, to be consistent with what chromium does.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1024. By Olivier Tilloy

Add some autopilot tests to verify that favicons are stored and updated in the history database.

1025. By Olivier Tilloy

Factor out common code.

1026. By Olivier Tilloy

Add autopilot tests to verify that a page’s title is stored in history, but not dynamically updated.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1027. By Olivier Tilloy

Add an autopilot test to verify that history.pushState() inserts a new entry in the history database.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Riccardo Padovani (rpadovani) wrote :

I only read the code and seems good to me, just a couple of very little improvements inline.

I'll test asap.

review: Needs Fixing
1028. By Olivier Tilloy

Swap the order in which conditions are evaluated, from cheapest to most expensive.

1029. By Olivier Tilloy

Move code out of else block to avoid compiler warning (control reaches end of non-void function [-Wreturn-type]).

Revision history for this message
Olivier Tilloy (osomon) wrote :

Thanks, those were very good points, I addressed them.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Riccardo Padovani (rpadovani) wrote :

Thanks! Tested a bit and seems to work like a charm.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/webbrowser/Browser.qml'
2--- src/app/webbrowser/Browser.qml 2016-02-23 11:24:33 +0000
3+++ src/app/webbrowser/Browser.qml 2016-03-01 09:31:14 +0000
4@@ -1263,20 +1263,47 @@
5 }
6 }
7
8+ QtObject {
9+ id: webviewInternal
10+ property url storedUrl: ""
11+ property bool titleSet: false
12+ property string title: ""
13+ }
14 onLoadEvent: {
15 if (event.type == Oxide.LoadEvent.TypeCommitted) {
16 chrome.findInPageMode = false
17+ webviewInternal.titleSet = false
18+ webviewInternal.title = title
19 }
20
21 if (webviewimpl.incognito) {
22 return
23 }
24
25- if (event.type == Oxide.LoadEvent.TypeSucceeded &&
26- 300 > event.httpStatusCode && event.httpStatusCode >= 200) {
27+ if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
28+ !event.isError &&
29+ (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
30+ webviewInternal.storedUrl = event.url
31 HistoryModel.add(event.url, title, icon)
32 }
33 }
34+ onTitleChanged: {
35+ if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
36+ // Record the title to avoid updating the history database
37+ // every time the page dynamically updates its title.
38+ // We don’t want pages that update their title every second
39+ // to achieve an ugly "scrolling title" effect to flood the
40+ // history database with updates.
41+ webviewInternal.titleSet = true
42+ webviewInternal.title = title
43+ HistoryModel.update(webviewInternal.storedUrl, title, icon)
44+ }
45+ }
46+ onIconChanged: {
47+ if (webviewInternal.storedUrl.toString()) {
48+ HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
49+ }
50+ }
51
52 onGeolocationPermissionRequested: requestGeolocationPermission(request)
53
54
55=== modified file 'src/app/webbrowser/history-model.cpp'
56--- src/app/webbrowser/history-model.cpp 2015-10-07 11:10:14 +0000
57+++ src/app/webbrowser/history-model.cpp 2016-03-01 09:31:14 +0000
58@@ -1,5 +1,5 @@
59 /*
60- * Copyright 2013-2015 Canonical Ltd.
61+ * Copyright 2013-2016 Canonical Ltd.
62 *
63 * This file is part of webbrowser-app.
64 *
65@@ -301,6 +301,42 @@
66 }
67
68 /*!
69+ Update an existing entry in the model.
70+
71+ If no entry with the same URL exists yet, do nothing (and return false).
72+ Otherwise the title and icon of the existing entry are updated (the number
73+ of visits remains unchanged).
74+
75+ Return true if an update actually happened, false otherwise.
76+*/
77+bool HistoryModel::update(const QUrl& url, const QString& title, const QUrl& icon)
78+{
79+ if (url.isEmpty()) {
80+ return false;
81+ }
82+ int index = getEntryIndex(url);
83+ if (index == -1) {
84+ return false;
85+ }
86+ QVector<int> roles;
87+ const HistoryEntry& entry = m_entries.at(index);
88+ if (title != entry.title) {
89+ m_entries[index].title = title;
90+ roles << Title;
91+ }
92+ if (icon != entry.icon) {
93+ m_entries[index].icon = icon;
94+ roles << Icon;
95+ }
96+ if (roles.isEmpty()) {
97+ return false;
98+ }
99+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), roles);
100+ updateExistingEntryInDatabase(entry);
101+ return true;
102+}
103+
104+/*!
105 Remove a given URL from the history model.
106
107 If the URL was not previously visited, do nothing.
108
109=== modified file 'src/app/webbrowser/history-model.h'
110--- src/app/webbrowser/history-model.h 2015-10-07 11:10:14 +0000
111+++ src/app/webbrowser/history-model.h 2016-03-01 09:31:14 +0000
112@@ -1,5 +1,5 @@
113 /*
114- * Copyright 2013-2015 Canonical Ltd.
115+ * Copyright 2013-2016 Canonical Ltd.
116 *
117 * This file is part of webbrowser-app.
118 *
119@@ -62,6 +62,7 @@
120 void setDatabasePath(const QString& path);
121
122 Q_INVOKABLE int add(const QUrl& url, const QString& title, const QUrl& icon);
123+ Q_INVOKABLE bool update(const QUrl& url, const QString& title, const QUrl& icon);
124 Q_INVOKABLE void removeEntryByUrl(const QUrl& url);
125 Q_INVOKABLE void removeEntriesByDate(const QDate& date);
126 Q_INVOKABLE void removeEntriesByDomain(const QString& domain);
127
128=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
129--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-10-26 11:09:25 +0000
130+++ tests/autopilot/webbrowser_app/tests/http_server.py 2016-03-01 09:31:14 +0000
131@@ -1,6 +1,6 @@
132 # -*- coding: utf-8 -*-
133 #
134-# Copyright 2013-2015 Canonical
135+# Copyright 2013-2016 Canonical
136 #
137 # This program is free software: you can redistribute it and/or modify it
138 # under the terms of the GNU General Public License version 3, as published
139@@ -215,6 +215,37 @@
140 "}, function() { location.href = '/test1' } " +
141 ", function() { location.href = '/test2' })</script>"
142 )
143+ elif self.path == "/favicon":
144+ self.send_response(200)
145+ html = '<html><head><link rel="icon" type="image/png" '
146+ html += 'href="/assets/icon1.png"></head>'
147+ html += '<body>favicon</body></html>'
148+ self.send_html(html)
149+ elif self.path == "/changingfavicon":
150+ self.send_response(200)
151+ html = '<html><head><link id="favicon" rel="shortcut icon" '
152+ html += 'type="image/png" href="icon0.png"></head><body><script>'
153+ html += 'var i = 0; window.setInterval(function() {'
154+ html += 'document.getElementById("favicon").href = ++i + '
155+ html += '".png"; }, 1000);</script></body></html>'
156+ self.send_html(html)
157+ elif self.path == "/changingtitle":
158+ self.send_response(200)
159+ html = '<html><head><title>title0</title></head><body><script>'
160+ html += 'var i = 0; window.setInterval(function() { '
161+ html += 'document.title = "title" + ++i; }, 500);</script></body>'
162+ html += '</html>'
163+ self.send_html(html)
164+ elif self.path == "/pushstate":
165+ self.send_response(200)
166+ html = '<html><head><title>push state</title></head>'
167+ html += '<body style="margin: 0"><a id="link">'
168+ html += '<div style="height: 100%"></div></a><script>'
169+ html += 'document.getElementById("link").addEventListener("click",'
170+ html += ' function(e) { document.title = "state pushed"; '
171+ html += 'history.pushState(null, null, "/statepushed"); });'
172+ html += '</script></body></html>'
173+ self.send_html(html)
174 else:
175 self.send_error(404)
176
177
178=== modified file 'tests/autopilot/webbrowser_app/tests/test_history.py'
179--- tests/autopilot/webbrowser_app/tests/test_history.py 2015-11-23 10:25:26 +0000
180+++ tests/autopilot/webbrowser_app/tests/test_history.py 2016-03-01 09:31:14 +0000
181@@ -1,6 +1,6 @@
182 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
183 #
184-# Copyright 2015 Canonical
185+# Copyright 2015-2016 Canonical
186 #
187 # This program is free software: you can redistribute it and/or modify it
188 # under the terms of the GNU General Public License version 3, as published
189@@ -16,7 +16,7 @@
190
191 import time
192
193-from testtools.matchers import Equals
194+from testtools.matchers import EndsWith, Equals, StartsWith
195 from autopilot.matchers import Eventually
196
197 from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
198@@ -24,45 +24,36 @@
199
200 class TestHistory(StartOpenRemotePageTestCaseBase):
201
202- def test_history_not_save_404(self):
203- url = self.base_url + "/404page"
204- self.main_window.go_to_url(url)
205- self.main_window.wait_until_page_loaded(url)
206-
207- # A valid url to be sure the fact the 404 page isn't present in the
208- # history view isn't a timing issue.
209- url = self.base_url + "/test2"
210- self.main_window.go_to_url(url)
211- self.main_window.wait_until_page_loaded(url)
212-
213- history = self.open_history()
214-
215- # We have domains with subsections only on mobiles.
216- # On wide we take all the entries directly
217+ def expect_history_entries(self, ordered_urls):
218+ history = self.main_window.get_history_view()
219 if self.main_window.wide:
220- delegates = history.get_entries()
221-
222- # 2 addresses: /test1 and /test2
223 self.assertThat(lambda: len(history.get_entries()),
224- Eventually(Equals(2)))
225- self.assertThat(sorted([delegate.url for delegate in delegates]),
226- Equals(sorted([self.url, url])))
227+ Eventually(Equals(len(ordered_urls))))
228+ entries = history.get_entries()
229 else:
230- # 1 domain: the local one
231- domain_entries = history.get_domain_entries()
232 self.assertThat(lambda: len(history.get_domain_entries()),
233 Eventually(Equals(1)))
234-
235- self.pointing_device.click_object(domain_entries[0])
236+ self.pointing_device.click_object(history.get_domain_entries()[0])
237 expanded_history = self.main_window.get_expanded_history_view()
238-
239- # 2 addresses: /test1 and /test2
240 self.assertThat(lambda: len(expanded_history.get_entries()),
241- Eventually(Equals(2)))
242-
243- delegates = expanded_history.get_entries()
244- self.assertThat(sorted([delegate.url for delegate in delegates]),
245- Equals(sorted([self.url, url])))
246+ Eventually(Equals(len(ordered_urls))))
247+ entries = expanded_history.get_entries()
248+ self.assertThat([entry.url for entry in entries], Equals(ordered_urls))
249+ return entries
250+
251+ def test_404_not_saved(self):
252+ url = self.base_url + "/notfound"
253+ self.main_window.go_to_url(url)
254+ self.main_window.wait_until_page_loaded(url)
255+
256+ # A valid url to be sure the fact the 404 page isn't present in the
257+ # history view isn't a timing issue.
258+ url = self.base_url + "/test2"
259+ self.main_window.go_to_url(url)
260+ self.main_window.wait_until_page_loaded(url)
261+
262+ self.open_history()
263+ self.expect_history_entries([url, self.url])
264
265 def test_expanded_history_view_header_swallows_clicks(self):
266 # Regression test for https://launchpad.net/bugs/1518904
267@@ -78,3 +69,58 @@
268 # There should be only one instance on the expanded history view.
269 # If there’s more, the following call will raise an exception.
270 self.main_window.get_expanded_history_view()
271+
272+ def test_favicon_saved(self):
273+ # Regression test for https://launchpad.net/bugs/1549780
274+ url = self.base_url + "/favicon"
275+ self.main_window.go_to_url(url)
276+ self.main_window.wait_until_page_loaded(url)
277+ self.open_history()
278+ first = self.expect_history_entries([url, self.url])[0]
279+ self.assertThat(first.url, Equals(url))
280+ favicon = self.base_url + "/assets/icon1.png"
281+ self.assertThat(first.icon, Equals(favicon))
282+
283+ def test_favicon_updated(self):
284+ # Verify that a page dynamically updating its favicon
285+ # triggers an update in the history database.
286+ url = self.base_url + "/changingfavicon"
287+ self.main_window.go_to_url(url)
288+ self.main_window.wait_until_page_loaded(url)
289+ self.open_history()
290+ first = self.expect_history_entries([url, self.url])[0]
291+ indexes = set()
292+ while len(indexes) < 3:
293+ self.assertThat(first.url, Equals(url))
294+ icon = first.icon
295+ self.assertThat(icon, StartsWith(self.base_url))
296+ self.assertThat(icon, EndsWith(".png"))
297+ indexes.add(int(first.icon[(len(self.base_url)+1):-4]))
298+
299+ def test_title_saved(self):
300+ self.open_history()
301+ entry = self.expect_history_entries([self.url])[0]
302+ self.assertThat(entry.title, Equals("test page 1"))
303+
304+ def test_title_not_updated(self):
305+ # Verify that a page dynamically updating its title
306+ # does NOT trigger an update in the history database.
307+ url = self.base_url + "/changingtitle"
308+ self.main_window.go_to_url(url)
309+ self.main_window.wait_until_page_loaded(url)
310+ self.open_history()
311+ first = self.expect_history_entries([url, self.url])[0]
312+ for i in range(10):
313+ self.assertThat(first.title, Equals("title0"))
314+ time.sleep(0.5)
315+
316+ def test_pushstate_updates_history(self):
317+ url = self.base_url + "/pushstate"
318+ self.main_window.go_to_url(url)
319+ self.main_window.wait_until_page_loaded(url)
320+ webview = self.main_window.get_current_webview()
321+ self.pointing_device.click_object(webview)
322+ pushed = self.base_url + "/statepushed"
323+ self.main_window.wait_until_page_loaded(pushed)
324+ self.open_history()
325+ self.expect_history_entries([pushed, url, self.url])
326
327=== modified file 'tests/unittests/history-model/tst_HistoryModelTests.cpp'
328--- tests/unittests/history-model/tst_HistoryModelTests.cpp 2015-10-07 11:10:14 +0000
329+++ tests/unittests/history-model/tst_HistoryModelTests.cpp 2016-03-01 09:31:14 +0000
330@@ -1,5 +1,5 @@
331 /*
332- * Copyright 2013 Canonical Ltd.
333+ * Copyright 2013-2016 Canonical Ltd.
334 *
335 * This file is part of webbrowser-app.
336 *
337@@ -136,6 +136,54 @@
338 QVERIFY(roles.contains(HistoryModel::Visits));
339 }
340
341+ void shouldUpdateAttributes()
342+ {
343+ qRegisterMetaType<QVector<int> >();
344+ QSignalSpy spyChanged(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
345+ QUrl url(QStringLiteral("http://example.org/"));
346+ QCOMPARE(model->add(url, QStringLiteral(""), QUrl()), 1);
347+
348+ QVERIFY(!model->update(QUrl(), QStringLiteral(""), QUrl()));
349+ QVERIFY(spyChanged.isEmpty());
350+
351+ QVERIFY(!model->update(QUrl(QStringLiteral("http://example.com/")), QStringLiteral(""), QUrl()));
352+ QVERIFY(spyChanged.isEmpty());
353+
354+ QVERIFY(!model->update(url, QStringLiteral(""), QUrl()));
355+ QVERIFY(spyChanged.isEmpty());
356+
357+ QVERIFY(model->update(url, QStringLiteral("title update"), QUrl()));
358+ QCOMPARE(spyChanged.count(), 1);
359+ QList<QVariant> args = spyChanged.takeFirst();
360+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
361+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
362+ QVector<int> roles = args.at(2).value<QVector<int> >();
363+ QCOMPARE(roles.size(), 1);
364+ QVERIFY(roles.contains(HistoryModel::Title));
365+ QCOMPARE(model->get(0)[QStringLiteral("visits")].toInt(), 1);
366+
367+ QVERIFY(model->update(url, QStringLiteral("title update"), QUrl(QStringLiteral("image://webicon/123"))));
368+ QCOMPARE(spyChanged.count(), 1);
369+ args = spyChanged.takeFirst();
370+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
371+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
372+ roles = args.at(2).value<QVector<int> >();
373+ QCOMPARE(roles.size(), 1);
374+ QVERIFY(roles.contains(HistoryModel::Icon));
375+ QCOMPARE(model->get(0)[QStringLiteral("visits")].toInt(), 1);
376+
377+ QVERIFY(model->update(url, QStringLiteral("title update 2"), QUrl(QStringLiteral("image://webicon/1234"))));
378+ QCOMPARE(spyChanged.count(), 1);
379+ args = spyChanged.takeFirst();
380+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
381+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
382+ roles = args.at(2).value<QVector<int> >();
383+ QCOMPARE(roles.size(), 2);
384+ QVERIFY(roles.contains(HistoryModel::Title));
385+ QVERIFY(roles.contains(HistoryModel::Icon));
386+ QCOMPARE(model->get(0)[QStringLiteral("visits")].toInt(), 1);
387+ }
388+
389 void shouldNotifyWhenHidingOrUnHidingExistingEntry()
390 {
391 qRegisterMetaType<QVector<int> >();

Subscribers

People subscribed via source and target branches

to status/vote changes: