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

Proposed by Ugo Riboni
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 995
Merged at revision: 1006
Proposed branch: lp:~uriboni/webbrowser-app/search-suggestions
Merge into: lp:webbrowser-app
Prerequisite: lp:~osomon/webbrowser-app/autopilot-temp-profile-config
Diff against target: 659 lines (+272/-54)
11 files modified
CMakeLists.txt (+2/-0)
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 (+6/-6)
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
Olivier Tilloy Approve
PS Jenkins bot continuous-integration Needs Fixing
Review via email: mp+258856@code.launchpad.net

This proposal supersedes a proposal from 2015-04-22.

Commit message

Add suggestions from search engines in the suggestions list.

Description of the change

Add suggestions from search engines in the suggestions list.

To post a comment you must log in.
Revision history for this message
Ugo Riboni (uriboni) wrote : Posted in a previous version of this proposal

Added inline comments

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

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
Revision history for this message
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

« 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 ?

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

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
Revision history for this message
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

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
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) : Posted in a previous version of this proposal
review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

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?

Revision history for this message
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

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.

Revision history for this message
Ugo Riboni (uriboni) wrote : Posted in a previous version of this proposal

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

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

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

> 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.

Yeah, that’s better and safer indeed.

> 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.

This is oxide-specific (see Oxide.WebContext.hostMappingRules), so indeed it won’t work with XmlHttpRequest. Sorry for putting you on a false trail, I hadn’t realized the requests were not going through the WebView.

> 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.

Yeah, that’s fine.

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

That looks all good now.
I have only one minor concern: in TestSuggestions.setUp(), you’re assigning to self.server, which is later overridden when calling the parent’s setUp(). I understand you’re doing that because the the ping_server() method relies on self.server, but I think this is confusing and makes the code harder to maintain in the long run. Can you modify ping_server() to take the server object as parameter, and assign the search suggestions server instance to a different member variable?

992. By Ugo Riboni

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

993. By Ugo Riboni

More safely store the server in an instance variable

994. By Ugo Riboni

Do the same in the package main

995. By Ugo Riboni

Better names

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'CMakeLists.txt'
--- CMakeLists.txt 2015-02-04 10:56:38 +0000
+++ CMakeLists.txt 2015-05-12 13:40:34 +0000
@@ -1,6 +1,8 @@
1cmake_minimum_required(VERSION 2.8.9)1cmake_minimum_required(VERSION 2.8.9)
2set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")2set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
33
4project("webbrowser")
5
4find_program(INTLTOOL_MERGE intltool-merge)6find_program(INTLTOOL_MERGE intltool-merge)
5if(NOT INTLTOOL_MERGE)7if(NOT INTLTOOL_MERGE)
6 message(FATAL_ERROR "Could not find intltool-merge, please install the intltool package")8 message(FATAL_ERROR "Could not find intltool-merge, please install the intltool package")
79
=== modified file 'src/app/config.h.in'
--- src/app/config.h.in 2015-03-23 22:35:12 +0000
+++ src/app/config.h.in 2015-05-12 13:40:34 +0000
@@ -30,6 +30,11 @@
30#define DEFAULT_SEARCH_DESC "Use google.com to search the Web"30#define DEFAULT_SEARCH_DESC "Use google.com to search the Web"
31#define DEFAULT_SEARCH_TEMPLATE "https://google.com/search?client=ubuntu&q={searchTerms}&ie=utf-8&oe=utf-8"31#define DEFAULT_SEARCH_TEMPLATE "https://google.com/search?client=ubuntu&q={searchTerms}&ie=utf-8&oe=utf-8"
3232
33// This API is not official, but it is what Google products and everyone else
34// use to implement search suggestions. The "client=firefox" is there to request
35// JSON output.
36#define DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE "http://suggestqueries.google.com/complete/search?client=firefox&q={searchTerms}"
37
33inline bool isRunningInstalled()38inline bool isRunningInstalled()
34{39{
35 static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath());40 static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath());
3641
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2015-05-06 21:51:16 +0000
+++ src/app/webbrowser/Browser.qml 2015-05-12 13:40:34 +0000
@@ -26,6 +26,8 @@
26import "../actions" as Actions26import "../actions" as Actions
27import ".."27import ".."
28import "../UrlUtils.js" as UrlUtils28import "../UrlUtils.js" as UrlUtils
29import "urlManagement.js" as UrlManagement
30
2931
30BrowserView {32BrowserView {
31 id: browser33 id: browser
@@ -272,12 +274,15 @@
272274
273 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 })275 searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 })
274276
275 models: [historySuggestions, bookmarksSuggestions]277 models: [historySuggestions,
278 bookmarksSuggestions,
279 searchSuggestions.limit(4)]
276280
277 LimitProxyModel {281 LimitProxyModel {
278 id: historySuggestions282 id: historySuggestions
279 limit: 4283 limit: 2
280 property string icon: "history"284 readonly property string icon: "history"
285 readonly property bool displayUrl: true
281 sourceModel: SuggestionsFilterModel {286 sourceModel: SuggestionsFilterModel {
282 sourceModel: browser.historyModel287 sourceModel: browser.historyModel
283 terms: suggestionsList.searchTerms288 terms: suggestionsList.searchTerms
@@ -287,8 +292,9 @@
287292
288 LimitProxyModel {293 LimitProxyModel {
289 id: bookmarksSuggestions294 id: bookmarksSuggestions
290 limit: 4295 limit: 2
291 property string icon: "non-starred"296 readonly property string icon: "non-starred"
297 readonly property bool displayUrl: true
292 sourceModel: SuggestionsFilterModel {298 sourceModel: SuggestionsFilterModel {
293 sourceModel: browser.bookmarksModel299 sourceModel: browser.bookmarksModel
294 terms: suggestionsList.searchTerms300 terms: suggestionsList.searchTerms
@@ -296,6 +302,21 @@
296 }302 }
297 }303 }
298304
305 SearchSuggestions {
306 id: searchSuggestions
307 terms: suggestionsList.searchTerms
308 searchEngine: currentSearchEngine
309 active: chrome.activeFocus &&
310 !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+"))
311
312 function limit(number) {
313 var slice = results.slice(0, number)
314 slice.icon = 'search'
315 slice.displayUrl = false
316 return slice
317 }
318 }
319
299 onSelected: {320 onSelected: {
300 browser.currentWebview.url = url321 browser.currentWebview.url = url
301 browser.currentWebview.forceActiveFocus()322 browser.currentWebview.forceActiveFocus()
302323
=== added file 'src/app/webbrowser/SearchSuggestions.qml'
--- src/app/webbrowser/SearchSuggestions.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/SearchSuggestions.qml 2015-05-12 13:40:34 +0000
@@ -0,0 +1,78 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20import webbrowserapp.private 0.1
21
22Item {
23 property var terms
24 property SearchEngine searchEngine
25 property var results: []
26 property bool active: false
27
28 property var _request: new XMLHttpRequest()
29 onSearchEngineChanged: resetSearch()
30 onTermsChanged: resetSearch()
31 onActiveChanged: resetSearch()
32
33 Component.onCompleted: {
34 _request.onreadystatechange = function() {
35 if (_request.readyState === XMLHttpRequest.DONE) {
36 results = parseResponse(_request.responseText)
37 }
38 }
39 }
40
41 Timer {
42 id: limiter
43 interval: 250
44 onTriggered: {
45 if (_request && terms.length > 0 && searchEngine) {
46 var url = searchEngine.suggestionsUrlTemplate
47 url = url.replace("{searchTerms}", encodeURIComponent(terms.join(" ")))
48
49 _request.open("GET", url);
50 _request.send();
51 }
52 }
53 }
54
55 function parseResponse(response) {
56 try {
57 var data = JSON.parse(response)
58 } catch (error) {
59 return []
60 }
61
62 if (data.length > 1) {
63 return data[1].map(function(result) {
64 return {
65 title: result,
66 url: searchEngine.urlTemplate.replace("{searchTerms}",
67 encodeURIComponent(result))
68 }
69 })
70 } else return []
71 }
72
73 function resetSearch() {
74 results = []
75 if (_request) _request.abort()
76 if (active) limiter.restart()
77 }
78}
079
=== modified file 'src/app/webbrowser/Suggestion.qml'
--- src/app/webbrowser/Suggestion.qml 2015-04-16 16:42:04 +0000
+++ src/app/webbrowser/Suggestion.qml 2015-05-12 13:40:34 +0000
@@ -41,7 +41,7 @@
41 right: parent.right41 right: parent.right
42 verticalCenter: parent.verticalCenter42 verticalCenter: parent.verticalCenter
43 }43 }
44 height: label.height + subLabel.height44 height: subLabel.visible ? label.height + subLabel.height : icon.height
4545
46 Icon {46 Icon {
47 id: icon47 id: icon
@@ -56,11 +56,13 @@
56 Label {56 Label {
57 id: label57 id: label
58 anchors {58 anchors {
59 top: parent.top59 top: subLabel.visible ? parent.top : undefined
60 verticalCenter: subLabel.visible ? undefined : parent.verticalCenter
60 left: icon.right61 left: icon.right
61 leftMargin: units.gu(2)62 leftMargin: units.gu(2)
62 right: parent.right63 right: parent.right
63 }64 }
65
64 elide: Text.ElideRight66 elide: Text.ElideRight
65 }67 }
6668
@@ -74,6 +76,7 @@
74 }76 }
75 fontSize: "small"77 fontSize: "small"
76 elide: Text.ElideRight78 elide: Text.ElideRight
79 visible: text !== ""
77 }80 }
78 }81 }
7982
8083
=== modified file 'src/app/webbrowser/Suggestions.qml'
--- src/app/webbrowser/Suggestions.qml 2015-04-16 16:41:31 +0000
+++ src/app/webbrowser/Suggestions.qml 2015-05-12 13:40:34 +0000
@@ -24,8 +24,8 @@
2424
25 property var searchTerms25 property var searchTerms
26 property var models26 property var models
27 property int count: models.reduce(countItems, 0)27 readonly property int count: models.reduce(countItems, 0)
28 property alias contentHeight: suggestionsList.contentHeight28 readonly property alias contentHeight: suggestionsList.contentHeight
2929
30 signal selected(url url)30 signal selected(url url)
3131
@@ -43,25 +43,34 @@
4343
44 model: suggestions.models44 model: suggestions.models
45 delegate: Column {45 delegate: Column {
46 id: suggestionsSection
46 width: suggestionsList.width47 width: suggestionsList.width
47 height: childrenRect.height48 height: childrenRect.height
4849
50 property string icon: models[index].icon
51 property bool displayUrl: models[index].displayUrl
52 property int firstItemIndex: models.slice(0, index).reduce(countItems, 0)
53
49 Repeater {54 Repeater {
50 id: suggestionsSource55 id: suggestionsSource
51 model: modelData56 model: modelData
52 property int firstItemIndex: models.slice(0, index).reduce(countItems, 0)
5357
54 delegate: Suggestion {58 delegate: Suggestion {
59 id: suggestion
55 width: suggestionsList.width60 width: suggestionsList.width
56 showDivider: suggestionsSource.firstItemIndex + index <61 showDivider: suggestionsSection.firstItemIndex + index <
57 suggestions.count - 162 suggestions.count - 1
5863
59 title: highlightTerms(model.title)64 // Necessary to support both using objects inheriting from
60 subtitle: highlightTerms(model.url)65 // QAbstractItemModel and JS arrays as models, since they
61 url: model.url66 // expose their data differently
62 icon: suggestionsSource.model.icon67 property var item: (model.modelData) ? model.modelData : model
6368
64 onSelected: suggestions.selected(url)69 title: highlightTerms(item.title)
70 subtitle: suggestionsSection.displayUrl ? highlightTerms(item.url) : ""
71 icon: suggestionsSection.icon
72
73 onSelected: suggestions.selected(item.url)
65 }74 }
66 }75 }
67 }76 }
@@ -97,5 +106,7 @@
97 return highlighted106 return highlighted
98 }107 }
99108
100 function countItems(total, model) { return total + model.count }109 function countItems(total, model) {
110 return total + (model.hasOwnProperty("length") ? model.length : model.count);
111 }
101}112}
102113
=== modified file 'src/app/webbrowser/searchengine.cpp'
--- src/app/webbrowser/searchengine.cpp 2015-03-19 11:58:33 +0000
+++ src/app/webbrowser/searchengine.cpp 2015-05-12 13:40:34 +0000
@@ -30,6 +30,7 @@
30 , m_name(DEFAULT_SEARCH_NAME)30 , m_name(DEFAULT_SEARCH_NAME)
31 , m_description(DEFAULT_SEARCH_DESC)31 , m_description(DEFAULT_SEARCH_DESC)
32 , m_template(DEFAULT_SEARCH_TEMPLATE)32 , m_template(DEFAULT_SEARCH_TEMPLATE)
33 , m_suggestionsTemplate(DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE)
33{34{
34}35}
3536
@@ -47,6 +48,7 @@
47 m_name = DEFAULT_SEARCH_NAME;48 m_name = DEFAULT_SEARCH_NAME;
48 m_description = DEFAULT_SEARCH_DESC;49 m_description = DEFAULT_SEARCH_DESC;
49 m_template = DEFAULT_SEARCH_TEMPLATE;50 m_template = DEFAULT_SEARCH_TEMPLATE;
51 m_suggestionsTemplate = DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE;
5052
51 if (!filename.isEmpty()) {53 if (!filename.isEmpty()) {
52 QString filepath = QStandardPaths::locate(QStandardPaths::DataLocation,54 QString filepath = QStandardPaths::locate(QStandardPaths::DataLocation,
@@ -66,9 +68,13 @@
66 } else if (name == "Description") {68 } else if (name == "Description") {
67 m_description = parser.readElementText();69 m_description = parser.readElementText();
68 } else if (name == "Url") {70 } else if (name == "Url") {
69 if (parser.attributes().value("type") == "text/html") {71 QStringRef type = parser.attributes().value("type");
72 if (type == "text/html") {
70 m_template = parser.attributes().value("template").toString();73 m_template = parser.attributes().value("template").toString();
74 } else if (type == "application/x-suggestions+json") {
75 m_suggestionsTemplate = parser.attributes().value("template").toString();
71 }76 }
77
72 }78 }
73 }79 }
74 }80 }
@@ -79,6 +85,7 @@
79 Q_EMIT nameChanged();85 Q_EMIT nameChanged();
80 Q_EMIT descriptionChanged();86 Q_EMIT descriptionChanged();
81 Q_EMIT urlTemplateChanged();87 Q_EMIT urlTemplateChanged();
88 Q_EMIT suggestionsUrlTemplateChanged();
82 }89 }
83}90}
8491
@@ -96,3 +103,8 @@
96{103{
97 return m_template;104 return m_template;
98}105}
106
107const QString& SearchEngine::suggestionsUrlTemplate() const
108{
109 return m_suggestionsTemplate;
110}
99111
=== modified file 'src/app/webbrowser/searchengine.h'
--- src/app/webbrowser/searchengine.h 2015-03-19 11:58:33 +0000
+++ src/app/webbrowser/searchengine.h 2015-05-12 13:40:34 +0000
@@ -31,6 +31,7 @@
31 Q_PROPERTY(QString name READ name NOTIFY nameChanged)31 Q_PROPERTY(QString name READ name NOTIFY nameChanged)
32 Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)32 Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
33 Q_PROPERTY(QString urlTemplate READ urlTemplate NOTIFY urlTemplateChanged)33 Q_PROPERTY(QString urlTemplate READ urlTemplate NOTIFY urlTemplateChanged)
34 Q_PROPERTY(QString suggestionsUrlTemplate READ suggestionsUrlTemplate NOTIFY suggestionsUrlTemplateChanged)
3435
35public:36public:
36 SearchEngine(QObject* parent=0);37 SearchEngine(QObject* parent=0);
@@ -41,18 +42,21 @@
41 const QString& name() const;42 const QString& name() const;
42 const QString& description() const;43 const QString& description() const;
43 const QString& urlTemplate() const;44 const QString& urlTemplate() const;
45 const QString& suggestionsUrlTemplate() const;
4446
45Q_SIGNALS:47Q_SIGNALS:
46 void filenameChanged() const;48 void filenameChanged() const;
47 void nameChanged() const;49 void nameChanged() const;
48 void descriptionChanged() const;50 void descriptionChanged() const;
49 void urlTemplateChanged() const;51 void urlTemplateChanged() const;
52 void suggestionsUrlTemplateChanged() const;
5053
51private:54private:
52 QString m_filename;55 QString m_filename;
53 QString m_name;56 QString m_name;
54 QString m_description;57 QString m_description;
55 QString m_template;58 QString m_template;
59 QString m_suggestionsTemplate;
56};60};
5761
58#endif // __SEARCH_ENGINE_H__62#endif // __SEARCH_ENGINE_H__
5963
=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-05-12 13:40:34 +0000
+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-05-12 13:40:34 +0000
@@ -166,8 +166,8 @@
166 self.assertThat(lambda: len(self.main_window.get_webviews()),166 self.assertThat(lambda: len(self.main_window.get_webviews()),
167 Eventually(Equals(count)))167 Eventually(Equals(count)))
168168
169 def ping_server(self):169 def ping_server(self, server):
170 url = "http://localhost:{}/ping".format(self.server.port)170 url = "http://localhost:{}/ping".format(server.port)
171 ping = urllib.request.urlopen(url)171 ping = urllib.request.urlopen(url)
172 self.assertThat(ping.read(), Equals(b"pong"))172 self.assertThat(ping.read(), Equals(b"pong"))
173173
@@ -185,12 +185,12 @@
185 """185 """
186186
187 def setUp(self):187 def setUp(self):
188 self.server = http_server.HTTPServerInAThread()188 self.http_server = http_server.HTTPServerInAThread()
189 self.ping_server()189 self.ping_server(self.http_server)
190 self.addCleanup(self.server.cleanup)190 self.addCleanup(self.http_server.cleanup)
191 self.useFixture(fixtures.EnvironmentVariable(191 self.useFixture(fixtures.EnvironmentVariable(
192 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES',192 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES',
193 "MAP test:80 localhost:{}".format(self.server.port)))193 "MAP test:80 localhost:{}".format(self.http_server.port)))
194 self.base_url = "http://test"194 self.base_url = "http://test"
195 self.url = self.base_url + "/test1"195 self.url = self.base_url + "/test1"
196 self.ARGS = self.ARGS + [self.url]196 self.ARGS = self.ARGS + [self.url]
197197
=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-03-20 15:08:55 +0000
+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-05-12 13:40:34 +0000
@@ -7,6 +7,7 @@
7# by the Free Software Foundation.7# by the Free Software Foundation.
88
9import http.server as http9import http.server as http
10import json
10import logging11import logging
11import threading12import threading
12import time13import time
@@ -19,6 +20,7 @@
19 """20 """
20 A custom HTTP request handler that serves GET resources.21 A custom HTTP request handler that serves GET resources.
21 """22 """
23 suggestions_data = {}
2224
23 def make_html(self, title, body):25 def make_html(self, title, body):
24 html = "<html><title>{}</title><body>{}</body></html>"26 html = "<html><title>{}</title><body>{}</body></html>"
@@ -130,6 +132,14 @@
130 html += '<div style="height: 100%"></div>'132 html += '<div style="height: 100%"></div>'
131 html += '</a></body></html>'133 html += '</a></body></html>'
132 self.send_html(html)134 self.send_html(html)
135 elif self.path.startswith("/suggest"):
136 self.send_response(200)
137 self.send_header("Content-Type", "text/x-suggestions+json")
138 self.end_headers()
139 query = self.path[len("/suggest?q="):]
140 if query in self.suggestions_data:
141 suggestions = self.suggestions_data[query]
142 self.wfile.write(json.dumps(suggestions).encode())
133 else:143 else:
134 self.send_error(404)144 self.send_error(404)
135145
@@ -145,10 +155,13 @@
145 """155 """
146 A simple custom HTTP server run in a separate thread.156 A simple custom HTTP server run in a separate thread.
147 """157 """
158 def set_suggestions_data(self, data):
159 self.handler.suggestions_data = data
148160
149 def __init__(self):161 def __init__(self):
150 # port == 0 will assign a random free port162 # port == 0 will assign a random free port
151 self.server = http.HTTPServer(("", 0), HTTPRequestHandler)163 self.handler = HTTPRequestHandler
164 self.server = http.HTTPServer(("", 0), self.handler)
152 self.server.allow_reuse_address = True165 self.server.allow_reuse_address = True
153 self.server_thread = threading.Thread(target=self.server.serve_forever)166 self.server_thread = threading.Thread(target=self.server.serve_forever)
154 self.server_thread.start()167 self.server_thread.start()
155168
=== modified file 'tests/autopilot/webbrowser_app/tests/test_suggestions.py'
--- tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-04-27 03:23:00 +0000
+++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-05-12 13:40:34 +0000
@@ -23,6 +23,7 @@
23from autopilot.matchers import Eventually23from autopilot.matchers import Eventually
2424
25from webbrowser_app.tests import StartOpenRemotePageTestCaseBase25from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
26from . import http_server
2627
2728
28class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase):29class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase):
@@ -31,6 +32,7 @@
3132
32 def setUp(self):33 def setUp(self):
33 self.create_temporary_profile()34 self.create_temporary_profile()
35
34 self.populate_history()36 self.populate_history()
35 self.populate_bookmarks()37 self.populate_bookmarks()
36 super(PrepopulatedDatabaseTestCaseBase, self).setUp()38 super(PrepopulatedDatabaseTestCaseBase, self).setUp()
@@ -56,7 +58,6 @@
56 "Ubuntu (philosophy) - Wikipedia, the free encyclopedia"),58 "Ubuntu (philosophy) - Wikipedia, the free encyclopedia"),
57 (search_uri.format("example"), "google.com",59 (search_uri.format("example"), "google.com",
58 "example - Google Search"),60 "example - Google Search"),
59 ("http://example.iana.org/", "iana.org", "Example Domain"),
60 ("http://www.iana.org/domains/special", "iana.org",61 ("http://www.iana.org/domains/special", "iana.org",
61 "IANA — Special Use Domains"),62 "IANA — Special Use Domains"),
62 ("http://doc.qt.io/qt-5/qtqml-index.html", "qt.io",63 ("http://doc.qt.io/qt-5/qtqml-index.html", "qt.io",
@@ -91,8 +92,6 @@
91 "Samarium - Element Information"),92 "Samarium - Element Information"),
92 ("http://en.wikipedia.org/wiki/Linux",93 ("http://en.wikipedia.org/wiki/Linux",
93 "Linux - Wikipedia, the free encyclopedia"),94 "Linux - Wikipedia, the free encyclopedia"),
94 ("https://www.linux.com/",
95 "Linux.com | The source for Linux information"),
96 ("http://doc.qt.io/qt-5/qtqml-index.html",95 ("http://doc.qt.io/qt-5/qtqml-index.html",
97 "Qt QML 5.4 - Qt Documentation")96 "Qt QML 5.4 - Qt Documentation")
98 ]97 ]
@@ -112,10 +111,54 @@
112111
113 """Test the address bar suggestions (based on history and bookmarks)."""112 """Test the address bar suggestions (based on history and bookmarks)."""
114113
114 def setup_suggestions_source(self, server):
115 search_engines_path = os.path.join(self.data_location, "searchengines")
116 os.makedirs(search_engines_path, exist_ok=True)
117 with open(os.path.join(search_engines_path, "test.xml"), "w") as f:
118 f.write("""
119 <OpenSearchDescription>
120 <Url type="application/x-suggestions+json"
121 template="http://localhost:{}/suggest?q={searchTerms}"/>
122 <Url type="text/html"
123 template="http://aserver.somewhere/search?q={searchTerms}"/>
124 </OpenSearchDescription>
125 """.replace("{}", str(server.port)))
126
127 with open(os.path.join(self.config_location, "webbrowser-app.conf"),
128 "w") as f:
129 f.write("""
130 [General]
131 searchEngine=test
132 """)
133 server.set_suggestions_data({
134 "high": ["high", ["highlight"]],
135 "foo": ["foo", ["food", "foot", "fool", "foobar", "foo five"]],
136 "QML": ["QML", ["qt qml", "qml documentation", "qml rocks"]]
137 })
138
115 def setUp(self):139 def setUp(self):
140 self.suggest_http_server = http_server.HTTPServerInAThread()
141 self.ping_server(self.suggest_http_server)
142 self.addCleanup(self.suggest_http_server.cleanup)
143
144 self.create_temporary_profile()
145 self.setup_suggestions_source(self.suggest_http_server)
146
116 super(TestSuggestions, self).setUp()147 super(TestSuggestions, self).setUp()
148
117 self.address_bar = self.main_window.address_bar149 self.address_bar = self.main_window.address_bar
118150
151 def highlight_term(self, text, term):
152 parts = text.split(term)
153 if len(parts) < 2:
154 return text
155 else:
156 pattern = '<html>{}{}{}</html>'
157 return pattern.format(parts[0], self.highlight(term), parts[1])
158
159 def highlight(self, text):
160 return '<b><font color="#dd4814">{}</font></b>'.format(text)
161
119 def assert_suggestions_eventually_shown(self):162 def assert_suggestions_eventually_shown(self):
120 suggestions = self.main_window.get_suggestions()163 suggestions = self.main_window.get_suggestions()
121 self.assertThat(suggestions.opacity, Eventually(Equals(1)))164 self.assertThat(suggestions.opacity, Eventually(Equals(1)))
@@ -136,38 +179,47 @@
136179
137 def test_list_of_suggestions_case_insensitive(self):180 def test_list_of_suggestions_case_insensitive(self):
138 suggestions = self.main_window.get_suggestions()181 suggestions = self.main_window.get_suggestions()
139 self.address_bar.write('xaMPL')182 self.address_bar.write('SpEciAl')
140 self.assertThat(suggestions.count, Eventually(Equals(2)))183 self.assertThat(suggestions.count, Eventually(Equals(1)))
141184
142 def test_list_of_suggestions_history_limits(self):185 def test_list_of_suggestions_history_limits(self):
143 suggestions = self.main_window.get_suggestions()186 suggestions = self.main_window.get_suggestions()
144 self.address_bar.write('ubuntu')187 self.address_bar.write('ubuntu')
145 self.assert_suggestions_eventually_shown()188 self.assert_suggestions_eventually_shown()
146 self.assertThat(suggestions.count, Eventually(Equals(4)))189 self.assertThat(suggestions.count, Eventually(Equals(2)))
147 self.address_bar.write('bleh', clear=False)190 self.address_bar.write('bleh', clear=False)
148 self.assertThat(suggestions.count, Eventually(Equals(0)))191 self.assertThat(suggestions.count, Eventually(Equals(0)))
149 self.address_bar.write('iana')192 self.address_bar.write('iana')
150 self.assertThat(suggestions.count, Eventually(Equals(2)))193 self.assertThat(suggestions.count, Eventually(Equals(1)))
151194
152 def test_list_of_suggestions_bookmark_limits(self):195 def test_list_of_suggestions_bookmark_limits(self):
153 suggestions = self.main_window.get_suggestions()196 suggestions = self.main_window.get_suggestions()
154 self.address_bar.write('element')197 self.address_bar.write('element')
155 self.assert_suggestions_eventually_shown()198 self.assert_suggestions_eventually_shown()
199 self.assertThat(suggestions.count, Eventually(Equals(2)))
200 self.address_bar.write('bleh', clear=False)
201 self.assertThat(suggestions.count, Eventually(Equals(0)))
202 self.address_bar.write('linux')
203 self.assertThat(suggestions.count, Eventually(Equals(1)))
204
205 def test_list_of_suggestions_search_limits(self):
206 suggestions = self.main_window.get_suggestions()
207 self.address_bar.write('foo')
208 self.assert_suggestions_eventually_shown()
156 self.assertThat(suggestions.count, Eventually(Equals(4)))209 self.assertThat(suggestions.count, Eventually(Equals(4)))
157 self.address_bar.write('bleh', clear=False)210 self.address_bar.write('bleh', clear=False)
158 self.assertThat(suggestions.count, Eventually(Equals(0)))211 self.assertThat(suggestions.count, Eventually(Equals(0)))
159 self.address_bar.write('linux')
160 self.assertThat(suggestions.count, Eventually(Equals(2)))
161212
162 def test_list_of_suggestions_order(self):213 def test_list_of_suggestions_order(self):
163 suggestions = self.main_window.get_suggestions()214 suggestions = self.main_window.get_suggestions()
164 self.address_bar.write('QML')215 self.address_bar.write('QML')
165 self.assert_suggestions_eventually_shown()216 self.assert_suggestions_eventually_shown()
166 self.assertThat(suggestions.count, Eventually(Equals(2)))217 self.assertThat(suggestions.count, Eventually(Equals(5)))
167 entries = suggestions.get_ordered_entries()218 entries = suggestions.get_ordered_entries()
168 self.assertThat(len(entries), Equals(2))219 self.assertThat(len(entries), Equals(5))
169 self.assertThat(entries[0].icon, Equals("history"))220 self.assertThat(entries[0].icon, Equals("history"))
170 self.assertThat(entries[1].icon, Equals("non-starred"))221 self.assertThat(entries[1].icon, Equals("non-starred"))
222 self.assertThat(entries[2].icon, Equals("search"))
171223
172 def test_clear_address_bar_dismisses_suggestions(self):224 def test_clear_address_bar_dismisses_suggestions(self):
173 self.address_bar.focus()225 self.address_bar.focus()
@@ -208,21 +260,29 @@
208 self.address_bar.focus()260 self.address_bar.focus()
209 self.assert_suggestions_eventually_shown()261 self.assert_suggestions_eventually_shown()
210 self.address_bar.clear()262 self.address_bar.clear()
211 self.address_bar.write('ubuntu')263 self.address_bar.write('linux')
212 self.assert_suggestions_eventually_shown()264 self.assert_suggestions_eventually_shown()
213 self.assertThat(suggestions.count, Eventually(Equals(4)))265 self.assertThat(suggestions.count, Eventually(Equals(1)))
214 entries = suggestions.get_ordered_entries()266 entries = suggestions.get_ordered_entries()
215 highlight = '<b><font color="#dd4814">Ubuntu</font></b>'267 url = "http://en.wikipedia.org/wiki/Linux"
216 url = "http://en.wikipedia.org/wiki/{}_(operating_system)"268 self.pointing_device.click_object(entries[0])
217 url = url.format(highlight)269 webview = self.main_window.get_current_webview()
218 entries = [entry for entry in entries if url in entry.subtitle]270 self.assertThat(webview.url, Eventually(Equals(url)))
219 entry = entries[0] if len(entries) == 1 else None271 self.assert_suggestions_eventually_hidden()
220 self.assertIsNotNone(entry)272
221 self.pointing_device.click_object(entry)273 def test_select_search_suggestion(self):
222 webview = self.main_window.get_current_webview()274 suggestions = self.main_window.get_suggestions()
223 url = "wikipedia.org/wiki/Ubuntu_(operating_system)"275 self.address_bar.focus()
276 self.assert_suggestions_eventually_shown()
277 self.address_bar.clear()
278 self.address_bar.write('high')
279 self.assert_suggestions_eventually_shown()
280 self.assertThat(suggestions.count, Eventually(Equals(1)))
281 entries = suggestions.get_ordered_entries()
282 self.pointing_device.click_object(entries[0])
283 webview = self.main_window.get_current_webview()
284 url = "aserver.somewhere/search?q=highlight"
224 self.assertThat(webview.url, Eventually(Contains(url)))285 self.assertThat(webview.url, Eventually(Contains(url)))
225 self.assert_suggestions_eventually_hidden()
226286
227 def test_special_characters(self):287 def test_special_characters(self):
228 self.address_bar.clear()288 self.address_bar.clear()
@@ -231,6 +291,15 @@
231 suggestions = self.main_window.get_suggestions()291 suggestions = self.main_window.get_suggestions()
232 self.assertThat(suggestions.count, Eventually(Equals(1)))292 self.assertThat(suggestions.count, Eventually(Equals(1)))
233 entry = suggestions.get_ordered_entries()[0]293 entry = suggestions.get_ordered_entries()[0]
234 highlight = '<b><font color="#dd4814">(phil</font></b>'294 url = "http://en.wikipedia.org/wiki/Ubuntu_(philosophy)"
235 url = "http://en.wikipedia.org/wiki/Ubuntu_{}osophy)".format(highlight)295 highlighted = self.highlight_term(url, "(phil")
236 self.assertThat(entry.subtitle, Contains(url))296 self.assertThat(entry.subtitle, Equals(highlighted))
297
298 def test_search_suggestions(self):
299 self.address_bar.write('high')
300 suggestions = self.main_window.get_suggestions()
301 self.assertThat(suggestions.count, Eventually(Equals(1)))
302 entries = suggestions.get_ordered_entries()
303 highlighted = self.highlight_term("highlight", "high")
304 self.assertThat(entries[0].title, Equals(highlighted))
305 self.assertThat(entries[0].subtitle, Equals(''))

Subscribers

People subscribed via source and target branches

to status/vote changes: