Merge lp:~uriboni/webbrowser-app/search-suggestions into lp:webbrowser-app

Proposed by Ugo Riboni on 2015-04-22
Status: Superseded
Proposed branch: lp:~uriboni/webbrowser-app/search-suggestions
Merge into: lp:webbrowser-app
Prerequisite: lp:~uriboni/webbrowser-app/bookmarks-in-suggestions
Diff against target: 633 lines (+272/-48)
10 files modified
src/app/config.h.in (+5/-0)
src/app/webbrowser/Browser.qml (+26/-5)
src/app/webbrowser/SearchSuggestions.qml (+78/-0)
src/app/webbrowser/Suggestion.qml (+5/-2)
src/app/webbrowser/Suggestions.qml (+22/-11)
src/app/webbrowser/searchengine.cpp (+13/-1)
src/app/webbrowser/searchengine.h (+4/-0)
tests/autopilot/webbrowser_app/tests/__init__.py (+8/-0)
tests/autopilot/webbrowser_app/tests/http_server.py (+14/-1)
tests/autopilot/webbrowser_app/tests/test_suggestions.py (+97/-28)
To merge this branch: bzr merge lp:~uriboni/webbrowser-app/search-suggestions
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing on 2015-05-07
Olivier Tilloy 2015-04-22 Approve on 2015-04-28
Review via email: mp+257062@code.launchpad.net

This proposal has been superseded by a proposal from 2015-05-12.

Description of the Change

Add suggestions from search engines in the suggestions list.

To post a comment you must log in.
Ugo Riboni (uriboni) wrote :

Added inline comments

Olivier Tilloy (osomon) wrote :

From a very quick functional test, this seems to work nicely (I’ll do more in-depth testing and a code review later).

One thing that I noticed is that the terms are not highlighted in search suggestions, as they are for history entries and bookmarks. According to the visual spec they should be.

review: Needs Fixing
976. By Ugo Riboni on 2015-04-22

Suggestion title should be highlighted, was removed by mistake

Olivier Tilloy (osomon) wrote :

« This API is not official, but it is what Google products and everyone else uses to implement search suggestions. The "client=firefox" is there to request JSON output. »

Can you add this as a comment in config.h.in ?

977. By Ugo Riboni on 2015-04-23

Add comment to clarify Google suggestion URL

978. By Ugo Riboni on 2015-04-23

Add search suggestion tests and fix the ones that were failing before

979. By Ugo Riboni on 2015-04-23

Fix flake8 issues, refactor and simplify some tests, fix some broken tests

Olivier Tilloy (osomon) wrote :

I’m seeing one funny behaviour: I’m adding a custom search engine definition (https://duckduckgo.com/opensearch.xml) and making it the default search engine, but the suggestions URL doesn’t seem to be used, instead the default suggestions from Google are used.

There are two things to fix here:
 - that XML description file seems to have a valid suggestions URL, for some reason it’s not being parsed/used, it should be
 - when loading a valid XML description file that doesn’t have a suggestions URL, we shouldn’t fall back to Google, instead there shouldn’t be any search suggestions

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

When launching the app, the address bar is not focused, so the suggestions list is not shown, yet I’m seeing a query issued to the suggestions URL with the current URL. There shouldn’t be any queries issued until the address bar has focus and its contents change.

review: Needs Fixing
980. By Ugo Riboni on 2015-04-27

Ask for search suggestions only when the user is typing something

981. By Ugo Riboni on 2015-04-28

Remove unnecessary property alias

Olivier Tilloy (osomon) :
review: Approve
982. By Ugo Riboni on 2015-04-28

Merge changes from trunk

983. By Ugo Riboni on 2015-05-06

Merge changes from trunk

984. By Ugo Riboni on 2015-05-06

Adjust order of suggestions according to new input from design. Fix tests accordingly.

Olivier Tilloy (osomon) wrote :

A few minor additional remarks:

In SearchSuggestions.qml:
  - QQuickItem already has an 'enabled' property, I don’t think it’s necessary to override its definition
  - in resetSearch(), the request (if any) should be aborted even if !enabled

A suggestion (pun not intended): in Browser.qml, in the two instances of LimitProxyModel that populate the list of suggestions for history entries and bookmarks, properties 'icon' and 'displayUrl' can be marked 'readonly'.

Similarly, in Suggestions.qml, the new properties added can be marked readonly.

In test_suggestions.py:
  - in setup_suggestions_source(), you should be able to replace "http://localhost:{}/" by "http://test/", as there is automatic mapping of test:80 requests to localhost:port
  - In highlight_term(), this function won’t work if there’s more than one occurrence of term in text. This is arguably unimportant for the scope of the tests, but the function could be simplified (and made to work in all cases) by simply returning "<html>{}</html>".format(re.sub(term, lambda m: self.highlight(m.group(0)), text))
  - in test_search_suggestions(), is the sleep really necessary?

Olivier Tilloy (osomon) wrote :

There is now a (trivial) conflict when merging this branch into the latest trunk.

test_suggestions.py will require a bit of refactoring to make use of the newly-introduced self.data_location (revision 994) to write test search engine descriptions.

985. By Ugo Riboni on 2015-05-12

Merge changes from trunk. Refactor some AP tests.

986. By Ugo Riboni on 2015-05-12

Merge new prerequisite branch to create separate temp dir for ~/.config

987. By Ugo Riboni on 2015-05-12

More fixes due to temp locations support

988. By Ugo Riboni on 2015-05-12

Create a new "active" property instead of overriding the Item.enabled property which might have side effects. Always abort the request when resetting.

989. By Ugo Riboni on 2015-05-12

Implement suggestions from code review

990. By Ugo Riboni on 2015-05-12

Remove unnecessary sleep

Ugo Riboni (uriboni) wrote :

All suggestions implemented except for the following:

> In SearchSuggestions.qml:
> - QQuickItem already has an 'enabled' property, I don’t think it’s necessary
> to override its definition

Item.enabled has a completely different meaning tied to keyboard focus, and re-using it to do something else is wrong (and actually does not ever work since the property is toggled on and off various times by QML itself).
It was my mistake to redefine it though, as the original can't be accessed any more, so I defined a new "active" property that does the same thing, while leaving Item.enabled alone and with its original meaning.

> In test_suggestions.py:
> - in setup_suggestions_source(), you should be able to replace
> "http://localhost:{}/" by "http://test/", as there is automatic mapping of
> test:80 requests to localhost:port

Is this a DNS mapping or a redirect of some sort ? Because it does not seem to work when using XmlHttpRequest from QML to get the list of suggestions. The responseText comes out empty, which is the way XmlHttpRequest signals errors, but I can't tell more details on why.
I spent way too much time on this already, so I will leave it as it was for now.

> - In highlight_term(), this function won’t work if there’s more than one
> occurrence of term in text. This is arguably unimportant for the scope of the
> tests, but the function could be simplified (and made to work in all cases) by
> simply returning "<html>{}</html>".format(re.sub(term, lambda m:
> self.highlight(m.group(0)), text))

This does not work. In some cases I get a complain from re.py of "unbalanced parenthesis". Unless you have a quick fix I would leave it as it is.

991. By Ugo Riboni on 2015-05-12

Add project name in main cmake file, for convenience in qtcreator

992. By Ugo Riboni on 2015-05-12

Keep the server in a temporary variable, instead of in the test instance

993. By Ugo Riboni on 2015-05-12

More safely store the server in an instance variable

994. By Ugo Riboni on 2015-05-12

Do the same in the package main

995. By Ugo Riboni on 2015-05-12

Better names

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/config.h.in'
2--- src/app/config.h.in 2015-03-23 22:35:12 +0000
3+++ src/app/config.h.in 2015-05-12 10:18:25 +0000
4@@ -30,6 +30,11 @@
5 #define DEFAULT_SEARCH_DESC "Use google.com to search the Web"
6 #define DEFAULT_SEARCH_TEMPLATE "https://google.com/search?client=ubuntu&q={searchTerms}&ie=utf-8&oe=utf-8"
7
8+// This API is not official, but it is what Google products and everyone else
9+// use to implement search suggestions. The "client=firefox" is there to request
10+// JSON output.
11+#define DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE "http://suggestqueries.google.com/complete/search?client=firefox&q={searchTerms}"
12+
13 inline bool isRunningInstalled()
14 {
15 static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath());
16
17=== modified file 'src/app/webbrowser/Browser.qml'
18--- src/app/webbrowser/Browser.qml 2015-05-06 21:51:16 +0000
19+++ src/app/webbrowser/Browser.qml 2015-05-12 10:18:25 +0000
20@@ -26,6 +26,8 @@
21 import "../actions" as Actions
22 import ".."
23 import "../UrlUtils.js" as UrlUtils
24+import "urlManagement.js" as UrlManagement
25+
26
27 BrowserView {
28 id: browser
29@@ -272,12 +274,15 @@
30
31 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 })
32
33- models: [historySuggestions, bookmarksSuggestions]
34+ models: [historySuggestions,
35+ bookmarksSuggestions,
36+ searchSuggestions.limit(4)]
37
38 LimitProxyModel {
39 id: historySuggestions
40- limit: 4
41- property string icon: "history"
42+ limit: 2
43+ readonly property string icon: "history"
44+ readonly property bool displayUrl: true
45 sourceModel: SuggestionsFilterModel {
46 sourceModel: browser.historyModel
47 terms: suggestionsList.searchTerms
48@@ -287,8 +292,9 @@
49
50 LimitProxyModel {
51 id: bookmarksSuggestions
52- limit: 4
53- property string icon: "non-starred"
54+ limit: 2
55+ readonly property string icon: "non-starred"
56+ readonly property bool displayUrl: true
57 sourceModel: SuggestionsFilterModel {
58 sourceModel: browser.bookmarksModel
59 terms: suggestionsList.searchTerms
60@@ -296,6 +302,21 @@
61 }
62 }
63
64+ SearchSuggestions {
65+ id: searchSuggestions
66+ terms: suggestionsList.searchTerms
67+ searchEngine: currentSearchEngine
68+ active: chrome.activeFocus &&
69+ !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+"))
70+
71+ function limit(number) {
72+ var slice = results.slice(0, number)
73+ slice.icon = 'search'
74+ slice.displayUrl = false
75+ return slice
76+ }
77+ }
78+
79 onSelected: {
80 browser.currentWebview.url = url
81 browser.currentWebview.forceActiveFocus()
82
83=== added file 'src/app/webbrowser/SearchSuggestions.qml'
84--- src/app/webbrowser/SearchSuggestions.qml 1970-01-01 00:00:00 +0000
85+++ src/app/webbrowser/SearchSuggestions.qml 2015-05-12 10:18:25 +0000
86@@ -0,0 +1,78 @@
87+/*
88+ * Copyright 2015 Canonical Ltd.
89+ *
90+ * This file is part of webbrowser-app.
91+ *
92+ * webbrowser-app is free software; you can redistribute it and/or modify
93+ * it under the terms of the GNU General Public License as published by
94+ * the Free Software Foundation; version 3.
95+ *
96+ * webbrowser-app is distributed in the hope that it will be useful,
97+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
98+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
99+ * GNU General Public License for more details.
100+ *
101+ * You should have received a copy of the GNU General Public License
102+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
103+ */
104+
105+import QtQuick 2.0
106+import webbrowserapp.private 0.1
107+
108+Item {
109+ property var terms
110+ property SearchEngine searchEngine
111+ property var results: []
112+ property bool active: false
113+
114+ property var _request: new XMLHttpRequest()
115+ onSearchEngineChanged: resetSearch()
116+ onTermsChanged: resetSearch()
117+ onActiveChanged: resetSearch()
118+
119+ Component.onCompleted: {
120+ _request.onreadystatechange = function() {
121+ if (_request.readyState === XMLHttpRequest.DONE) {
122+ results = parseResponse(_request.responseText)
123+ }
124+ }
125+ }
126+
127+ Timer {
128+ id: limiter
129+ interval: 250
130+ onTriggered: {
131+ if (_request && terms.length > 0 && searchEngine) {
132+ var url = searchEngine.suggestionsUrlTemplate
133+ url = url.replace("{searchTerms}", encodeURIComponent(terms.join(" ")))
134+
135+ _request.open("GET", url);
136+ _request.send();
137+ }
138+ }
139+ }
140+
141+ function parseResponse(response) {
142+ try {
143+ var data = JSON.parse(response)
144+ } catch (error) {
145+ return []
146+ }
147+
148+ if (data.length > 1) {
149+ return data[1].map(function(result) {
150+ return {
151+ title: result,
152+ url: searchEngine.urlTemplate.replace("{searchTerms}",
153+ encodeURIComponent(result))
154+ }
155+ })
156+ } else return []
157+ }
158+
159+ function resetSearch() {
160+ results = []
161+ if (_request) _request.abort()
162+ if (active) limiter.restart()
163+ }
164+}
165
166=== modified file 'src/app/webbrowser/Suggestion.qml'
167--- src/app/webbrowser/Suggestion.qml 2015-04-16 16:42:04 +0000
168+++ src/app/webbrowser/Suggestion.qml 2015-05-12 10:18:25 +0000
169@@ -41,7 +41,7 @@
170 right: parent.right
171 verticalCenter: parent.verticalCenter
172 }
173- height: label.height + subLabel.height
174+ height: subLabel.visible ? label.height + subLabel.height : icon.height
175
176 Icon {
177 id: icon
178@@ -56,11 +56,13 @@
179 Label {
180 id: label
181 anchors {
182- top: parent.top
183+ top: subLabel.visible ? parent.top : undefined
184+ verticalCenter: subLabel.visible ? undefined : parent.verticalCenter
185 left: icon.right
186 leftMargin: units.gu(2)
187 right: parent.right
188 }
189+
190 elide: Text.ElideRight
191 }
192
193@@ -74,6 +76,7 @@
194 }
195 fontSize: "small"
196 elide: Text.ElideRight
197+ visible: text !== ""
198 }
199 }
200
201
202=== modified file 'src/app/webbrowser/Suggestions.qml'
203--- src/app/webbrowser/Suggestions.qml 2015-04-16 16:41:31 +0000
204+++ src/app/webbrowser/Suggestions.qml 2015-05-12 10:18:25 +0000
205@@ -24,8 +24,8 @@
206
207 property var searchTerms
208 property var models
209- property int count: models.reduce(countItems, 0)
210- property alias contentHeight: suggestionsList.contentHeight
211+ readonly property int count: models.reduce(countItems, 0)
212+ readonly property alias contentHeight: suggestionsList.contentHeight
213
214 signal selected(url url)
215
216@@ -43,25 +43,34 @@
217
218 model: suggestions.models
219 delegate: Column {
220+ id: suggestionsSection
221 width: suggestionsList.width
222 height: childrenRect.height
223
224+ property string icon: models[index].icon
225+ property bool displayUrl: models[index].displayUrl
226+ property int firstItemIndex: models.slice(0, index).reduce(countItems, 0)
227+
228 Repeater {
229 id: suggestionsSource
230 model: modelData
231- property int firstItemIndex: models.slice(0, index).reduce(countItems, 0)
232
233 delegate: Suggestion {
234+ id: suggestion
235 width: suggestionsList.width
236- showDivider: suggestionsSource.firstItemIndex + index <
237+ showDivider: suggestionsSection.firstItemIndex + index <
238 suggestions.count - 1
239
240- title: highlightTerms(model.title)
241- subtitle: highlightTerms(model.url)
242- url: model.url
243- icon: suggestionsSource.model.icon
244-
245- onSelected: suggestions.selected(url)
246+ // Necessary to support both using objects inheriting from
247+ // QAbstractItemModel and JS arrays as models, since they
248+ // expose their data differently
249+ property var item: (model.modelData) ? model.modelData : model
250+
251+ title: highlightTerms(item.title)
252+ subtitle: suggestionsSection.displayUrl ? highlightTerms(item.url) : ""
253+ icon: suggestionsSection.icon
254+
255+ onSelected: suggestions.selected(item.url)
256 }
257 }
258 }
259@@ -97,5 +106,7 @@
260 return highlighted
261 }
262
263- function countItems(total, model) { return total + model.count }
264+ function countItems(total, model) {
265+ return total + (model.hasOwnProperty("length") ? model.length : model.count);
266+ }
267 }
268
269=== modified file 'src/app/webbrowser/searchengine.cpp'
270--- src/app/webbrowser/searchengine.cpp 2015-03-19 11:58:33 +0000
271+++ src/app/webbrowser/searchengine.cpp 2015-05-12 10:18:25 +0000
272@@ -30,6 +30,7 @@
273 , m_name(DEFAULT_SEARCH_NAME)
274 , m_description(DEFAULT_SEARCH_DESC)
275 , m_template(DEFAULT_SEARCH_TEMPLATE)
276+ , m_suggestionsTemplate(DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE)
277 {
278 }
279
280@@ -47,6 +48,7 @@
281 m_name = DEFAULT_SEARCH_NAME;
282 m_description = DEFAULT_SEARCH_DESC;
283 m_template = DEFAULT_SEARCH_TEMPLATE;
284+ m_suggestionsTemplate = DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE;
285
286 if (!filename.isEmpty()) {
287 QString filepath = QStandardPaths::locate(QStandardPaths::DataLocation,
288@@ -66,9 +68,13 @@
289 } else if (name == "Description") {
290 m_description = parser.readElementText();
291 } else if (name == "Url") {
292- if (parser.attributes().value("type") == "text/html") {
293+ QStringRef type = parser.attributes().value("type");
294+ if (type == "text/html") {
295 m_template = parser.attributes().value("template").toString();
296+ } else if (type == "application/x-suggestions+json") {
297+ m_suggestionsTemplate = parser.attributes().value("template").toString();
298 }
299+
300 }
301 }
302 }
303@@ -79,6 +85,7 @@
304 Q_EMIT nameChanged();
305 Q_EMIT descriptionChanged();
306 Q_EMIT urlTemplateChanged();
307+ Q_EMIT suggestionsUrlTemplateChanged();
308 }
309 }
310
311@@ -96,3 +103,8 @@
312 {
313 return m_template;
314 }
315+
316+const QString& SearchEngine::suggestionsUrlTemplate() const
317+{
318+ return m_suggestionsTemplate;
319+}
320
321=== modified file 'src/app/webbrowser/searchengine.h'
322--- src/app/webbrowser/searchengine.h 2015-03-19 11:58:33 +0000
323+++ src/app/webbrowser/searchengine.h 2015-05-12 10:18:25 +0000
324@@ -31,6 +31,7 @@
325 Q_PROPERTY(QString name READ name NOTIFY nameChanged)
326 Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
327 Q_PROPERTY(QString urlTemplate READ urlTemplate NOTIFY urlTemplateChanged)
328+ Q_PROPERTY(QString suggestionsUrlTemplate READ suggestionsUrlTemplate NOTIFY suggestionsUrlTemplateChanged)
329
330 public:
331 SearchEngine(QObject* parent=0);
332@@ -41,18 +42,21 @@
333 const QString& name() const;
334 const QString& description() const;
335 const QString& urlTemplate() const;
336+ const QString& suggestionsUrlTemplate() const;
337
338 Q_SIGNALS:
339 void filenameChanged() const;
340 void nameChanged() const;
341 void descriptionChanged() const;
342 void urlTemplateChanged() const;
343+ void suggestionsUrlTemplateChanged() const;
344
345 private:
346 QString m_filename;
347 QString m_name;
348 QString m_description;
349 QString m_template;
350+ QString m_suggestionsTemplate;
351 };
352
353 #endif // __SEARCH_ENGINE_H__
354
355=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
356--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-04-27 03:23:00 +0000
357+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-05-12 10:18:25 +0000
358@@ -65,6 +65,14 @@
359 if not os.path.exists(self.data_location):
360 os.makedirs(self.data_location)
361
362+ xdg_config = os.path.join(self._temp_xdg_dir, 'config')
363+ self.useFixture(fixtures.EnvironmentVariable(
364+ 'XDG_CONFIG_HOME',
365+ xdg_config))
366+ self.config_location = os.path.join(xdg_config, appname)
367+ if not os.path.exists(self.config_location):
368+ os.makedirs(self.config_location)
369+
370 xdg_cache = os.path.join(self._temp_xdg_dir, 'cache')
371 self.useFixture(fixtures.EnvironmentVariable(
372 'XDG_CACHE_HOME',
373
374=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
375--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-03-20 15:08:55 +0000
376+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-05-12 10:18:25 +0000
377@@ -7,6 +7,7 @@
378 # by the Free Software Foundation.
379
380 import http.server as http
381+import json
382 import logging
383 import threading
384 import time
385@@ -19,6 +20,7 @@
386 """
387 A custom HTTP request handler that serves GET resources.
388 """
389+ suggestions_data = {}
390
391 def make_html(self, title, body):
392 html = "<html><title>{}</title><body>{}</body></html>"
393@@ -130,6 +132,14 @@
394 html += '<div style="height: 100%"></div>'
395 html += '</a></body></html>'
396 self.send_html(html)
397+ elif self.path.startswith("/suggest"):
398+ self.send_response(200)
399+ self.send_header("Content-Type", "text/x-suggestions+json")
400+ self.end_headers()
401+ query = self.path[len("/suggest?q="):]
402+ if query in self.suggestions_data:
403+ suggestions = self.suggestions_data[query]
404+ self.wfile.write(json.dumps(suggestions).encode())
405 else:
406 self.send_error(404)
407
408@@ -145,10 +155,13 @@
409 """
410 A simple custom HTTP server run in a separate thread.
411 """
412+ def set_suggestions_data(self, data):
413+ self.handler.suggestions_data = data
414
415 def __init__(self):
416 # port == 0 will assign a random free port
417- self.server = http.HTTPServer(("", 0), HTTPRequestHandler)
418+ self.handler = HTTPRequestHandler
419+ self.server = http.HTTPServer(("", 0), self.handler)
420 self.server.allow_reuse_address = True
421 self.server_thread = threading.Thread(target=self.server.serve_forever)
422 self.server_thread.start()
423
424=== modified file 'tests/autopilot/webbrowser_app/tests/test_suggestions.py'
425--- tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-04-27 03:23:00 +0000
426+++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-05-12 10:18:25 +0000
427@@ -23,6 +23,7 @@
428 from autopilot.matchers import Eventually
429
430 from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
431+from . import http_server
432
433
434 class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase):
435@@ -31,6 +32,7 @@
436
437 def setUp(self):
438 self.create_temporary_profile()
439+
440 self.populate_history()
441 self.populate_bookmarks()
442 super(PrepopulatedDatabaseTestCaseBase, self).setUp()
443@@ -56,7 +58,6 @@
444 "Ubuntu (philosophy) - Wikipedia, the free encyclopedia"),
445 (search_uri.format("example"), "google.com",
446 "example - Google Search"),
447- ("http://example.iana.org/", "iana.org", "Example Domain"),
448 ("http://www.iana.org/domains/special", "iana.org",
449 "IANA — Special Use Domains"),
450 ("http://doc.qt.io/qt-5/qtqml-index.html", "qt.io",
451@@ -91,8 +92,6 @@
452 "Samarium - Element Information"),
453 ("http://en.wikipedia.org/wiki/Linux",
454 "Linux - Wikipedia, the free encyclopedia"),
455- ("https://www.linux.com/",
456- "Linux.com | The source for Linux information"),
457 ("http://doc.qt.io/qt-5/qtqml-index.html",
458 "Qt QML 5.4 - Qt Documentation")
459 ]
460@@ -112,10 +111,54 @@
461
462 """Test the address bar suggestions (based on history and bookmarks)."""
463
464+ def setup_suggestions_source(self, server):
465+ search_engines_path = os.path.join(self.data_location, "searchengines")
466+ os.makedirs(search_engines_path, exist_ok=True)
467+ with open(os.path.join(search_engines_path, "test.xml"), "w") as f:
468+ f.write("""
469+ <OpenSearchDescription>
470+ <Url type="application/x-suggestions+json"
471+ template="http://localhost:{}/suggest?q={searchTerms}"/>
472+ <Url type="text/html"
473+ template="http://aserver.somewhere/search?q={searchTerms}"/>
474+ </OpenSearchDescription>
475+ """.replace("{}", str(server.port)))
476+
477+ with open(os.path.join(self.config_location, "webbrowser-app.conf"),
478+ "w") as f:
479+ f.write("""
480+ [General]
481+ searchEngine=test
482+ """)
483+ server.set_suggestions_data({
484+ "high": ["high", ["highlight"]],
485+ "foo": ["foo", ["food", "foot", "fool", "foobar", "foo five"]],
486+ "QML": ["QML", ["qt qml", "qml documentation", "qml rocks"]]
487+ })
488+
489 def setUp(self):
490+ self.server = http_server.HTTPServerInAThread()
491+ self.ping_server()
492+ self.addCleanup(self.server.cleanup)
493+
494+ self.create_temporary_profile()
495+ self.setup_suggestions_source(self.server)
496+
497 super(TestSuggestions, self).setUp()
498+
499 self.address_bar = self.main_window.address_bar
500
501+ def highlight_term(self, text, term):
502+ parts = text.split(term)
503+ if len(parts) < 2:
504+ return text
505+ else:
506+ pattern = '<html>{}{}{}</html>'
507+ return pattern.format(parts[0], self.highlight(term), parts[1])
508+
509+ def highlight(self, text):
510+ return '<b><font color="#dd4814">{}</font></b>'.format(text)
511+
512 def assert_suggestions_eventually_shown(self):
513 suggestions = self.main_window.get_suggestions()
514 self.assertThat(suggestions.opacity, Eventually(Equals(1)))
515@@ -136,38 +179,47 @@
516
517 def test_list_of_suggestions_case_insensitive(self):
518 suggestions = self.main_window.get_suggestions()
519- self.address_bar.write('xaMPL')
520- self.assertThat(suggestions.count, Eventually(Equals(2)))
521+ self.address_bar.write('SpEciAl')
522+ self.assertThat(suggestions.count, Eventually(Equals(1)))
523
524 def test_list_of_suggestions_history_limits(self):
525 suggestions = self.main_window.get_suggestions()
526 self.address_bar.write('ubuntu')
527 self.assert_suggestions_eventually_shown()
528- self.assertThat(suggestions.count, Eventually(Equals(4)))
529+ self.assertThat(suggestions.count, Eventually(Equals(2)))
530 self.address_bar.write('bleh', clear=False)
531 self.assertThat(suggestions.count, Eventually(Equals(0)))
532 self.address_bar.write('iana')
533- self.assertThat(suggestions.count, Eventually(Equals(2)))
534+ self.assertThat(suggestions.count, Eventually(Equals(1)))
535
536 def test_list_of_suggestions_bookmark_limits(self):
537 suggestions = self.main_window.get_suggestions()
538 self.address_bar.write('element')
539 self.assert_suggestions_eventually_shown()
540+ self.assertThat(suggestions.count, Eventually(Equals(2)))
541+ self.address_bar.write('bleh', clear=False)
542+ self.assertThat(suggestions.count, Eventually(Equals(0)))
543+ self.address_bar.write('linux')
544+ self.assertThat(suggestions.count, Eventually(Equals(1)))
545+
546+ def test_list_of_suggestions_search_limits(self):
547+ suggestions = self.main_window.get_suggestions()
548+ self.address_bar.write('foo')
549+ self.assert_suggestions_eventually_shown()
550 self.assertThat(suggestions.count, Eventually(Equals(4)))
551 self.address_bar.write('bleh', clear=False)
552 self.assertThat(suggestions.count, Eventually(Equals(0)))
553- self.address_bar.write('linux')
554- self.assertThat(suggestions.count, Eventually(Equals(2)))
555
556 def test_list_of_suggestions_order(self):
557 suggestions = self.main_window.get_suggestions()
558 self.address_bar.write('QML')
559 self.assert_suggestions_eventually_shown()
560- self.assertThat(suggestions.count, Eventually(Equals(2)))
561+ self.assertThat(suggestions.count, Eventually(Equals(5)))
562 entries = suggestions.get_ordered_entries()
563- self.assertThat(len(entries), Equals(2))
564+ self.assertThat(len(entries), Equals(5))
565 self.assertThat(entries[0].icon, Equals("history"))
566 self.assertThat(entries[1].icon, Equals("non-starred"))
567+ self.assertThat(entries[2].icon, Equals("search"))
568
569 def test_clear_address_bar_dismisses_suggestions(self):
570 self.address_bar.focus()
571@@ -208,21 +260,29 @@
572 self.address_bar.focus()
573 self.assert_suggestions_eventually_shown()
574 self.address_bar.clear()
575- self.address_bar.write('ubuntu')
576- self.assert_suggestions_eventually_shown()
577- self.assertThat(suggestions.count, Eventually(Equals(4)))
578- entries = suggestions.get_ordered_entries()
579- highlight = '<b><font color="#dd4814">Ubuntu</font></b>'
580- url = "http://en.wikipedia.org/wiki/{}_(operating_system)"
581- url = url.format(highlight)
582- entries = [entry for entry in entries if url in entry.subtitle]
583- entry = entries[0] if len(entries) == 1 else None
584- self.assertIsNotNone(entry)
585- self.pointing_device.click_object(entry)
586- webview = self.main_window.get_current_webview()
587- url = "wikipedia.org/wiki/Ubuntu_(operating_system)"
588+ self.address_bar.write('linux')
589+ self.assert_suggestions_eventually_shown()
590+ self.assertThat(suggestions.count, Eventually(Equals(1)))
591+ entries = suggestions.get_ordered_entries()
592+ url = "http://en.wikipedia.org/wiki/Linux"
593+ self.pointing_device.click_object(entries[0])
594+ webview = self.main_window.get_current_webview()
595+ self.assertThat(webview.url, Eventually(Equals(url)))
596+ self.assert_suggestions_eventually_hidden()
597+
598+ def test_select_search_suggestion(self):
599+ suggestions = self.main_window.get_suggestions()
600+ self.address_bar.focus()
601+ self.assert_suggestions_eventually_shown()
602+ self.address_bar.clear()
603+ self.address_bar.write('high')
604+ self.assert_suggestions_eventually_shown()
605+ self.assertThat(suggestions.count, Eventually(Equals(1)))
606+ entries = suggestions.get_ordered_entries()
607+ self.pointing_device.click_object(entries[0])
608+ webview = self.main_window.get_current_webview()
609+ url = "aserver.somewhere/search?q=highlight"
610 self.assertThat(webview.url, Eventually(Contains(url)))
611- self.assert_suggestions_eventually_hidden()
612
613 def test_special_characters(self):
614 self.address_bar.clear()
615@@ -231,6 +291,15 @@
616 suggestions = self.main_window.get_suggestions()
617 self.assertThat(suggestions.count, Eventually(Equals(1)))
618 entry = suggestions.get_ordered_entries()[0]
619- highlight = '<b><font color="#dd4814">(phil</font></b>'
620- url = "http://en.wikipedia.org/wiki/Ubuntu_{}osophy)".format(highlight)
621- self.assertThat(entry.subtitle, Contains(url))
622+ url = "http://en.wikipedia.org/wiki/Ubuntu_(philosophy)"
623+ highlighted = self.highlight_term(url, "(phil")
624+ self.assertThat(entry.subtitle, Equals(highlighted))
625+
626+ def test_search_suggestions(self):
627+ self.address_bar.write('high')
628+ suggestions = self.main_window.get_suggestions()
629+ self.assertThat(suggestions.count, Eventually(Equals(1)))
630+ entries = suggestions.get_ordered_entries()
631+ highlighted = self.highlight_term("highlight", "high")
632+ self.assertThat(entries[0].title, Equals(highlighted))
633+ self.assertThat(entries[0].subtitle, Equals(''))

Subscribers

People subscribed via source and target branches

to status/vote changes: