Merge lp:~osomon/webbrowser-app/systemwide-search-engines into lp:webbrowser-app

Proposed by Olivier Tilloy on 2015-04-29
Status: Merged
Approved by: Olivier Tilloy on 2015-05-15
Approved revision: 997
Merged at revision: 1012
Proposed branch: lp:~osomon/webbrowser-app/systemwide-search-engines
Merge into: lp:webbrowser-app
Diff against target: 1134 lines (+752/-69)
22 files modified
debian/control (+1/-0)
src/app/config.h.in (+0/-9)
src/app/webbrowser/Browser.qml (+1/-0)
src/app/webbrowser/CMakeLists.txt (+4/-0)
src/app/webbrowser/SearchEngines.qml (+91/-0)
src/app/webbrowser/SettingsPage.qml (+13/-10)
src/app/webbrowser/searchengine.cpp (+91/-49)
src/app/webbrowser/searchengine.h (+13/-0)
src/app/webbrowser/searchengines/bing.xml (+6/-0)
src/app/webbrowser/searchengines/duckduckgo.xml (+6/-0)
src/app/webbrowser/searchengines/google.xml (+6/-0)
src/app/webbrowser/searchengines/wikipedia.xml (+6/-0)
src/app/webbrowser/searchengines/yahoo.xml (+6/-0)
src/app/webbrowser/webbrowser-app.cpp (+7/-0)
tests/autopilot/webbrowser_app/emulators/browser.py (+7/-0)
tests/autopilot/webbrowser_app/tests/test_settings.py (+43/-1)
tests/unittests/CMakeLists.txt (+1/-0)
tests/unittests/qml/CMakeLists.txt (+1/-0)
tests/unittests/qml/tst_QmlTests.cpp (+81/-0)
tests/unittests/qml/tst_SearchEngines.qml (+117/-0)
tests/unittests/search-engine/CMakeLists.txt (+9/-0)
tests/unittests/search-engine/tst_SearchEngineTests.cpp (+242/-0)
To merge this branch: bzr merge lp:~osomon/webbrowser-app/systemwide-search-engines
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing on 2015-05-14
Ugo Riboni (community) 2015-04-29 Needs Fixing on 2015-05-06
Review via email: mp+257830@code.launchpad.net

Commit Message

Look for custom search engines description files in several locations.
This adds a build dependency on qml-module-qt-labs-folderlistmodel, to run unit tests at package construction time.

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

Running the new AP tests gives this error:

Traceback (most recent call last):
  File "/home/nerochiaro/projects/phone/webbrowser-app/tests/autopilot/webbrowser_app/tests/test_settings.py", line 45, in test_open_close_searchengine_page
    self.assertThat(old_engine, NotEquals(""))
  File "/usr/lib/python3/dist-packages/testtools/testcase.py", line 423, in assertThat
    raise mismatch_error
testtools.matchers._impl.MismatchError: '' == ''

More info on my environment:
I have one file in ~/.local/share/webbrowser-app/searchengines called bing.xml and completely empty
I built the branch in-tree and I am running the tests from tests/autopilot

This seems to indicate that the browser is reading the files from ~/.local/share/webbrowser-app which should not happen during tests.

First verify why this makes the tests fail. I don't see why it should, so I suspect a bug somewhere.

Second, we are testing by doing things in the current user directory. This is not safe. We should add a test mode command line switch to the app which calls QStandardPaths::setTestModeEnabled(true), so that QT will return special test paths for all standard paths. Then run our tests in this mode.

Once that is done we should ideally set up more AP tests to verify the following features:
- test that the names and descriptions of the engines correspond to what is read from the files
- test overriding an engine by placing a file of the same name, and verifying that the search url, description and name are correctly overriden
- test overriding an engine by placing an empty file of the same name, then verifying that the engine is removed
- verify a few of the above by manipulating the files in the list while app is running and verifying that the settings page picks these up the next time it is opened.

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

> Running the new AP tests gives this error:
>
> Traceback (most recent call last):
> File "/home/nerochiaro/projects/phone/webbrowser-
> app/tests/autopilot/webbrowser_app/tests/test_settings.py", line 45, in
> test_open_close_searchengine_page
> self.assertThat(old_engine, NotEquals(""))
> File "/usr/lib/python3/dist-packages/testtools/testcase.py", line 423, in
> assertThat
> raise mismatch_error
> testtools.matchers._impl.MismatchError: '' == ''
>
> More info on my environment:
> I have one file in ~/.local/share/webbrowser-app/searchengines called bing.xml
> and completely empty
> I built the branch in-tree and I am running the tests from tests/autopilot

This test passes here, tests ran in the same conditions. Can you reliably reproduce the failure?

> This seems to indicate that the browser is reading the files from
> ~/.local/share/webbrowser-app which should not happen during tests.
>
> First verify why this makes the tests fail. I don't see why it should, so I
> suspect a bug somewhere.
>
> Second, we are testing by doing things in the current user directory. This is
> not safe. We should add a test mode command line switch to the app which calls
> QStandardPaths::setTestModeEnabled(true), so that QT will return special test
> paths for all standard paths. Then run our tests in this mode.

This will be addressed by https://code.launchpad.net/~osomon/webbrowser-app/autopilot-temp-profile/+merge/257490, although the implementation differs from your suggestion, but achieves the same result. It’s in a silo, awaiting QA verification for landing.

> Once that is done we should ideally set up more AP tests to verify the
> following features:
> - test that the names and descriptions of the engines correspond to what is
> read from the files

This is already unit-tested.

> - test overriding an engine by placing a file of the same name, and verifying
> that the search url, description and name are correctly overriden
> - test overriding an engine by placing an empty file of the same name, then
> verifying that the engine is removed

I’ll add unit tests for the above two suggestions, thanks.

> - verify a few of the above by manipulating the files in the list while app is
> running and verifying that the settings page picks these up the next time it
> is opened.

Although a valid test, that sounds a bit overkill for an autopilot test. I’ll see if I can write a QML test for it though.

990. By Olivier Tilloy on 2015-05-08

Add a couple of unit tests for SearchEngine.

991. By Olivier Tilloy on 2015-05-08

Add QML tests for the SearchEngines component.

992. By Olivier Tilloy on 2015-05-08

Improve the SearchEngines component to update the list of engines whenever a description file is added/removed.

993. By Olivier Tilloy on 2015-05-08

Remove an unused test helper.

994. By Olivier Tilloy on 2015-05-08

Add missing build dependency.

995. By Olivier Tilloy on 2015-05-14

Merge the latest changes from trunk and resolve a bunch of conflicts.

996. By Olivier Tilloy on 2015-05-14

Also test the suggestionsUrlTemplate property in the unit tests.

997. By Olivier Tilloy on 2015-05-14

Cosmetics.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2015-05-11 19:13:32 +0000
3+++ debian/control 2015-05-14 07:06:15 +0000
4@@ -10,6 +10,7 @@
5 libqt5sql5-sqlite,
6 python3-all,
7 python3-flake8,
8+ qml-module-qt-labs-folderlistmodel,
9 qml-module-qtquick2 (>= 5.4) | qtdeclarative5-qtquick2-plugin (>= 5.4),
10 qml-module-qttest | qtdeclarative5-test-plugin,
11 qt5-default,
12
13=== modified file 'src/app/config.h.in'
14--- src/app/config.h.in 2015-04-23 14:03:49 +0000
15+++ src/app/config.h.in 2015-05-14 07:06:15 +0000
16@@ -26,15 +26,6 @@
17 #define APP_ID "webbrowser-app"
18 #define REMOTE_INSPECTOR_PORT 9221
19
20-#define DEFAULT_SEARCH_NAME "Google Search"
21-#define DEFAULT_SEARCH_DESC "Use google.com to search the Web"
22-#define DEFAULT_SEARCH_TEMPLATE "https://google.com/search?client=ubuntu&q={searchTerms}&ie=utf-8&oe=utf-8"
23-
24-// This API is not official, but it is what Google products and everyone else
25-// use to implement search suggestions. The "client=firefox" is there to request
26-// JSON output.
27-#define DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE "http://suggestqueries.google.com/complete/search?client=firefox&q={searchTerms}"
28-
29 inline bool isRunningInstalled()
30 {
31 static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath());
32
33=== modified file 'src/app/webbrowser/Browser.qml'
34--- src/app/webbrowser/Browser.qml 2015-05-12 15:19:32 +0000
35+++ src/app/webbrowser/Browser.qml 2015-05-14 07:06:15 +0000
36@@ -176,6 +176,7 @@
37
38 SearchEngine {
39 id: currentSearchEngine
40+ searchPaths: searchEnginesSearchPaths
41 filename: settings.searchEngine
42 }
43
44
45=== modified file 'src/app/webbrowser/CMakeLists.txt'
46--- src/app/webbrowser/CMakeLists.txt 2015-04-22 09:03:56 +0000
47+++ src/app/webbrowser/CMakeLists.txt 2015-05-14 07:06:15 +0000
48@@ -54,6 +54,10 @@
49 DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser
50 FILES_MATCHING PATTERN *.qml)
51
52+install(DIRECTORY searchengines
53+ DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser
54+ FILES_MATCHING PATTERN *.xml)
55+
56 configure_file(${DESKTOP_FILE}.in.in ${DESKTOP_FILE}.in @ONLY)
57 add_custom_target(${DESKTOP_FILE} ALL
58 COMMENT "Merging translations into ${DESKTOP_FILE}"
59
60=== added file 'src/app/webbrowser/SearchEngines.qml'
61--- src/app/webbrowser/SearchEngines.qml 1970-01-01 00:00:00 +0000
62+++ src/app/webbrowser/SearchEngines.qml 2015-05-14 07:06:15 +0000
63@@ -0,0 +1,91 @@
64+/*
65+ * Copyright 2015 Canonical Ltd.
66+ *
67+ * This file is part of webbrowser-app.
68+ *
69+ * webbrowser-app is free software; you can redistribute it and/or modify
70+ * it under the terms of the GNU General Public License as published by
71+ * the Free Software Foundation; version 3.
72+ *
73+ * webbrowser-app is distributed in the hope that it will be useful,
74+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
75+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
76+ * GNU General Public License for more details.
77+ *
78+ * You should have received a copy of the GNU General Public License
79+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
80+ */
81+
82+import QtQuick 2.0
83+import Qt.labs.folderlistmodel 2.1
84+import webbrowserapp.private 0.1
85+
86+Item {
87+ id: searchEngines
88+
89+ property var searchPaths: []
90+ readonly property var engines: ListModel {}
91+
92+ Repeater {
93+ id: repeater
94+ model: searchEngines.searchPaths
95+ delegate: Item {
96+ property var folder: FolderListModel {
97+ folder: modelData
98+ showDirs: false
99+ nameFilters: ["*.xml"]
100+ sortField: FolderListModel.Name
101+ onCountChanged: delayedPopulation.restart()
102+ }
103+ }
104+ onItemRemoved: delayedPopulation.restart()
105+ }
106+
107+ QtObject {
108+ id: internal
109+
110+ function populateModel() {
111+ engines.clear()
112+ for (var i = repeater.count - 1; i >= 0; --i) {
113+ var folder = repeater.itemAt(i).folder
114+ for (var j = 0; j < folder.count; ++j) {
115+ var name = folder.get(j, "fileBaseName")
116+ var engine = searchEngineComponent.createObject(null, {filename: name})
117+ var found = -1
118+ for (var k = 0; k < engines.count; ++k) {
119+ if (engines.get(k).filename == name) {
120+ found = k
121+ break
122+ }
123+ }
124+ if (engine.valid && (found == -1)) {
125+ var insertIndex = 0
126+ for (var k = 0; k < engines.count; ++k) {
127+ if (engines.get(k).filename > name) {
128+ insertIndex = k
129+ break
130+ }
131+ }
132+ engines.insert(k, {"filename": name})
133+ } else if (!engine.valid && (found > -1)) {
134+ engines.remove(found)
135+ }
136+ }
137+ }
138+ }
139+ }
140+
141+ Timer {
142+ id: delayedPopulation
143+ interval: 50
144+ onTriggered: internal.populateModel()
145+ }
146+
147+ Component {
148+ id: searchEngineComponent
149+
150+ SearchEngine {
151+ searchPaths: searchEngines.searchPaths
152+ }
153+ }
154+}
155
156=== modified file 'src/app/webbrowser/SettingsPage.qml'
157--- src/app/webbrowser/SettingsPage.qml 2015-04-22 10:25:56 +0000
158+++ src/app/webbrowser/SettingsPage.qml 2015-05-14 07:06:15 +0000
159@@ -17,7 +17,6 @@
160 */
161
162 import QtQuick 2.0
163-import Qt.labs.folderlistmodel 2.1
164 import Qt.labs.settings 1.0
165 import Ubuntu.Components 1.1
166 import Ubuntu.Components.Popups 1.0
167@@ -40,12 +39,9 @@
168 color: "#f6f6f6"
169 }
170
171- FolderListModel {
172- id: searchEngineFolder
173- folder: dataLocation +"/searchengines"
174- showDirs: false
175- nameFilters: ["*.xml"]
176- sortField: FolderListModel.Name
177+ SearchEngines {
178+ id: searchEngines
179+ searchPaths: searchEnginesSearchPaths
180 }
181
182 SettingsPageHeader {
183@@ -74,14 +70,18 @@
184 width: parent.width
185
186 ListItem.Subtitled {
187+ objectName: "searchengine"
188+
189 SearchEngine {
190 id: currentSearchEngine
191+ searchPaths: searchEngines.searchPaths
192 filename: settingsObject.searchEngine
193 }
194+
195 text: i18n.tr("Search engine")
196 subText: currentSearchEngine.name
197
198- visible: searchEngineFolder.count > 1
199+ visible: searchEngines.engines.count > 1
200
201 onClicked: searchEngineComponent.createObject(subpageContainer);
202 }
203@@ -161,6 +161,7 @@
204
205 Item {
206 id: searchEngineItem
207+ objectName: "searchEnginePage"
208 anchors.fill: parent
209
210 Rectangle {
211@@ -183,12 +184,14 @@
212 bottom: parent.bottom
213 }
214
215- model: searchEngineFolder
216+ model: searchEngines.engines
217
218 delegate: ListItem.Standard {
219+ objectName: "searchEngineDelegate_" + index
220 SearchEngine {
221 id: searchEngineDelegate
222- filename: model.fileBaseName
223+ searchPaths: searchEngines.searchPaths
224+ filename: model.filename
225 }
226 text: searchEngineDelegate.name
227
228
229=== modified file 'src/app/webbrowser/searchengine.cpp'
230--- src/app/webbrowser/searchengine.cpp 2015-04-21 11:56:57 +0000
231+++ src/app/webbrowser/searchengine.cpp 2015-05-14 07:06:15 +0000
232@@ -17,21 +17,29 @@
233 */
234
235 // local
236-#include "config.h"
237 #include "searchengine.h"
238
239 // Qt
240+#include <QtCore/QDir>
241 #include <QtCore/QFile>
242-#include <QtCore/QStandardPaths>
243 #include <QtCore/QXmlStreamReader>
244
245 SearchEngine::SearchEngine(QObject* parent)
246 : QObject(parent)
247- , m_name(DEFAULT_SEARCH_NAME)
248- , m_description(DEFAULT_SEARCH_DESC)
249- , m_template(DEFAULT_SEARCH_TEMPLATE)
250- , m_suggestionsTemplate(DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE)
251-{
252+{}
253+
254+const QStringList& SearchEngine::searchPaths() const
255+{
256+ return m_searchPaths;
257+}
258+
259+void SearchEngine::setSearchPaths(const QStringList& searchPaths)
260+{
261+ if (searchPaths != m_searchPaths) {
262+ m_searchPaths = searchPaths;
263+ Q_EMIT searchPathsChanged();
264+ locateAndParseDescription();
265+ }
266 }
267
268 const QString& SearchEngine::filename() const
269@@ -44,48 +52,7 @@
270 if (filename != m_filename) {
271 m_filename = filename;
272 Q_EMIT filenameChanged();
273-
274- m_name = DEFAULT_SEARCH_NAME;
275- m_description = DEFAULT_SEARCH_DESC;
276- m_template = DEFAULT_SEARCH_TEMPLATE;
277- m_suggestionsTemplate = DEFAULT_SEARCH_SUGGESTIONS_TEMPLATE;
278-
279- if (!filename.isEmpty()) {
280- QString filepath = QStandardPaths::locate(QStandardPaths::DataLocation,
281- "searchengines/" + filename + ".xml");
282- if (!filepath.isEmpty()) {
283- QFile file(filepath);
284- if (file.open(QIODevice::ReadOnly)) {
285- // Parse OpenSearch description file
286- // (http://www.opensearch.org/Specifications/OpenSearch/1.1)
287- QXmlStreamReader parser(&file);
288- while (!parser.atEnd()) {
289- parser.readNext();
290- if (parser.isStartElement()) {
291- QStringRef name = parser.name();
292- if (name == "ShortName") {
293- m_name = parser.readElementText();
294- } else if (name == "Description") {
295- m_description = parser.readElementText();
296- } else if (name == "Url") {
297- QStringRef type = parser.attributes().value("type");
298- if (type == "text/html") {
299- m_template = parser.attributes().value("template").toString();
300- } else if (type == "application/x-suggestions+json") {
301- m_suggestionsTemplate = parser.attributes().value("template").toString();
302- }
303-
304- }
305- }
306- }
307- }
308- }
309- }
310-
311- Q_EMIT nameChanged();
312- Q_EMIT descriptionChanged();
313- Q_EMIT urlTemplateChanged();
314- Q_EMIT suggestionsUrlTemplateChanged();
315+ locateAndParseDescription();
316 }
317 }
318
319@@ -108,3 +75,78 @@
320 {
321 return m_suggestionsTemplate;
322 }
323+
324+bool SearchEngine::isValid() const
325+{
326+ return !m_searchPaths.isEmpty() &&
327+ !m_filename.isEmpty() &&
328+ !m_name.isEmpty() &&
329+ !m_template.isEmpty();
330+}
331+
332+void SearchEngine::locateAndParseDescription()
333+{
334+ QString filepath;
335+ if (!m_filename.isEmpty()) {
336+ Q_FOREACH(const QString& path, m_searchPaths) {
337+ QDir dir(path);
338+ QString filename = m_filename + ".xml";
339+ if (dir.exists(filename)) {
340+ filepath = dir.filePath(filename);
341+ break;
342+ }
343+ }
344+ }
345+
346+ QString oldName = m_name;
347+ m_name.clear();
348+ QString oldDescription = m_description;
349+ m_description.clear();
350+ QString oldTemplate = m_template;
351+ m_template.clear();
352+ QString oldSuggestionsTemplate = m_suggestionsTemplate;
353+ m_suggestionsTemplate.clear();
354+ bool wasValid = isValid();
355+
356+ if (!filepath.isEmpty()) {
357+ QFile file(filepath);
358+ if (file.open(QIODevice::ReadOnly)) {
359+ // Parse OpenSearch description file
360+ // (http://www.opensearch.org/Specifications/OpenSearch/1.1)
361+ QXmlStreamReader parser(&file);
362+ while (!parser.atEnd()) {
363+ parser.readNext();
364+ if (parser.isStartElement()) {
365+ QStringRef name = parser.name();
366+ if (name == "ShortName") {
367+ m_name = parser.readElementText();
368+ } else if (name == "Description") {
369+ m_description = parser.readElementText();
370+ } else if (name == "Url") {
371+ QStringRef type = parser.attributes().value("type");
372+ if (type == "text/html") {
373+ m_template = parser.attributes().value("template").toString();
374+ } else if (type == "application/x-suggestions+json") {
375+ m_suggestionsTemplate = parser.attributes().value("template").toString();
376+ }
377+ }
378+ }
379+ }
380+ }
381+ }
382+ if (m_name != oldName) {
383+ Q_EMIT nameChanged();
384+ }
385+ if (m_description != oldDescription) {
386+ Q_EMIT descriptionChanged();
387+ }
388+ if (m_template != oldTemplate) {
389+ Q_EMIT urlTemplateChanged();
390+ }
391+ if (m_suggestionsTemplate != oldSuggestionsTemplate) {
392+ Q_EMIT suggestionsUrlTemplateChanged();
393+ }
394+ if (isValid() != wasValid) {
395+ Q_EMIT validChanged();
396+ }
397+}
398
399=== modified file 'src/app/webbrowser/searchengine.h'
400--- src/app/webbrowser/searchengine.h 2015-04-21 11:56:57 +0000
401+++ src/app/webbrowser/searchengine.h 2015-05-14 07:06:15 +0000
402@@ -22,20 +22,26 @@
403 // Qt
404 #include <QtCore/QObject>
405 #include <QtCore/QString>
406+#include <QtCore/QStringList>
407
408 class SearchEngine : public QObject
409 {
410 Q_OBJECT
411
412+ Q_PROPERTY(QStringList searchPaths READ searchPaths WRITE setSearchPaths NOTIFY searchPathsChanged)
413 Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged)
414 Q_PROPERTY(QString name READ name NOTIFY nameChanged)
415 Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
416 Q_PROPERTY(QString urlTemplate READ urlTemplate NOTIFY urlTemplateChanged)
417 Q_PROPERTY(QString suggestionsUrlTemplate READ suggestionsUrlTemplate NOTIFY suggestionsUrlTemplateChanged)
418+ Q_PROPERTY(bool valid READ isValid NOTIFY validChanged)
419
420 public:
421 SearchEngine(QObject* parent=0);
422
423+ const QStringList& searchPaths() const;
424+ void setSearchPaths(const QStringList& searchPaths);
425+
426 const QString& filename() const;
427 void setFilename(const QString& filename);
428
429@@ -44,14 +50,21 @@
430 const QString& urlTemplate() const;
431 const QString& suggestionsUrlTemplate() const;
432
433+ bool isValid() const;
434+
435 Q_SIGNALS:
436+ void searchPathsChanged() const;
437 void filenameChanged() const;
438 void nameChanged() const;
439 void descriptionChanged() const;
440 void urlTemplateChanged() const;
441 void suggestionsUrlTemplateChanged() const;
442+ void validChanged() const;
443
444 private:
445+ void locateAndParseDescription();
446+
447+ QStringList m_searchPaths;
448 QString m_filename;
449 QString m_name;
450 QString m_description;
451
452=== added directory 'src/app/webbrowser/searchengines'
453=== added file 'src/app/webbrowser/searchengines/bing.xml'
454--- src/app/webbrowser/searchengines/bing.xml 1970-01-01 00:00:00 +0000
455+++ src/app/webbrowser/searchengines/bing.xml 2015-05-14 07:06:15 +0000
456@@ -0,0 +1,6 @@
457+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
458+ <ShortName>Bing</ShortName>
459+ <Description>Bing. Search by Microsoft.</Description>
460+ <Url type="text/html" template="https://www.bing.com/search?q={searchTerms}"/>
461+ <Url type="application/x-suggestions+json" template="https://www.bing.com/osjson.aspx?query={searchTerms}"/>
462+</OpenSearchDescription>
463
464=== added file 'src/app/webbrowser/searchengines/duckduckgo.xml'
465--- src/app/webbrowser/searchengines/duckduckgo.xml 1970-01-01 00:00:00 +0000
466+++ src/app/webbrowser/searchengines/duckduckgo.xml 2015-05-14 07:06:15 +0000
467@@ -0,0 +1,6 @@
468+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
469+ <ShortName>DuckDuckGo</ShortName>
470+ <Description>Search DuckDuckGo</Description>
471+ <Url type="text/html" template="https://duckduckgo.com/?q={searchTerms}"/>
472+ <Url type="application/x-suggestions+json" template="https://ac.duckduckgo.com/ac/?q={searchTerms}&amp;type=list"/>
473+</OpenSearchDescription>
474
475=== added file 'src/app/webbrowser/searchengines/google.xml'
476--- src/app/webbrowser/searchengines/google.xml 1970-01-01 00:00:00 +0000
477+++ src/app/webbrowser/searchengines/google.xml 2015-05-14 07:06:15 +0000
478@@ -0,0 +1,6 @@
479+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
480+ <ShortName>Google</ShortName>
481+ <Description>Google Search</Description>
482+ <Url type="text/html" template="https://google.com/search?client=ubuntu&amp;q={searchTerms}&amp;ie=utf-8&amp;oe=utf-8"/>
483+ <Url type="application/x-suggestions+json" template="https://www.google.com/complete/search?client=firefox&amp;q={searchTerms}"/>
484+</OpenSearchDescription>
485
486=== added file 'src/app/webbrowser/searchengines/wikipedia.xml'
487--- src/app/webbrowser/searchengines/wikipedia.xml 1970-01-01 00:00:00 +0000
488+++ src/app/webbrowser/searchengines/wikipedia.xml 2015-05-14 07:06:15 +0000
489@@ -0,0 +1,6 @@
490+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
491+ <ShortName>Wikipedia</ShortName>
492+ <Description>Wikipedia, the Free Encyclopedia</Description>
493+ <Url type="text/html" template="https://wikipedia.org/wiki/Special:Search?search={searchTerms}"/>
494+ <Url type="application/x-suggestions+json" template="https://wikipedia.org/w/api.php?action=opensearch&amp;search={searchTerms}"/>
495+</OpenSearchDescription>
496
497=== added file 'src/app/webbrowser/searchengines/yahoo.xml'
498--- src/app/webbrowser/searchengines/yahoo.xml 1970-01-01 00:00:00 +0000
499+++ src/app/webbrowser/searchengines/yahoo.xml 2015-05-14 07:06:15 +0000
500@@ -0,0 +1,6 @@
501+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
502+ <ShortName>Yahoo</ShortName>
503+ <Description>Yahoo Search</Description>
504+ <Url type="text/html" template="https://search.yahoo.com/yhs/search?ei=UTF-8&amp;p={searchTerms}"/>
505+ <Url type="application/x-suggestions+json" template="https://search.yahoo.com/sugg/ff?output=fxjson&amp;command={searchTerms}"/>
506+</OpenSearchDescription>
507
508=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
509--- src/app/webbrowser/webbrowser-app.cpp 2015-05-01 19:28:53 +0000
510+++ src/app/webbrowser/webbrowser-app.cpp 2015-05-14 07:06:15 +0000
511@@ -78,12 +78,19 @@
512 qmlRegisterType<SuggestionsFilterModel>(uri, 0, 1, "SuggestionsFilterModel");
513
514 if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml")) {
515+ QStringList searchEnginesSearchPaths;
516+ searchEnginesSearchPaths << QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/searchengines";
517+ searchEnginesSearchPaths << UbuntuBrowserDirectory() + "/webbrowser/searchengines";
518+ m_engine->rootContext()->setContextProperty("searchEnginesSearchPaths", searchEnginesSearchPaths);
519+
520 m_window->setProperty("newSession", m_arguments.contains("--new-session"));
521+
522 QVariantList urls;
523 Q_FOREACH(const QUrl& url, this->urls()) {
524 urls.append(url);
525 }
526 m_window->setProperty("urls", urls);
527+
528 m_component->completeCreate();
529 return true;
530 } else {
531
532=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
533--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-05-06 22:37:45 +0000
534+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-05-14 07:06:15 +0000
535@@ -247,6 +247,13 @@
536 def get_header(self):
537 return self.select_single(SettingsPageHeader)
538
539+ def get_searchengine_entry(self):
540+ return self.select_single("Subtitled", objectName="searchengine")
541+
542+ def get_searchengine_page(self):
543+ return self.wait_select_single("QQuickItem",
544+ objectName="searchEnginePage")
545+
546 def get_homepage_entry(self):
547 return self.select_single("Subtitled", objectName="homepage")
548
549
550=== modified file 'tests/autopilot/webbrowser_app/tests/test_settings.py'
551--- tests/autopilot/webbrowser_app/tests/test_settings.py 2015-04-07 13:36:13 +0000
552+++ tests/autopilot/webbrowser_app/tests/test_settings.py 2015-05-14 07:06:15 +0000
553@@ -18,7 +18,7 @@
554
555 from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
556
557-from testtools.matchers import Equals, NotEquals
558+from testtools.matchers import Equals, GreaterThan, NotEquals
559 from autopilot.matchers import Eventually
560 from autopilot.platform import model
561
562@@ -38,6 +38,44 @@
563 settings.get_header().click_back_button()
564 settings.wait_until_destroyed()
565
566+ def test_open_close_searchengine_page(self):
567+ settings = self.open_settings()
568+ searchengine = settings.get_searchengine_entry()
569+ old_engine = searchengine.subText
570+ self.assertThat(old_engine, NotEquals(""))
571+ self.pointing_device.click_object(searchengine)
572+ searchengine_page = settings.get_searchengine_page()
573+ searchengine_header = searchengine_page.select_single(
574+ browser.SettingsPageHeader)
575+ searchengine_header.click_back_button()
576+ searchengine_page.wait_until_destroyed()
577+ self.assertThat(searchengine.subText, Equals(old_engine))
578+
579+ def test_change_searchengine(self):
580+ settings = self.open_settings()
581+ searchengine = settings.get_searchengine_entry()
582+ old_engine = searchengine.subText
583+ self.assertThat(old_engine, NotEquals(""))
584+ self.pointing_device.click_object(searchengine)
585+ searchengine_page = settings.get_searchengine_page()
586+ self.assertThat(lambda: len(searchengine_page.select_many("Standard")),
587+ Eventually(GreaterThan(1)))
588+ delegates = searchengine_page.select_many("Standard")
589+ delegates.sort(key=lambda delegate: delegate.objectName)
590+ new_index = -1
591+ for (i, delegate) in enumerate(delegates):
592+ checkbox = delegate.select_single(uitk.CheckBox)
593+ if (new_index == -1) and not checkbox.checked:
594+ new_index = i
595+ self.assertThat(checkbox.checked,
596+ Equals(delegate.text == old_engine))
597+ new_engine = delegates[new_index].text
598+ self.assertThat(new_engine, NotEquals(old_engine))
599+ self.pointing_device.click_object(
600+ delegates[new_index].select_single(uitk.CheckBox))
601+ searchengine_page.wait_until_destroyed()
602+ self.assertThat(searchengine.subText, Eventually(Equals(new_engine)))
603+
604 def test_change_homepage(self):
605 settings = self.open_settings()
606 homepage = settings.get_homepage_entry()
607@@ -112,6 +150,10 @@
608 reset = settings.get_reset_settings_entry()
609 self.pointing_device.click_object(reset)
610
611+ searchengine = settings.get_searchengine_entry()
612+ self.assertThat(searchengine.subText,
613+ Eventually(Equals("Google")))
614+
615 homepage = settings.get_homepage_entry()
616 self.assertThat(homepage.subText,
617 Eventually(Equals("http://start.ubuntu.com")))
618
619=== modified file 'tests/unittests/CMakeLists.txt'
620--- tests/unittests/CMakeLists.txt 2015-04-22 09:03:56 +0000
621+++ tests/unittests/CMakeLists.txt 2015-05-14 07:06:15 +0000
622@@ -19,3 +19,4 @@
623 add_subdirectory(favicon-fetcher)
624 add_subdirectory(webapp-container-hook)
625 add_subdirectory(intent-filter)
626+add_subdirectory(search-engine)
627
628=== modified file 'tests/unittests/qml/CMakeLists.txt'
629--- tests/unittests/qml/CMakeLists.txt 2015-02-26 17:49:24 +0000
630+++ tests/unittests/qml/CMakeLists.txt 2015-05-14 07:06:15 +0000
631@@ -10,6 +10,7 @@
632 set(SOURCES
633 ${webbrowser-common_SOURCE_DIR}/favicon-fetcher.cpp
634 ${webbrowser-app_SOURCE_DIR}/file-operations.cpp
635+ ${webbrowser-app_SOURCE_DIR}/searchengine.cpp
636 tst_QmlTests.cpp
637 )
638 add_executable(${TEST} ${SOURCES})
639
640=== modified file 'tests/unittests/qml/tst_QmlTests.cpp'
641--- tests/unittests/qml/tst_QmlTests.cpp 2015-02-18 19:32:11 +0000
642+++ tests/unittests/qml/tst_QmlTests.cpp 2015-05-14 07:06:15 +0000
643@@ -17,12 +17,17 @@
644 */
645
646 // Qt
647+#include <QtCore/QFile>
648+#include <QtCore/QObject>
649+#include <QtCore/QString>
650+#include <QtCore/QTemporaryDir>
651 #include <QtQml/QtQml>
652 #include <QtQuickTest/QtQuickTest>
653
654 // local
655 #include "favicon-fetcher.h"
656 #include "file-operations.h"
657+#include "searchengine.h"
658
659 static QObject* FileOperations_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
660 {
661@@ -31,13 +36,89 @@
662 return new FileOperations();
663 }
664
665+class TestContext : public QObject
666+{
667+ Q_OBJECT
668+
669+ Q_PROPERTY(QString testDir1 READ testDir1 CONSTANT)
670+ Q_PROPERTY(QString testDir2 READ testDir2 CONSTANT)
671+
672+public:
673+ explicit TestContext(QObject* parent=0)
674+ : QObject(parent)
675+ {}
676+
677+ QString testDir1() const
678+ {
679+ return m_testDir1.path();
680+ }
681+
682+ QString testDir2() const
683+ {
684+ return m_testDir2.path();
685+ }
686+
687+ Q_INVOKABLE bool writeSearchEngineDescription(
688+ const QString& path, const QString& filename, const QString& name,
689+ const QString& description, const QString& urlTemplate)
690+ {
691+ QFile file(QDir(path).absoluteFilePath(QString("%1.xml").arg(filename)));
692+ if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
693+ QTextStream out(&file);
694+ out << "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">";
695+ out << "<ShortName>" << name << "</ShortName>";
696+ out << "<Description>" << description << "</Description>";
697+ out << "<Url type=\"text/html\" template=\"" << urlTemplate << "\"/>";
698+ out << "</OpenSearchDescription>";
699+ file.close();
700+ return true;
701+ } else {
702+ return false;
703+ }
704+ }
705+
706+ Q_INVOKABLE bool writeInvalidSearchEngineDescription(const QString& path, const QString& filename)
707+ {
708+ QFile file(QDir(path).absoluteFilePath(QString("%1.xml").arg(filename)));
709+ if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
710+ QTextStream out(&file);
711+ out << "invalid";
712+ file.close();
713+ return true;
714+ } else {
715+ return false;
716+ }
717+ }
718+
719+ Q_INVOKABLE bool deleteSearchEngineDescription(const QString& path, const QString& filename)
720+ {
721+ return QFile(QDir(path).absoluteFilePath(QString("%1.xml").arg(filename))).remove();
722+ }
723+
724+private:
725+ QTemporaryDir m_testDir1;
726+ QTemporaryDir m_testDir2;
727+};
728+
729+static QObject* TestContext_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
730+{
731+ Q_UNUSED(engine);
732+ Q_UNUSED(scriptEngine);
733+ return new TestContext();
734+}
735+
736 int main(int argc, char** argv)
737 {
738 const char* commonUri = "webbrowsercommon.private";
739 qmlRegisterType<FaviconFetcher>(commonUri, 0, 1, "FaviconFetcher");
740
741 const char* browserUri = "webbrowserapp.private";
742+ qmlRegisterType<SearchEngine>(browserUri, 0, 1, "SearchEngine");
743 qmlRegisterSingletonType<FileOperations>(browserUri, 0, 1, "FileOperations", FileOperations_singleton_factory);
744
745+ qmlRegisterSingletonType<TestContext>("webbrowsertest.private", 0, 1, "TestContext", TestContext_singleton_factory);
746+
747 return quick_test_main(argc, argv, "QmlTests", 0);
748 }
749+
750+#include "tst_QmlTests.moc"
751
752=== added file 'tests/unittests/qml/tst_SearchEngines.qml'
753--- tests/unittests/qml/tst_SearchEngines.qml 1970-01-01 00:00:00 +0000
754+++ tests/unittests/qml/tst_SearchEngines.qml 2015-05-14 07:06:15 +0000
755@@ -0,0 +1,117 @@
756+/*
757+ * Copyright 2015 Canonical Ltd.
758+ *
759+ * This file is part of webbrowser-app.
760+ *
761+ * webbrowser-app is free software; you can redistribute it and/or modify
762+ * it under the terms of the GNU General Public License as published by
763+ * the Free Software Foundation; version 3.
764+ *
765+ * webbrowser-app is distributed in the hope that it will be useful,
766+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
767+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
768+ * GNU General Public License for more details.
769+ *
770+ * You should have received a copy of the GNU General Public License
771+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
772+ */
773+
774+import QtQuick 2.0
775+import QtTest 1.0
776+import "../../../src/app/webbrowser"
777+import webbrowsertest.private 0.1
778+import webbrowserapp.private 0.1
779+
780+Item {
781+ id: root
782+
783+ width: 200
784+ height: 200
785+
786+ SearchEngines {
787+ id: searchEngines
788+ readonly property int count: engines.count
789+ }
790+
791+ SearchEngine {
792+ id: testEngine
793+ searchPaths: searchEngines.searchPaths
794+ }
795+
796+ TestCase {
797+ name: "SearchEngines"
798+
799+ function checkEngine(index, filename, name, description, urlTemplate) {
800+ compare(searchEngines.engines.get(index).filename, filename)
801+ testEngine.filename = filename
802+ compare(testEngine.name, name)
803+ compare(testEngine.description, description)
804+ compare(testEngine.urlTemplate, urlTemplate)
805+ testEngine.filename = ""
806+ }
807+
808+ function test_no_engines() {
809+ searchEngines.searchPaths = []
810+ tryCompare(searchEngines, "count", 0)
811+ }
812+
813+ function test_find_engines() {
814+ verify(TestContext.writeSearchEngineDescription(
815+ TestContext.testDir1, "engine1", "engine1", "engine1 search",
816+ "https://example.org/search1?q={searchTerms}"))
817+ verify(TestContext.writeSearchEngineDescription(
818+ TestContext.testDir2, "engine2", "engine2", "engine2 search",
819+ "https://example.org/search2?q={searchTerms}"))
820+ searchEngines.searchPaths = [TestContext.testDir1, TestContext.testDir2]
821+ tryCompare(searchEngines, "count", 2)
822+ checkEngine(0, "engine1", "engine1", "engine1 search",
823+ "https://example.org/search1?q={searchTerms}")
824+ checkEngine(1, "engine2", "engine2", "engine2 search",
825+ "https://example.org/search2?q={searchTerms}")
826+
827+ // override engine2 in dir2 with another description in dir1
828+ verify(TestContext.writeSearchEngineDescription(
829+ TestContext.testDir1, "engine2",
830+ "engine2-overridden", "engine2-overridden search",
831+ "https://example.org/search2-overridden?q={searchTerms}"))
832+ compare(searchEngines.count, 2)
833+ checkEngine(1, "engine2", "engine2-overridden",
834+ "engine2-overridden search",
835+ "https://example.org/search2-overridden?q={searchTerms}")
836+
837+ // reverse the order of search paths to verify that the order
838+ // of precedence is updated
839+ searchEngines.searchPaths = [TestContext.testDir2, TestContext.testDir1]
840+ tryCompare(searchEngines, "count", 2)
841+ checkEngine(0, "engine1", "engine1", "engine1 search",
842+ "https://example.org/search1?q={searchTerms}")
843+ checkEngine(1, "engine2", "engine2", "engine2 search",
844+ "https://example.org/search2?q={searchTerms}")
845+
846+ // override engine2 with an invalid description and verify
847+ // that it is removed from the list
848+ verify(TestContext.deleteSearchEngineDescription(
849+ TestContext.testDir2, "engine2"))
850+ verify(TestContext.writeInvalidSearchEngineDescription(
851+ TestContext.testDir2, "engine2"))
852+ tryCompare(searchEngines, "count", 1)
853+ checkEngine(0, "engine1", "engine1", "engine1 search",
854+ "https://example.org/search1?q={searchTerms}")
855+
856+ // remove the invalid description and verify that the other
857+ // description re-appears
858+ verify(TestContext.deleteSearchEngineDescription(
859+ TestContext.testDir2, "engine2"))
860+ tryCompare(searchEngines, "count", 2)
861+ checkEngine(1, "engine2", "engine2-overridden",
862+ "engine2-overridden search",
863+ "https://example.org/search2-overridden?q={searchTerms}")
864+
865+ // clean up
866+ verify(TestContext.deleteSearchEngineDescription(
867+ TestContext.testDir1, "engine2"))
868+ verify(TestContext.deleteSearchEngineDescription(
869+ TestContext.testDir1, "engine1"))
870+ }
871+ }
872+}
873
874=== added directory 'tests/unittests/search-engine'
875=== added file 'tests/unittests/search-engine/CMakeLists.txt'
876--- tests/unittests/search-engine/CMakeLists.txt 1970-01-01 00:00:00 +0000
877+++ tests/unittests/search-engine/CMakeLists.txt 2015-05-14 07:06:15 +0000
878@@ -0,0 +1,9 @@
879+set(TEST tst_SearchEngineTests)
880+set(SOURCES
881+ ${webbrowser-app_SOURCE_DIR}/searchengine.cpp
882+ tst_SearchEngineTests.cpp
883+)
884+add_executable(${TEST} ${SOURCES})
885+include_directories(${webbrowser-app_SOURCE_DIR})
886+qt5_use_modules(${TEST} Core Test)
887+add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
888
889=== added file 'tests/unittests/search-engine/tst_SearchEngineTests.cpp'
890--- tests/unittests/search-engine/tst_SearchEngineTests.cpp 1970-01-01 00:00:00 +0000
891+++ tests/unittests/search-engine/tst_SearchEngineTests.cpp 2015-05-14 07:06:15 +0000
892@@ -0,0 +1,242 @@
893+/*
894+ * Copyright 2015 Canonical Ltd.
895+ *
896+ * This file is part of webbrowser-app.
897+ *
898+ * webbrowser-app is free software; you can redistribute it and/or modify
899+ * it under the terms of the GNU General Public License as published by
900+ * the Free Software Foundation; version 3.
901+ *
902+ * webbrowser-app is distributed in the hope that it will be useful,
903+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
904+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
905+ * GNU General Public License for more details.
906+ *
907+ * You should have received a copy of the GNU General Public License
908+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
909+ */
910+
911+// Qt
912+#include <QtCore/QDir>
913+#include <QtCore/QFile>
914+#include <QtCore/QTemporaryDir>
915+#include <QtCore/QTextStream>
916+#include <QtTest/QSignalSpy>
917+#include <QtTest/QtTest>
918+
919+// local
920+#include "searchengine.h"
921+
922+class SearchEngineTests : public QObject
923+{
924+ Q_OBJECT
925+
926+private:
927+ QTemporaryDir* dir1;
928+ QTemporaryDir* dir2;
929+ SearchEngine* engine;
930+ QSignalSpy* searchPathsSpy;
931+ QSignalSpy* filenameSpy;
932+ QSignalSpy* nameSpy;
933+ QSignalSpy* descriptionSpy;
934+ QSignalSpy* urlTemplateSpy;
935+ QSignalSpy* suggestionsUrlTemplateSpy;
936+ QSignalSpy* validSpy;
937+
938+private Q_SLOTS:
939+ void init()
940+ {
941+ dir1 = new QTemporaryDir;
942+ QVERIFY(dir1->isValid());
943+
944+ dir2 = new QTemporaryDir;
945+ QVERIFY(dir2->isValid());
946+
947+ engine = new SearchEngine;
948+ searchPathsSpy = new QSignalSpy(engine, SIGNAL(searchPathsChanged()));
949+ filenameSpy = new QSignalSpy(engine, SIGNAL(filenameChanged()));
950+ nameSpy = new QSignalSpy(engine, SIGNAL(nameChanged()));
951+ descriptionSpy = new QSignalSpy(engine, SIGNAL(descriptionChanged()));
952+ urlTemplateSpy = new QSignalSpy(engine, SIGNAL(urlTemplateChanged()));
953+ suggestionsUrlTemplateSpy = new QSignalSpy(engine, SIGNAL(suggestionsUrlTemplateChanged()));
954+ validSpy = new QSignalSpy(engine, SIGNAL(validChanged()));
955+
956+ QFile file(QDir(dir1->path()).absoluteFilePath("engine1.xml"));
957+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
958+ QTextStream out(&file);
959+ out << "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">";
960+ out << "<ShortName>engine1</ShortName>";
961+ out << "<Description>engine1 search</Description>";
962+ out << "<Url type=\"text/html\" template=\"https://example.org/search1?q={searchTerms}\"/>";
963+ out << "<Url type=\"application/x-suggestions+json\" template=\"https://example.org/suggest1?q={searchTerms}\"/>";
964+ out << "</OpenSearchDescription>";
965+ file.close();
966+
967+ file.setFileName(QDir(dir2->path()).absoluteFilePath("engine2.xml"));
968+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
969+ out.setDevice(&file);
970+ out << "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">";
971+ out << "<ShortName>engine2</ShortName>";
972+ out << "<Url type=\"text/html\" template=\"https://example.org/search2?q={searchTerms}\"/>";
973+ out << "</OpenSearchDescription>";
974+ file.close();
975+
976+ file.setFileName(QDir(dir2->path()).absoluteFilePath("invalid.xml"));
977+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
978+ out.setDevice(&file);
979+ out << "invalid";
980+ file.close();
981+
982+ engine->setSearchPaths({dir1->path(), dir2->path()});
983+ QCOMPARE(searchPathsSpy->count(), 1);
984+ searchPathsSpy->clear();
985+ nameSpy->clear();
986+ descriptionSpy->clear();
987+ urlTemplateSpy->clear();
988+ suggestionsUrlTemplateSpy->clear();
989+ validSpy->clear();
990+ QVERIFY(!engine->isValid());
991+ }
992+
993+ void cleanup()
994+ {
995+ delete validSpy;
996+ delete suggestionsUrlTemplateSpy;
997+ delete urlTemplateSpy;
998+ delete descriptionSpy;
999+ delete nameSpy;
1000+ delete filenameSpy;
1001+ delete searchPathsSpy;
1002+ delete engine;
1003+ delete dir1;
1004+ delete dir2;
1005+ }
1006+
1007+ void shouldChangeSearchPaths()
1008+ {
1009+ QCOMPARE(engine->searchPaths(), QStringList({dir1->path(), dir2->path()}));
1010+ engine->setSearchPaths({dir2->path()});
1011+ QCOMPARE(searchPathsSpy->count(), 1);
1012+ QCOMPARE(engine->searchPaths(), QStringList({dir2->path()}));
1013+ }
1014+
1015+ void shouldChangeFilename()
1016+ {
1017+ QVERIFY(engine->filename().isEmpty());
1018+ engine->setFilename("engine1");
1019+ QCOMPARE(filenameSpy->count(), 1);
1020+ QCOMPARE(engine->filename(), QString("engine1"));
1021+ engine->setFilename("");
1022+ QCOMPARE(filenameSpy->count(), 2);
1023+ QVERIFY(engine->filename().isEmpty());
1024+ }
1025+
1026+ void shouldParseValidDescriptionWithDescription()
1027+ {
1028+ engine->setFilename("engine1");
1029+ QCOMPARE(nameSpy->count(), 1);
1030+ QCOMPARE(engine->name(), QString("engine1"));
1031+ QCOMPARE(descriptionSpy->count(), 1);
1032+ QCOMPARE(engine->description(), QString("engine1 search"));
1033+ QCOMPARE(urlTemplateSpy->count(), 1);
1034+ QCOMPARE(engine->urlTemplate(), QString("https://example.org/search1?q={searchTerms}"));
1035+ QCOMPARE(suggestionsUrlTemplateSpy->count(), 1);
1036+ QCOMPARE(engine->suggestionsUrlTemplate(), QString("https://example.org/suggest1?q={searchTerms}"));
1037+ QCOMPARE(validSpy->count(), 1);
1038+ QVERIFY(engine->isValid());
1039+ }
1040+
1041+ void shouldParseValidDescriptionWithoutDescriptionAndSuggestionsTemplate()
1042+ {
1043+ engine->setFilename("engine2");
1044+ QCOMPARE(nameSpy->count(), 1);
1045+ QCOMPARE(engine->name(), QString("engine2"));
1046+ QVERIFY(descriptionSpy->isEmpty());
1047+ QVERIFY(engine->description().isEmpty());
1048+ QCOMPARE(urlTemplateSpy->count(), 1);
1049+ QCOMPARE(engine->urlTemplate(), QString("https://example.org/search2?q={searchTerms}"));
1050+ QVERIFY(suggestionsUrlTemplateSpy->isEmpty());
1051+ QVERIFY(engine->suggestionsUrlTemplate().isEmpty());
1052+ QCOMPARE(validSpy->count(), 1);
1053+ QVERIFY(engine->isValid());
1054+ }
1055+
1056+ void shouldFailToParseInvalidDescription()
1057+ {
1058+ engine->setFilename("invalid");
1059+ QVERIFY(nameSpy->isEmpty());
1060+ QVERIFY(engine->name().isEmpty());
1061+ QVERIFY(descriptionSpy->isEmpty());
1062+ QVERIFY(engine->description().isEmpty());
1063+ QVERIFY(urlTemplateSpy->isEmpty());
1064+ QVERIFY(engine->urlTemplate().isEmpty());
1065+ QVERIFY(suggestionsUrlTemplateSpy->isEmpty());
1066+ QVERIFY(engine->suggestionsUrlTemplate().isEmpty());
1067+ QVERIFY(validSpy->isEmpty());
1068+ QVERIFY(!engine->isValid());
1069+ }
1070+
1071+ void shouldFailToLocateNonexistentDescription()
1072+ {
1073+ engine->setFilename("nonexistent");
1074+ QVERIFY(nameSpy->isEmpty());
1075+ QVERIFY(engine->name().isEmpty());
1076+ QVERIFY(descriptionSpy->isEmpty());
1077+ QVERIFY(engine->description().isEmpty());
1078+ QVERIFY(urlTemplateSpy->isEmpty());
1079+ QVERIFY(engine->urlTemplate().isEmpty());
1080+ QVERIFY(suggestionsUrlTemplateSpy->isEmpty());
1081+ QVERIFY(engine->suggestionsUrlTemplate().isEmpty());
1082+ QVERIFY(validSpy->isEmpty());
1083+ QVERIFY(!engine->isValid());
1084+ }
1085+
1086+ void shouldOverrideExistingDescription()
1087+ {
1088+ QFile file(QDir(dir1->path()).absoluteFilePath("engine2.xml"));
1089+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
1090+ QTextStream out(&file);
1091+ out << "<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">";
1092+ out << "<ShortName>engine2-overridden</ShortName>";
1093+ out << "<Description>engine2 overridden search</Description>";
1094+ out << "<Url type=\"text/html\" template=\"https://example.org/search2overridden?q={searchTerms}\"/>";
1095+ out << "<Url type=\"application/x-suggestions+json\" template=\"https://example.org/suggest2?q={searchTerms}\"/>";
1096+ out << "</OpenSearchDescription>";
1097+ file.close();
1098+
1099+ engine->setFilename("engine2");
1100+ QCOMPARE(nameSpy->count(), 1);
1101+ QCOMPARE(engine->name(), QString("engine2-overridden"));
1102+ QCOMPARE(descriptionSpy->count(), 1);
1103+ QCOMPARE(engine->description(), QString("engine2 overridden search"));
1104+ QCOMPARE(urlTemplateSpy->count(), 1);
1105+ QCOMPARE(engine->urlTemplate(), QString("https://example.org/search2overridden?q={searchTerms}"));
1106+ QCOMPARE(suggestionsUrlTemplateSpy->count(), 1);
1107+ QCOMPARE(engine->suggestionsUrlTemplate(), QString("https://example.org/suggest2?q={searchTerms}"));
1108+ QCOMPARE(validSpy->count(), 1);
1109+ QVERIFY(engine->isValid());
1110+ }
1111+
1112+ void shouldOverrideAndInvalidateDescription()
1113+ {
1114+ QFile file(QDir(dir1->path()).absoluteFilePath("engine2.xml"));
1115+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
1116+ file.close();
1117+
1118+ engine->setFilename("engine2");
1119+ QVERIFY(nameSpy->isEmpty());
1120+ QVERIFY(engine->name().isEmpty());
1121+ QVERIFY(descriptionSpy->isEmpty());
1122+ QVERIFY(engine->description().isEmpty());
1123+ QVERIFY(urlTemplateSpy->isEmpty());
1124+ QVERIFY(engine->urlTemplate().isEmpty());
1125+ QVERIFY(suggestionsUrlTemplateSpy->isEmpty());
1126+ QVERIFY(engine->suggestionsUrlTemplate().isEmpty());
1127+ QVERIFY(validSpy->isEmpty());
1128+ QVERIFY(!engine->isValid());
1129+ }
1130+};
1131+
1132+QTEST_MAIN(SearchEngineTests)
1133+
1134+#include "tst_SearchEngineTests.moc"

Subscribers

People subscribed via source and target branches

to status/vote changes: