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
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2016-02-23 11:24:33 +0000
+++ src/app/webbrowser/Browser.qml 2016-03-01 09:31:14 +0000
@@ -1263,20 +1263,47 @@
1263 }1263 }
1264 }1264 }
12651265
1266 QtObject {
1267 id: webviewInternal
1268 property url storedUrl: ""
1269 property bool titleSet: false
1270 property string title: ""
1271 }
1266 onLoadEvent: {1272 onLoadEvent: {
1267 if (event.type == Oxide.LoadEvent.TypeCommitted) {1273 if (event.type == Oxide.LoadEvent.TypeCommitted) {
1268 chrome.findInPageMode = false1274 chrome.findInPageMode = false
1275 webviewInternal.titleSet = false
1276 webviewInternal.title = title
1269 }1277 }
12701278
1271 if (webviewimpl.incognito) {1279 if (webviewimpl.incognito) {
1272 return1280 return
1273 }1281 }
12741282
1275 if (event.type == Oxide.LoadEvent.TypeSucceeded &&1283 if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
1276 300 > event.httpStatusCode && event.httpStatusCode >= 200) {1284 !event.isError &&
1285 (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
1286 webviewInternal.storedUrl = event.url
1277 HistoryModel.add(event.url, title, icon)1287 HistoryModel.add(event.url, title, icon)
1278 }1288 }
1279 }1289 }
1290 onTitleChanged: {
1291 if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
1292 // Record the title to avoid updating the history database
1293 // every time the page dynamically updates its title.
1294 // We don’t want pages that update their title every second
1295 // to achieve an ugly "scrolling title" effect to flood the
1296 // history database with updates.
1297 webviewInternal.titleSet = true
1298 webviewInternal.title = title
1299 HistoryModel.update(webviewInternal.storedUrl, title, icon)
1300 }
1301 }
1302 onIconChanged: {
1303 if (webviewInternal.storedUrl.toString()) {
1304 HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
1305 }
1306 }
12801307
1281 onGeolocationPermissionRequested: requestGeolocationPermission(request)1308 onGeolocationPermissionRequested: requestGeolocationPermission(request)
12821309
12831310
=== modified file 'src/app/webbrowser/history-model.cpp'
--- src/app/webbrowser/history-model.cpp 2015-10-07 11:10:14 +0000
+++ src/app/webbrowser/history-model.cpp 2016-03-01 09:31:14 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright 2013-2015 Canonical Ltd.2 * Copyright 2013-2016 Canonical Ltd.
3 *3 *
4 * This file is part of webbrowser-app.4 * This file is part of webbrowser-app.
5 *5 *
@@ -301,6 +301,42 @@
301}301}
302302
303/*!303/*!
304 Update an existing entry in the model.
305
306 If no entry with the same URL exists yet, do nothing (and return false).
307 Otherwise the title and icon of the existing entry are updated (the number
308 of visits remains unchanged).
309
310 Return true if an update actually happened, false otherwise.
311*/
312bool HistoryModel::update(const QUrl& url, const QString& title, const QUrl& icon)
313{
314 if (url.isEmpty()) {
315 return false;
316 }
317 int index = getEntryIndex(url);
318 if (index == -1) {
319 return false;
320 }
321 QVector<int> roles;
322 const HistoryEntry& entry = m_entries.at(index);
323 if (title != entry.title) {
324 m_entries[index].title = title;
325 roles << Title;
326 }
327 if (icon != entry.icon) {
328 m_entries[index].icon = icon;
329 roles << Icon;
330 }
331 if (roles.isEmpty()) {
332 return false;
333 }
334 Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), roles);
335 updateExistingEntryInDatabase(entry);
336 return true;
337}
338
339/*!
304 Remove a given URL from the history model.340 Remove a given URL from the history model.
305341
306 If the URL was not previously visited, do nothing.342 If the URL was not previously visited, do nothing.
307343
=== modified file 'src/app/webbrowser/history-model.h'
--- src/app/webbrowser/history-model.h 2015-10-07 11:10:14 +0000
+++ src/app/webbrowser/history-model.h 2016-03-01 09:31:14 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright 2013-2015 Canonical Ltd.2 * Copyright 2013-2016 Canonical Ltd.
3 *3 *
4 * This file is part of webbrowser-app.4 * This file is part of webbrowser-app.
5 *5 *
@@ -62,6 +62,7 @@
62 void setDatabasePath(const QString& path);62 void setDatabasePath(const QString& path);
6363
64 Q_INVOKABLE int add(const QUrl& url, const QString& title, const QUrl& icon);64 Q_INVOKABLE int add(const QUrl& url, const QString& title, const QUrl& icon);
65 Q_INVOKABLE bool update(const QUrl& url, const QString& title, const QUrl& icon);
65 Q_INVOKABLE void removeEntryByUrl(const QUrl& url);66 Q_INVOKABLE void removeEntryByUrl(const QUrl& url);
66 Q_INVOKABLE void removeEntriesByDate(const QDate& date);67 Q_INVOKABLE void removeEntriesByDate(const QDate& date);
67 Q_INVOKABLE void removeEntriesByDomain(const QString& domain);68 Q_INVOKABLE void removeEntriesByDomain(const QString& domain);
6869
=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-10-26 11:09:25 +0000
+++ tests/autopilot/webbrowser_app/tests/http_server.py 2016-03-01 09:31:14 +0000
@@ -1,6 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2#2#
3# Copyright 2013-2015 Canonical3# Copyright 2013-2016 Canonical
4#4#
5# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -215,6 +215,37 @@
215 "}, function() { location.href = '/test1' } " +215 "}, function() { location.href = '/test1' } " +
216 ", function() { location.href = '/test2' })</script>"216 ", function() { location.href = '/test2' })</script>"
217 )217 )
218 elif self.path == "/favicon":
219 self.send_response(200)
220 html = '<html><head><link rel="icon" type="image/png" '
221 html += 'href="/assets/icon1.png"></head>'
222 html += '<body>favicon</body></html>'
223 self.send_html(html)
224 elif self.path == "/changingfavicon":
225 self.send_response(200)
226 html = '<html><head><link id="favicon" rel="shortcut icon" '
227 html += 'type="image/png" href="icon0.png"></head><body><script>'
228 html += 'var i = 0; window.setInterval(function() {'
229 html += 'document.getElementById("favicon").href = ++i + '
230 html += '".png"; }, 1000);</script></body></html>'
231 self.send_html(html)
232 elif self.path == "/changingtitle":
233 self.send_response(200)
234 html = '<html><head><title>title0</title></head><body><script>'
235 html += 'var i = 0; window.setInterval(function() { '
236 html += 'document.title = "title" + ++i; }, 500);</script></body>'
237 html += '</html>'
238 self.send_html(html)
239 elif self.path == "/pushstate":
240 self.send_response(200)
241 html = '<html><head><title>push state</title></head>'
242 html += '<body style="margin: 0"><a id="link">'
243 html += '<div style="height: 100%"></div></a><script>'
244 html += 'document.getElementById("link").addEventListener("click",'
245 html += ' function(e) { document.title = "state pushed"; '
246 html += 'history.pushState(null, null, "/statepushed"); });'
247 html += '</script></body></html>'
248 self.send_html(html)
218 else:249 else:
219 self.send_error(404)250 self.send_error(404)
220251
221252
=== modified file 'tests/autopilot/webbrowser_app/tests/test_history.py'
--- tests/autopilot/webbrowser_app/tests/test_history.py 2015-11-23 10:25:26 +0000
+++ tests/autopilot/webbrowser_app/tests/test_history.py 2016-03-01 09:31:14 +0000
@@ -1,6 +1,6 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Copyright 2015 Canonical3# Copyright 2015-2016 Canonical
4#4#
5# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -16,7 +16,7 @@
1616
17import time17import time
1818
19from testtools.matchers import Equals19from testtools.matchers import EndsWith, Equals, StartsWith
20from autopilot.matchers import Eventually20from autopilot.matchers import Eventually
2121
22from webbrowser_app.tests import StartOpenRemotePageTestCaseBase22from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
@@ -24,45 +24,36 @@
2424
25class TestHistory(StartOpenRemotePageTestCaseBase):25class TestHistory(StartOpenRemotePageTestCaseBase):
2626
27 def test_history_not_save_404(self):27 def expect_history_entries(self, ordered_urls):
28 url = self.base_url + "/404page"28 history = self.main_window.get_history_view()
29 self.main_window.go_to_url(url)
30 self.main_window.wait_until_page_loaded(url)
31
32 # A valid url to be sure the fact the 404 page isn't present in the
33 # history view isn't a timing issue.
34 url = self.base_url + "/test2"
35 self.main_window.go_to_url(url)
36 self.main_window.wait_until_page_loaded(url)
37
38 history = self.open_history()
39
40 # We have domains with subsections only on mobiles.
41 # On wide we take all the entries directly
42 if self.main_window.wide:29 if self.main_window.wide:
43 delegates = history.get_entries()
44
45 # 2 addresses: /test1 and /test2
46 self.assertThat(lambda: len(history.get_entries()),30 self.assertThat(lambda: len(history.get_entries()),
47 Eventually(Equals(2)))31 Eventually(Equals(len(ordered_urls))))
48 self.assertThat(sorted([delegate.url for delegate in delegates]),32 entries = history.get_entries()
49 Equals(sorted([self.url, url])))
50 else:33 else:
51 # 1 domain: the local one
52 domain_entries = history.get_domain_entries()
53 self.assertThat(lambda: len(history.get_domain_entries()),34 self.assertThat(lambda: len(history.get_domain_entries()),
54 Eventually(Equals(1)))35 Eventually(Equals(1)))
5536 self.pointing_device.click_object(history.get_domain_entries()[0])
56 self.pointing_device.click_object(domain_entries[0])
57 expanded_history = self.main_window.get_expanded_history_view()37 expanded_history = self.main_window.get_expanded_history_view()
58
59 # 2 addresses: /test1 and /test2
60 self.assertThat(lambda: len(expanded_history.get_entries()),38 self.assertThat(lambda: len(expanded_history.get_entries()),
61 Eventually(Equals(2)))39 Eventually(Equals(len(ordered_urls))))
6240 entries = expanded_history.get_entries()
63 delegates = expanded_history.get_entries()41 self.assertThat([entry.url for entry in entries], Equals(ordered_urls))
64 self.assertThat(sorted([delegate.url for delegate in delegates]),42 return entries
65 Equals(sorted([self.url, url])))43
44 def test_404_not_saved(self):
45 url = self.base_url + "/notfound"
46 self.main_window.go_to_url(url)
47 self.main_window.wait_until_page_loaded(url)
48
49 # A valid url to be sure the fact the 404 page isn't present in the
50 # history view isn't a timing issue.
51 url = self.base_url + "/test2"
52 self.main_window.go_to_url(url)
53 self.main_window.wait_until_page_loaded(url)
54
55 self.open_history()
56 self.expect_history_entries([url, self.url])
6657
67 def test_expanded_history_view_header_swallows_clicks(self):58 def test_expanded_history_view_header_swallows_clicks(self):
68 # Regression test for https://launchpad.net/bugs/151890459 # Regression test for https://launchpad.net/bugs/1518904
@@ -78,3 +69,58 @@
78 # There should be only one instance on the expanded history view.69 # There should be only one instance on the expanded history view.
79 # If there’s more, the following call will raise an exception.70 # If there’s more, the following call will raise an exception.
80 self.main_window.get_expanded_history_view()71 self.main_window.get_expanded_history_view()
72
73 def test_favicon_saved(self):
74 # Regression test for https://launchpad.net/bugs/1549780
75 url = self.base_url + "/favicon"
76 self.main_window.go_to_url(url)
77 self.main_window.wait_until_page_loaded(url)
78 self.open_history()
79 first = self.expect_history_entries([url, self.url])[0]
80 self.assertThat(first.url, Equals(url))
81 favicon = self.base_url + "/assets/icon1.png"
82 self.assertThat(first.icon, Equals(favicon))
83
84 def test_favicon_updated(self):
85 # Verify that a page dynamically updating its favicon
86 # triggers an update in the history database.
87 url = self.base_url + "/changingfavicon"
88 self.main_window.go_to_url(url)
89 self.main_window.wait_until_page_loaded(url)
90 self.open_history()
91 first = self.expect_history_entries([url, self.url])[0]
92 indexes = set()
93 while len(indexes) < 3:
94 self.assertThat(first.url, Equals(url))
95 icon = first.icon
96 self.assertThat(icon, StartsWith(self.base_url))
97 self.assertThat(icon, EndsWith(".png"))
98 indexes.add(int(first.icon[(len(self.base_url)+1):-4]))
99
100 def test_title_saved(self):
101 self.open_history()
102 entry = self.expect_history_entries([self.url])[0]
103 self.assertThat(entry.title, Equals("test page 1"))
104
105 def test_title_not_updated(self):
106 # Verify that a page dynamically updating its title
107 # does NOT trigger an update in the history database.
108 url = self.base_url + "/changingtitle"
109 self.main_window.go_to_url(url)
110 self.main_window.wait_until_page_loaded(url)
111 self.open_history()
112 first = self.expect_history_entries([url, self.url])[0]
113 for i in range(10):
114 self.assertThat(first.title, Equals("title0"))
115 time.sleep(0.5)
116
117 def test_pushstate_updates_history(self):
118 url = self.base_url + "/pushstate"
119 self.main_window.go_to_url(url)
120 self.main_window.wait_until_page_loaded(url)
121 webview = self.main_window.get_current_webview()
122 self.pointing_device.click_object(webview)
123 pushed = self.base_url + "/statepushed"
124 self.main_window.wait_until_page_loaded(pushed)
125 self.open_history()
126 self.expect_history_entries([pushed, url, self.url])
81127
=== modified file 'tests/unittests/history-model/tst_HistoryModelTests.cpp'
--- tests/unittests/history-model/tst_HistoryModelTests.cpp 2015-10-07 11:10:14 +0000
+++ tests/unittests/history-model/tst_HistoryModelTests.cpp 2016-03-01 09:31:14 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright 2013 Canonical Ltd.2 * Copyright 2013-2016 Canonical Ltd.
3 *3 *
4 * This file is part of webbrowser-app.4 * This file is part of webbrowser-app.
5 *5 *
@@ -136,6 +136,54 @@
136 QVERIFY(roles.contains(HistoryModel::Visits));136 QVERIFY(roles.contains(HistoryModel::Visits));
137 }137 }
138138
139 void shouldUpdateAttributes()
140 {
141 qRegisterMetaType<QVector<int> >();
142 QSignalSpy spyChanged(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
143 QUrl url(QStringLiteral("http://example.org/"));
144 QCOMPARE(model->add(url, QStringLiteral(""), QUrl()), 1);
145
146 QVERIFY(!model->update(QUrl(), QStringLiteral(""), QUrl()));
147 QVERIFY(spyChanged.isEmpty());
148
149 QVERIFY(!model->update(QUrl(QStringLiteral("http://example.com/")), QStringLiteral(""), QUrl()));
150 QVERIFY(spyChanged.isEmpty());
151
152 QVERIFY(!model->update(url, QStringLiteral(""), QUrl()));
153 QVERIFY(spyChanged.isEmpty());
154
155 QVERIFY(model->update(url, QStringLiteral("title update"), QUrl()));
156 QCOMPARE(spyChanged.count(), 1);
157 QList<QVariant> args = spyChanged.takeFirst();
158 QCOMPARE(args.at(0).toModelIndex().row(), 0);
159 QCOMPARE(args.at(1).toModelIndex().row(), 0);
160 QVector<int> roles = args.at(2).value<QVector<int> >();
161 QCOMPARE(roles.size(), 1);
162 QVERIFY(roles.contains(HistoryModel::Title));
163 QCOMPARE(model->get(0)[QStringLiteral("visits")].toInt(), 1);
164
165 QVERIFY(model->update(url, QStringLiteral("title update"), QUrl(QStringLiteral("image://webicon/123"))));
166 QCOMPARE(spyChanged.count(), 1);
167 args = spyChanged.takeFirst();
168 QCOMPARE(args.at(0).toModelIndex().row(), 0);
169 QCOMPARE(args.at(1).toModelIndex().row(), 0);
170 roles = args.at(2).value<QVector<int> >();
171 QCOMPARE(roles.size(), 1);
172 QVERIFY(roles.contains(HistoryModel::Icon));
173 QCOMPARE(model->get(0)[QStringLiteral("visits")].toInt(), 1);
174
175 QVERIFY(model->update(url, QStringLiteral("title update 2"), QUrl(QStringLiteral("image://webicon/1234"))));
176 QCOMPARE(spyChanged.count(), 1);
177 args = spyChanged.takeFirst();
178 QCOMPARE(args.at(0).toModelIndex().row(), 0);
179 QCOMPARE(args.at(1).toModelIndex().row(), 0);
180 roles = args.at(2).value<QVector<int> >();
181 QCOMPARE(roles.size(), 2);
182 QVERIFY(roles.contains(HistoryModel::Title));
183 QVERIFY(roles.contains(HistoryModel::Icon));
184 QCOMPARE(model->get(0)[QStringLiteral("visits")].toInt(), 1);
185 }
186
139 void shouldNotifyWhenHidingOrUnHidingExistingEntry()187 void shouldNotifyWhenHidingOrUnHidingExistingEntry()
140 {188 {
141 qRegisterMetaType<QVector<int> >();189 qRegisterMetaType<QVector<int> >();

Subscribers

People subscribed via source and target branches

to status/vote changes: