testunifiedsearchlistmodel.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. /*
  2. * Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 2 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  11. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. * for more details.
  13. */
  14. #include "gui/tray/unifiedsearchresultslistmodel.h"
  15. #include "account.h"
  16. #include "accountstate.h"
  17. #include "syncenginetestutils.h"
  18. #include <QAbstractItemModelTester>
  19. #include <QDesktopServices>
  20. #include <QSignalSpy>
  21. #include <QTest>
  22. namespace {
  23. /**
  24. * @brief The FakeDesktopServicesUrlHandler
  25. * overrides QDesktopServices::openUrl
  26. **/
  27. class FakeDesktopServicesUrlHandler : public QObject
  28. {
  29. Q_OBJECT
  30. public:
  31. FakeDesktopServicesUrlHandler(QObject *parent = nullptr)
  32. : QObject(parent)
  33. {}
  34. public:
  35. signals:
  36. void resultClicked(const QUrl &url);
  37. };
  38. /**
  39. * @brief The FakeProvider
  40. * is a simple structure that represents initial list of providers and their properties
  41. **/
  42. class FakeProvider
  43. {
  44. public:
  45. QString _id;
  46. QString _name;
  47. qint32 _order = std::numeric_limits<qint32>::max();
  48. quint32 _numItemsToInsert = 5; // how many fake resuls to insert
  49. };
  50. // this will be used when initializing fake search results data for each provider
  51. static const QVector<FakeProvider> fakeProvidersInitInfo = {
  52. {QStringLiteral("settings_apps"), QStringLiteral("Apps"), -50, 10},
  53. {QStringLiteral("talk-message"), QStringLiteral("Messages"), -2, 17},
  54. {QStringLiteral("files"), QStringLiteral("Files"), 5, 3},
  55. {QStringLiteral("deck"), QStringLiteral("Deck"), 10, 5},
  56. {QStringLiteral("comments"), QStringLiteral("Comments"), 10, 2},
  57. {QStringLiteral("mail"), QStringLiteral("Mails"), 10, 15},
  58. {QStringLiteral("calendar"), QStringLiteral("Events"), 30, 11}
  59. };
  60. static QByteArray fake404Response = R"(
  61. {"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}}
  62. )";
  63. static QByteArray fake400Response = R"(
  64. {"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}}
  65. )";
  66. static QByteArray fake500Response = R"(
  67. {"ocs":{"meta":{"status":"failure","statuscode":500,"message":"Internal Server Error.\n"},"data":[]}}
  68. )";
  69. /**
  70. * @brief The FakeSearchResultsStorage
  71. * emulates the real server storage that contains all the results that UnifiedSearchListmodel will search for
  72. **/
  73. class FakeSearchResultsStorage
  74. {
  75. class Provider
  76. {
  77. public:
  78. class SearchResult
  79. {
  80. public:
  81. QString _thumbnailUrl;
  82. QString _title;
  83. QString _subline;
  84. QString _resourceUrl;
  85. QString _icon;
  86. bool _rounded;
  87. };
  88. QString _id;
  89. QString _name;
  90. qint32 _order = std::numeric_limits<qint32>::max();
  91. qint32 _cursor = 0;
  92. bool _isPaginated = false;
  93. QVector<SearchResult> _results;
  94. };
  95. FakeSearchResultsStorage() = default;
  96. public:
  97. static FakeSearchResultsStorage *instance()
  98. {
  99. if (!_instance) {
  100. _instance = new FakeSearchResultsStorage();
  101. _instance->init();
  102. }
  103. return _instance;
  104. };
  105. static void destroy()
  106. {
  107. if (_instance) {
  108. delete _instance;
  109. }
  110. _instance = nullptr;
  111. }
  112. void init()
  113. {
  114. if (!_searchResultsData.isEmpty()) {
  115. return;
  116. }
  117. _metaSuccess = {{QStringLiteral("status"), QStringLiteral("ok")}, {QStringLiteral("statuscode"), 200},
  118. {QStringLiteral("message"), QStringLiteral("OK")}};
  119. initProvidersResponse();
  120. initSearchResultsData();
  121. }
  122. // initialize the JSON response containing the fake list of providers and their properties
  123. void initProvidersResponse()
  124. {
  125. QList<QVariant> providersList;
  126. for (const auto &fakeProviderInitInfo : fakeProvidersInitInfo) {
  127. providersList.push_back(QVariantMap{
  128. {QStringLiteral("id"), fakeProviderInitInfo._id},
  129. {QStringLiteral("name"), fakeProviderInitInfo._name},
  130. {QStringLiteral("order"), fakeProviderInitInfo._order},
  131. });
  132. }
  133. const QVariantMap ocsMap = {
  134. {QStringLiteral("meta"), _metaSuccess},
  135. {QStringLiteral("data"), providersList}
  136. };
  137. _providersResponse =
  138. QJsonDocument::fromVariant(QVariantMap{{QStringLiteral("ocs"), ocsMap}}).toJson(QJsonDocument::Compact);
  139. }
  140. // init the map of fake search results for each provider
  141. void initSearchResultsData()
  142. {
  143. for (const auto &fakeProvider : fakeProvidersInitInfo) {
  144. auto &providerData = _searchResultsData[fakeProvider._id];
  145. providerData._id = fakeProvider._id;
  146. providerData._name = fakeProvider._name;
  147. providerData._order = fakeProvider._order;
  148. if (fakeProvider._numItemsToInsert > pageSize) {
  149. providerData._isPaginated = true;
  150. }
  151. for (quint32 i = 0; i < fakeProvider._numItemsToInsert; ++i) {
  152. providerData._results.push_back(
  153. {"http://example.de/avatar/john/64", QString(QStringLiteral("John Doe in ") + fakeProvider._name),
  154. QString(QStringLiteral("We a discussion about ") + fakeProvider._name
  155. + QStringLiteral(" already. But, let's have a follow up tomorrow afternoon.")),
  156. "http://example.de/call/abcde12345#message_12345", QStringLiteral("icon-talk"), true});
  157. }
  158. }
  159. }
  160. const QList<QVariant> resultsForProvider(const QString &providerId, int cursor)
  161. {
  162. QList<QVariant> list;
  163. const auto results = resultsForProviderAsVector(providerId, cursor);
  164. if (results.isEmpty()) {
  165. return list;
  166. }
  167. for (const auto &result : results) {
  168. list.push_back(QVariantMap{
  169. {"thumbnailUrl", result._thumbnailUrl},
  170. {"title", result._title},
  171. {"subline", result._subline},
  172. {"resourceUrl", result._resourceUrl},
  173. {"icon", result._icon},
  174. {"rounded", result._rounded}
  175. });
  176. }
  177. return list;
  178. }
  179. const QVector<Provider::SearchResult> resultsForProviderAsVector(const QString &providerId, int cursor)
  180. {
  181. QVector<Provider::SearchResult> results;
  182. const auto provider = _searchResultsData.value(providerId, Provider());
  183. if (provider._id.isEmpty() || cursor > provider._results.size()) {
  184. return results;
  185. }
  186. const int n = cursor + pageSize > provider._results.size()
  187. ? 0
  188. : cursor + pageSize;
  189. for (int i = cursor; i < n; ++i) {
  190. results.push_back(provider._results[i]);
  191. }
  192. return results;
  193. }
  194. const QByteArray queryProvider(const QString &providerId, const QString &searchTerm, int cursor)
  195. {
  196. if (!_searchResultsData.contains(providerId)) {
  197. return fake404Response;
  198. }
  199. if (searchTerm == QStringLiteral("[HTTP500]")) {
  200. return fake500Response;
  201. }
  202. if (searchTerm == QStringLiteral("[empty]")) {
  203. const QVariantMap dataMap = {{QStringLiteral("name"), _searchResultsData[providerId]._name},
  204. {QStringLiteral("isPaginated"), false}, {QStringLiteral("cursor"), 0},
  205. {QStringLiteral("entries"), QVariantList{}}};
  206. const QVariantMap ocsMap = {{QStringLiteral("meta"), _metaSuccess}, {QStringLiteral("data"), dataMap}};
  207. return QJsonDocument::fromVariant(QVariantMap{{QStringLiteral("ocs"), ocsMap}})
  208. .toJson(QJsonDocument::Compact);
  209. }
  210. const auto provider = _searchResultsData.value(providerId, Provider());
  211. const auto nextCursor = cursor + pageSize;
  212. const QVariantMap dataMap = {{QStringLiteral("name"), _searchResultsData[providerId]._name},
  213. {QStringLiteral("isPaginated"), _searchResultsData[providerId]._isPaginated},
  214. {QStringLiteral("cursor"), nextCursor},
  215. {QStringLiteral("entries"), resultsForProvider(providerId, cursor)}};
  216. const QVariantMap ocsMap = {{QStringLiteral("meta"), _metaSuccess}, {QStringLiteral("data"), dataMap}};
  217. return QJsonDocument::fromVariant(QVariantMap{{QStringLiteral("ocs"), ocsMap}}).toJson(QJsonDocument::Compact);
  218. }
  219. [[nodiscard]] const QByteArray &fakeProvidersResponseJson() const { return _providersResponse; }
  220. private:
  221. static FakeSearchResultsStorage *_instance;
  222. static const int pageSize = 5;
  223. QMap<QString, Provider> _searchResultsData;
  224. QByteArray _providersResponse = fake404Response;
  225. QVariantMap _metaSuccess;
  226. };
  227. FakeSearchResultsStorage *FakeSearchResultsStorage::_instance = nullptr;
  228. }
  229. class TestUnifiedSearchListmodel : public QObject
  230. {
  231. Q_OBJECT
  232. public:
  233. TestUnifiedSearchListmodel() = default;
  234. QScopedPointer<FakeQNAM> fakeQnam;
  235. OCC::AccountPtr account;
  236. QScopedPointer<OCC::AccountState> accountState;
  237. QScopedPointer<OCC::UnifiedSearchResultsListModel> model;
  238. QScopedPointer<QAbstractItemModelTester> modelTester;
  239. QScopedPointer<FakeDesktopServicesUrlHandler> fakeDesktopServicesUrlHandler;
  240. static const int searchResultsReplyDelay = 100;
  241. private slots:
  242. void initTestCase()
  243. {
  244. fakeQnam.reset(new FakeQNAM({}));
  245. account = OCC::Account::create();
  246. account->setCredentials(new FakeCredentials{fakeQnam.data()});
  247. account->setUrl(QUrl(("http://example.de")));
  248. accountState.reset(new OCC::AccountState(account));
  249. fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  250. Q_UNUSED(device);
  251. QNetworkReply *reply = nullptr;
  252. const auto urlQuery = QUrlQuery(req.url());
  253. const auto format = urlQuery.queryItemValue(QStringLiteral("format"));
  254. const auto cursor = urlQuery.queryItemValue(QStringLiteral("cursor")).toInt();
  255. const auto searchTerm = urlQuery.queryItemValue(QStringLiteral("term"));
  256. const auto path = req.url().path();
  257. if (!req.url().toString().startsWith(accountState->account()->url().toString())) {
  258. reply = new FakeErrorReply(op, req, this, 404, fake404Response);
  259. }
  260. if (format != QStringLiteral("json")) {
  261. reply = new FakeErrorReply(op, req, this, 400, fake400Response);
  262. }
  263. // handle fetch of providers list
  264. if (path.startsWith(QStringLiteral("/ocs/v2.php/search/providers")) && searchTerm.isEmpty()) {
  265. reply = new FakePayloadReply(op, req,
  266. FakeSearchResultsStorage::instance()->fakeProvidersResponseJson(), fakeQnam.data());
  267. // handle search for provider
  268. } else if (path.startsWith(QStringLiteral("/ocs/v2.php/search/providers")) && !searchTerm.isEmpty()) {
  269. const auto pathSplit = path.mid(QString(QStringLiteral("/ocs/v2.php/search/providers")).size())
  270. .split(QLatin1Char('/'), Qt::SkipEmptyParts);
  271. if (!pathSplit.isEmpty() && path.contains(pathSplit.first())) {
  272. reply = new FakePayloadReply(op, req,
  273. FakeSearchResultsStorage::instance()->queryProvider(pathSplit.first(), searchTerm, cursor),
  274. searchResultsReplyDelay, fakeQnam.data());
  275. }
  276. }
  277. if (!reply) {
  278. return qobject_cast<QNetworkReply*>(new FakeErrorReply(op, req, this, 404, QByteArrayLiteral("{error: \"Not found!\"}")));
  279. }
  280. return reply;
  281. });
  282. model.reset(new OCC::UnifiedSearchResultsListModel(accountState.data()));
  283. modelTester.reset(new QAbstractItemModelTester(model.data()));
  284. fakeDesktopServicesUrlHandler.reset(new FakeDesktopServicesUrlHandler);
  285. }
  286. void testSetSearchTermStartStopSearch()
  287. {
  288. // make sure the model is empty
  289. model->setSearchTerm(QStringLiteral(""));
  290. QVERIFY(model->rowCount() == 0);
  291. // #1 test setSearchTerm actually sets the search term and the signal is emitted
  292. QSignalSpy searhTermChanged(model.data(), &OCC::UnifiedSearchResultsListModel::searchTermChanged);
  293. model->setSearchTerm(QStringLiteral("dis"));
  294. QCOMPARE(searhTermChanged.count(), 1);
  295. QCOMPARE(model->searchTerm(), QStringLiteral("dis"));
  296. // #2 test setSearchTerm actually sets the search term and the signal is emitted
  297. searhTermChanged.clear();
  298. model->setSearchTerm(model->searchTerm() + QStringLiteral("cuss"));
  299. QCOMPARE(model->searchTerm(), QStringLiteral("discuss"));
  300. QCOMPARE(searhTermChanged.count(), 1);
  301. // #3 test that model has not started search yet
  302. QVERIFY(!model->isSearchInProgress());
  303. // #4 test that model has started the search after specific delay
  304. QSignalSpy searchInProgressChanged(model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
  305. // allow search jobs to get created within the model
  306. QVERIFY(searchInProgressChanged.wait());
  307. QCOMPARE(searchInProgressChanged.count(), 1);
  308. QVERIFY(model->isSearchInProgress());
  309. // #5 test that model has stopped the search after setting empty search term
  310. model->setSearchTerm(QStringLiteral(""));
  311. QVERIFY(!model->isSearchInProgress());
  312. }
  313. void testSetSearchTermResultsFound()
  314. {
  315. // make sure the model is empty
  316. model->setSearchTerm(QStringLiteral(""));
  317. QVERIFY(model->rowCount() == 0);
  318. // test that search term gets set, search gets started and enough results get returned
  319. model->setSearchTerm(model->searchTerm() + QStringLiteral("discuss"));
  320. QSignalSpy searchInProgressChanged(
  321. model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
  322. QVERIFY(searchInProgressChanged.wait());
  323. // make sure search has started
  324. QCOMPARE(searchInProgressChanged.count(), 1);
  325. QVERIFY(model->isSearchInProgress());
  326. QVERIFY(searchInProgressChanged.wait());
  327. // make sure search has finished
  328. QVERIFY(!model->isSearchInProgress());
  329. QVERIFY(model->rowCount() > 0);
  330. }
  331. void testSetSearchTermResultsNotFound()
  332. {
  333. // make sure the model is empty
  334. model->setSearchTerm(QStringLiteral(""));
  335. QVERIFY(model->rowCount() == 0);
  336. // test that search term gets set, search gets started and enough results get returned
  337. model->setSearchTerm(model->searchTerm() + QStringLiteral("[empty]"));
  338. QSignalSpy searchInProgressChanged(
  339. model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
  340. QVERIFY(searchInProgressChanged.wait());
  341. // make sure search has started
  342. QCOMPARE(searchInProgressChanged.count(), 1);
  343. QVERIFY(model->isSearchInProgress());
  344. QVERIFY(searchInProgressChanged.wait());
  345. // make sure search has finished
  346. QVERIFY(!model->isSearchInProgress());
  347. QVERIFY(model->rowCount() == 0);
  348. }
  349. void testFetchMoreClicked()
  350. {
  351. // make sure the model is empty
  352. model->setSearchTerm(QStringLiteral(""));
  353. QVERIFY(model->rowCount() == 0);
  354. QSignalSpy searchInProgressChanged(
  355. model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
  356. // test that search term gets set, search gets started and enough results get returned
  357. model->setSearchTerm(model->searchTerm() + QStringLiteral("whatever"));
  358. QVERIFY(searchInProgressChanged.wait());
  359. // make sure search has started
  360. QVERIFY(model->isSearchInProgress());
  361. QVERIFY(searchInProgressChanged.wait());
  362. // make sure search has finished
  363. QVERIFY(!model->isSearchInProgress());
  364. const auto numRowsInModelPrev = model->rowCount();
  365. // test fetch more results
  366. QSignalSpy currentFetchMoreInProgressProviderIdChanged(
  367. model.data(), &OCC::UnifiedSearchResultsListModel::currentFetchMoreInProgressProviderIdChanged);
  368. QSignalSpy rowsInserted(model.data(), &OCC::UnifiedSearchResultsListModel::rowsInserted);
  369. for (int i = 0; i < model->rowCount(); ++i) {
  370. const auto type = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::TypeRole);
  371. if (type == OCC::UnifiedSearchResult::Type::FetchMoreTrigger) {
  372. const auto providerId =
  373. model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ProviderIdRole)
  374. .toString();
  375. model->fetchMoreTriggerClicked(providerId);
  376. break;
  377. }
  378. }
  379. // make sure the currentFetchMoreInProgressProviderId was set back and forth and correct number fows has been inserted
  380. QCOMPARE(currentFetchMoreInProgressProviderIdChanged.count(), 1);
  381. const auto providerIdFetchMoreTriggered = model->currentFetchMoreInProgressProviderId();
  382. QVERIFY(!providerIdFetchMoreTriggered.isEmpty());
  383. QVERIFY(currentFetchMoreInProgressProviderIdChanged.wait());
  384. QVERIFY(model->currentFetchMoreInProgressProviderId().isEmpty());
  385. QCOMPARE(rowsInserted.count(), 1);
  386. const auto arguments = rowsInserted.takeFirst();
  387. QVERIFY(arguments.size() > 0);
  388. const auto first = arguments.at(0).toInt();
  389. const auto last = arguments.at(1).toInt();
  390. const int numInsertedExpected = last - first;
  391. QCOMPARE(model->rowCount() - numRowsInModelPrev, numInsertedExpected);
  392. // make sure the FetchMoreTrigger gets removed when no more results available
  393. if (!providerIdFetchMoreTriggered.isEmpty()) {
  394. currentFetchMoreInProgressProviderIdChanged.clear();
  395. rowsInserted.clear();
  396. QSignalSpy rowsRemoved(model.data(), &OCC::UnifiedSearchResultsListModel::rowsRemoved);
  397. for (int i = 0; i < 10; ++i) {
  398. model->fetchMoreTriggerClicked(providerIdFetchMoreTriggered);
  399. QVERIFY(currentFetchMoreInProgressProviderIdChanged.wait());
  400. if (rowsRemoved.count() > 0) {
  401. break;
  402. }
  403. }
  404. QCOMPARE(rowsRemoved.count(), 1);
  405. bool isFetchMoreTriggerFound = false;
  406. for (int i = 0; i < model->rowCount(); ++i) {
  407. const auto type = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::TypeRole);
  408. const auto providerId = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ProviderIdRole)
  409. .toString();
  410. if (type == OCC::UnifiedSearchResult::Type::FetchMoreTrigger
  411. && providerId == providerIdFetchMoreTriggered) {
  412. isFetchMoreTriggerFound = true;
  413. break;
  414. }
  415. }
  416. QVERIFY(!isFetchMoreTriggerFound);
  417. }
  418. }
  419. void testSearchResultlicked()
  420. {
  421. // make sure the model is empty
  422. model->setSearchTerm(QStringLiteral(""));
  423. QVERIFY(model->rowCount() == 0);
  424. // test that search term gets set, search gets started and enough results get returned
  425. model->setSearchTerm(model->searchTerm() + QStringLiteral("discuss"));
  426. QSignalSpy searchInProgressChanged(
  427. model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
  428. QVERIFY(searchInProgressChanged.wait());
  429. // make sure search has started
  430. QCOMPARE(searchInProgressChanged.count(), 1);
  431. QVERIFY(model->isSearchInProgress());
  432. QVERIFY(searchInProgressChanged.wait());
  433. // make sure search has finished and some results has been received
  434. QVERIFY(!model->isSearchInProgress());
  435. QVERIFY(model->rowCount() != 0);
  436. QDesktopServices::setUrlHandler("http", fakeDesktopServicesUrlHandler.data(), "resultClicked");
  437. QDesktopServices::setUrlHandler("https", fakeDesktopServicesUrlHandler.data(), "resultClicked");
  438. QSignalSpy resultClicked(fakeDesktopServicesUrlHandler.data(), &FakeDesktopServicesUrlHandler::resultClicked);
  439. // test click on a result item
  440. QString urlForClickedResult;
  441. for (int i = 0; i < model->rowCount(); ++i) {
  442. const auto type = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::TypeRole);
  443. if (type == OCC::UnifiedSearchResult::Type::Default) {
  444. const auto providerId =
  445. model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ProviderIdRole)
  446. .toString();
  447. urlForClickedResult = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ResourceUrlRole).toString();
  448. if (!providerId.isEmpty() && !urlForClickedResult.isEmpty()) {
  449. model->resultClicked(providerId, QUrl(urlForClickedResult));
  450. break;
  451. }
  452. }
  453. }
  454. QCOMPARE(resultClicked.count(), 1);
  455. const auto arguments = resultClicked.takeFirst();
  456. const auto urlOpenTriggeredViaDesktopServices = arguments.at(0).toString();
  457. QCOMPARE(urlOpenTriggeredViaDesktopServices, urlForClickedResult);
  458. }
  459. void testSetSearchTermResultsError()
  460. {
  461. // make sure the model is empty
  462. model->setSearchTerm(QStringLiteral(""));
  463. QVERIFY(model->rowCount() == 0);
  464. QSignalSpy errorStringChanged(model.data(), &OCC::UnifiedSearchResultsListModel::errorStringChanged);
  465. QSignalSpy searchInProgressChanged(
  466. model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
  467. model->setSearchTerm(model->searchTerm() + QStringLiteral("[HTTP500]"));
  468. QVERIFY(searchInProgressChanged.wait());
  469. // make sure search has started
  470. QVERIFY(model->isSearchInProgress());
  471. QVERIFY(searchInProgressChanged.wait());
  472. // make sure search has finished
  473. QVERIFY(!model->isSearchInProgress());
  474. // make sure the model is empty and an error string has been set
  475. QVERIFY(model->rowCount() == 0);
  476. QVERIFY(errorStringChanged.count() > 0);
  477. QVERIFY(!model->errorString().isEmpty());
  478. }
  479. void cleanupTestCase()
  480. {
  481. FakeSearchResultsStorage::destroy();
  482. }
  483. };
  484. QTEST_MAIN(TestUnifiedSearchListmodel)
  485. #include "testunifiedsearchlistmodel.moc"