File indexing completed on 2024-11-10 05:11:07

0001 /*
0002  * Copyright 2018 by Marco Martin <mart@kde.org>
0003  *
0004  * Licensed under the Apache License, Version 2.0 (the "License");
0005  * you may not use this file except in compliance with the License.
0006  * You may obtain a copy of the License at
0007  *
0008  *    http://www.apache.org/licenses/LICENSE-2.0
0009  *
0010  * Unless required by applicable law or agreed to in writing, software
0011  * distributed under the License is distributed on an "AS IS" BASIS,
0012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013  * See the License for the specific language governing permissions and
0014  * limitations under the License.
0015  *
0016  */
0017 
0018 #include <QtTest>
0019 #include <QWebSocket>
0020 #include <QWebSocketServer>
0021 #include <QAbstractItemModel>
0022 #include <QQuickView>
0023 #include <QQmlEngine>
0024 #include "../import/mycroftcontroller.h"
0025 #include "../import/abstractdelegate.h"
0026 #include "../import/filereader.h"
0027 #include "../import/globalsettings.h"
0028 #include "../import/activeskillsmodel.h"
0029 #include "../import/delegatesmodel.h"
0030 #include "../import/abstractskillview.h"
0031 #include "../import/sessiondatamap.h"
0032 #include "../import/sessiondatamodel.h"
0033 
0034 class ServerTest : public QObject
0035 {
0036     Q_OBJECT
0037 
0038 public Q_SLOTS:
0039     void initTestCase();
0040 
0041 private Q_SLOTS:
0042     void testGuiConnection();
0043     void testActiveSkills();
0044     void testSessionData();
0045     void testChangeSessionData();
0046     void testShowGui();
0047     void testClientToServerData();
0048     void testShowSecondGuiPage();
0049     void testEventsFromServer();
0050     void testEventsFromClient();
0051     void testMoveGuiPage();
0052     void testRemoveGuiPage();
0053     void testSwitchSkill();
0054 
0055 private:
0056     AbstractDelegate *delegateForSkill(const QString &skill, const QUrl &url);
0057     QList <AbstractDelegate *>delegatesForSkill(const QString &skill);
0058 
0059     //Client
0060     MycroftController *m_controller;
0061     AbstractSkillView *m_view;
0062 
0063     QQuickView *m_window;
0064 
0065     //Server
0066     QWebSocketServer *m_mainServerSocket;
0067     QWebSocketServer *m_guiServerSocket;
0068 
0069     QWebSocket *m_mainWebSocket;
0070     QWebSocket *m_guiWebSocket;
0071 };
0072 
0073 
0074 static QObject *fileReaderSingletonProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
0075 {
0076     Q_UNUSED(engine)
0077     Q_UNUSED(scriptEngine)
0078 
0079     return new FileReader;
0080 }
0081 
0082 static QObject *globalSettingsSingletonProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
0083 {
0084     Q_UNUSED(engine)
0085     Q_UNUSED(scriptEngine)
0086 
0087     return new GlobalSettings;
0088 }
0089 
0090 static QObject *mycroftControllerSingletonProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
0091 {
0092     Q_UNUSED(engine);
0093     Q_UNUSED(scriptEngine);
0094 
0095     return MycroftController::instance();
0096 }
0097 
0098 
0099 
0100 AbstractDelegate *ServerTest::delegateForSkill(const QString &skill, const QUrl &url)
0101 {
0102     DelegatesModel *delegatesModel = m_view->activeSkills()->delegatesModelForSkill(skill);
0103     if (!delegatesModel) {
0104         return nullptr;
0105     }
0106 
0107     for (auto d : delegatesModel->delegates()) {
0108         if (d->qmlUrl() == url) {
0109             return d;
0110         }
0111     }
0112     return nullptr;
0113 }
0114 
0115 QList <AbstractDelegate *>ServerTest::delegatesForSkill(const QString &skill)
0116 {
0117     DelegatesModel *delegatesModel = m_view->activeSkills()->delegatesModelForSkill(skill);
0118 
0119     if (!delegatesModel) {
0120         return {};
0121     }
0122     return delegatesModel->delegates();
0123 }
0124 
0125 void ServerTest::initTestCase()
0126 {
0127     m_mainServerSocket = new QWebSocketServer(QStringLiteral("core"),
0128                                             QWebSocketServer::NonSecureMode, this);
0129     m_mainServerSocket->listen(QHostAddress::Any, 8181);
0130     m_guiServerSocket = new QWebSocketServer(QStringLiteral("gui"),
0131                                             QWebSocketServer::NonSecureMode, this);
0132     m_guiServerSocket->listen(QHostAddress::Any, 1818);
0133     m_controller = MycroftController::instance();
0134     //TODO: delete
0135     //m_view = new AbstractSkillView;
0136     m_window = new QQuickView;
0137     bool pluginFound = false;
0138     for (const auto &path : m_window->engine()->importPathList()) {
0139         QDir importDir(path);
0140         if (importDir.entryList().contains(QStringLiteral("Mycroft"))) {
0141             pluginFound = true;
0142             break;
0143         }
0144     }
0145 
0146     if (!pluginFound) {
0147         qmlRegisterSingletonType<MycroftController>("Mycroft", 1, 0, "MycroftController", mycroftControllerSingletonProvider);
0148         qmlRegisterSingletonType<GlobalSettings>("Mycroft", 1, 0, "GlobalSettings", globalSettingsSingletonProvider);
0149         qmlRegisterSingletonType<FileReader>("Mycroft", 1, 0, "FileReader", fileReaderSingletonProvider);
0150         qmlRegisterType<AbstractSkillView>("Mycroft", 1, 0, "AbstractSkillView");
0151         qmlRegisterType<AbstractDelegate>("Mycroft", 1, 0, "AbstractDelegate");
0152 
0153         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/AudioPlayer.qml")), "Mycroft", 1, 0, "AudioPlayer");
0154         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/AutoFitLabel.qml")), "Mycroft", 1, 0, "AutoFitLabel");
0155         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/Delegate.qml")), "Mycroft", 1, 0, "Delegate");
0156         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/PaginatedText.qml")), "Mycroft", 1, 0, "PaginatedText");
0157         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/ProportionalDelegate.qml")), "Mycroft", 1, 0, "ProportionalDelegate");
0158         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/ScrollableDelegate.qml")), "Mycroft", 1, 0, "ScrollableDelegate");
0159         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/SkillView.qml")), "Mycroft", 1, 0, "SkillView");
0160         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/SlideShow.qml")), "Mycroft", 1, 0, "SlideShow");
0161         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/SlidingImage.qml")), "Mycroft", 1, 0, "SlidingImage");
0162         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/StatusIndicator.qml")), "Mycroft", 1, 0, "StatusIndicator");
0163         qmlRegisterType(QUrl(QStringLiteral("qrc:/qml/VideoPlayer.qml")), "Mycroft", 1, 0, "VideoPlayer");
0164 
0165         qmlRegisterUncreatableType<ActiveSkillsModel>("Mycroft", 1, 0, "ActiveSkillsModel", QStringLiteral("You cannot instantiate items of type ActiveSkillsModel"));
0166         qmlRegisterUncreatableType<DelegatesModel>("Mycroft", 1, 0, "DelegatesModel", QStringLiteral("You cannot instantiate items of type DelegatesModel"));
0167         qmlRegisterUncreatableType<SessionDataMap>("Mycroft", 1, 0, "SessionDataMap", QStringLiteral("You cannot instantiate items of type SessionDataMap"));
0168 
0169         qmlRegisterType(QUrl::fromLocalFile(QFINDTESTDATA(QStringLiteral("../import/qml/Delegate.qml"))), "Mycroft", 1, 0, "Delegate");
0170 
0171         qmlProtectModule("Mycroft", 1);
0172     }
0173 
0174     m_window->setResizeMode(QQuickView::SizeRootObjectToView);
0175     m_window->resize(400, 800);
0176 
0177     //Load the AbstractSkillview from our specialization in QML
0178     m_window->setSource(QUrl::fromLocalFile(QFINDTESTDATA("../import/qml/SkillView.qml")));
0179     if (m_window->errors().length() > 0) {
0180         qWarning() << m_window->errors();
0181     }
0182     m_window->show();
0183     m_view = qobject_cast<AbstractSkillView *>(m_window->rootObject());
0184     QVERIFY(m_view);
0185 
0186     new QAbstractItemModelTester(m_view->activeSkills(), QAbstractItemModelTester::FailureReportingMode::QtTest, this);
0187 }
0188 
0189 //TODO: test a spotty connection
0190 void ServerTest::testGuiConnection()
0191 {
0192     QSignalSpy newConnectionSpy(m_mainServerSocket, &QWebSocketServer::newConnection);
0193     QSignalSpy controllerSocketStatusChangedSpy(m_controller, &MycroftController::socketStatusChanged);
0194     m_controller->start();
0195 
0196     //wait the server received a connection and the client got connected state
0197     newConnectionSpy.wait();
0198 
0199     m_mainWebSocket = m_mainServerSocket->nextPendingConnection();
0200     QSignalSpy textFromMainSpy(m_mainWebSocket, &QWebSocket::textMessageReceived);
0201     QVERIFY(m_mainWebSocket);
0202 
0203     controllerSocketStatusChangedSpy.wait();
0204     QCOMPARE(m_controller->status(), MycroftController::Open);
0205 
0206     textFromMainSpy.wait();
0207     auto doc = QJsonDocument::fromJson(textFromMainSpy.first().first().toString().toLatin1());
0208     auto type = doc[QStringLiteral("type")].toString();
0209     auto guiId = doc[QStringLiteral("data")][QStringLiteral("gui_id")].toString();
0210 
0211     QCOMPARE(type, QStringLiteral("mycroft.gui.connected"));
0212     QVERIFY(guiId.length() > 0);
0213 
0214     //Now, connect the gui
0215     QSignalSpy newGuiConnectionSpy(m_guiServerSocket, &QWebSocketServer::newConnection);
0216     m_mainWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.gui.port\", \"data\": {\"gui_id\": \"%1\", \"port\": 1818}}").arg(guiId));
0217     newGuiConnectionSpy.wait();
0218     m_guiWebSocket = m_guiServerSocket->nextPendingConnection();
0219     QVERIFY(m_guiWebSocket);
0220 }
0221 
0222 void ServerTest::testActiveSkills()
0223 {
0224     QSignalSpy skillInsertedSpy(m_view->activeSkills(), &ActiveSkillsModel::rowsInserted);
0225     QSignalSpy skillMovedSpy(m_view->activeSkills(), &ActiveSkillsModel::rowsMoved);
0226     QSignalSpy skillRemovedSpy(m_view->activeSkills(), &ActiveSkillsModel::rowsRemoved);
0227 
0228     QCOMPARE(m_view->activeSkills()->rowCount(), 0);
0229 
0230     //Add weather skill
0231     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.insert\", \"namespace\": \"mycroft.system.active_skills\", \"position\": 0, \"data\": [{\"skill_id\": \"mycroft.weather\"}]}"));
0232 
0233     skillInsertedSpy.wait();
0234 
0235     QCOMPARE(m_view->activeSkills()->rowCount(), 1);
0236     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0237 
0238     //Add food-wizard skill, before weather
0239     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.insert\", \"namespace\": \"mycroft.system.active_skills\", \"position\": 0, \"data\": [{\"skill_id\": \"aiix.food-wizard\"}]}"));
0240 
0241     skillInsertedSpy.wait();
0242 
0243     QCOMPARE(m_view->activeSkills()->rowCount(), 2);
0244     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0245     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0246 
0247     //Add timer skill, between food-wizard and weather
0248     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.insert\", \"namespace\": \"mycroft.system.active_skills\", \"position\": 1, \"data\": [{\"skill_id\": \"mycroft.timer\"}]}"));
0249 
0250     skillInsertedSpy.wait();
0251 
0252     QCOMPARE(m_view->activeSkills()->rowCount(), 3);
0253     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0254     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.timer"));
0255     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(2,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0256 
0257     //Add shopping skill, wiki and weather in the end: weather will be ignored as is already present
0258     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.insert\", \"namespace\": \"mycroft.system.active_skills\", \"position\": 3, \"data\": [{\"skill_id\": \"aiix.shopping-demo\"}, {\"skill_id\": \"mycroft.wiki\"}, {\"skill_id\": \"mycroft.weather\"}]}"));
0259 
0260     skillInsertedSpy.wait();
0261 
0262     //5 because weather was ignored
0263     QCOMPARE(m_view->activeSkills()->rowCount(), 5);
0264     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0265     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.timer"));
0266     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(2,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0267     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(3,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.shopping-demo"));
0268     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(4,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.wiki"));
0269 
0270     //Move timer in first position
0271     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.move\", \"namespace\": \"mycroft.system.active_skills\", \"from\": 2, \"to\": 1, \"items_number\": 1}"));
0272 
0273     skillMovedSpy.wait();
0274 
0275     QCOMPARE(m_view->activeSkills()->rowCount(), 5);
0276     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0277     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0278     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(2,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.timer"));
0279     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(3,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.shopping-demo"));
0280     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(4,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.wiki"));
0281 
0282     //Move weather and food-wizard in front
0283     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.move\", \"namespace\": \"mycroft.system.active_skills\", \"from\": 1, \"to\": 0, \"items_number\": 2}"));
0284 
0285     skillMovedSpy.wait();
0286 
0287     QCOMPARE(m_view->activeSkills()->rowCount(), 5);
0288     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0289     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.timer"));
0290     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(2,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0291     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(3,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.shopping-demo"));
0292     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(4,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.wiki"));
0293 
0294     //Move timer and food-wizard in the back
0295     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.move\", \"namespace\": \"mycroft.system.active_skills\", \"from\": 1, \"to\": 4, \"items_number\": 2}"));
0296 
0297     skillMovedSpy.wait();
0298 
0299     QCOMPARE(m_view->activeSkills()->rowCount(), 5);
0300     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0301     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.shopping-demo"));
0302     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(2,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.timer"));
0303     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(3,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0304     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(4,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.wiki"));
0305 
0306     //Remove shopping-demo and timer
0307     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.remove\", \"namespace\": \"mycroft.system.active_skills\", \"position\": 1, \"items_number\": 2}"));
0308 
0309     skillRemovedSpy.wait();
0310 
0311     QCOMPARE(m_view->activeSkills()->rowCount(), 3);
0312     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(0,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.weather"));
0313     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(1,0), ActiveSkillsModel::SkillId), QStringLiteral("aiix.food-wizard"));
0314     QCOMPARE(m_view->activeSkills()->data(m_view->activeSkills()->index(2,0), ActiveSkillsModel::SkillId), QStringLiteral("mycroft.wiki"));
0315 }
0316 
0317 void ServerTest::testSessionData()
0318 {
0319     SessionDataMap *map = m_view->sessionDataForSkill(QStringLiteral("mycroft.weather"));
0320     QVERIFY(map);
0321     //No sessiondata for invalid skills
0322     QVERIFY(!m_view->sessionDataForSkill(QStringLiteral("invalidskillid")));
0323 
0324     QSignalSpy dataChangedSpy(map, &SessionDataMap::valueChanged);
0325 
0326     //set data for weather skill
0327     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.set\", \"namespace\": \"mycroft.weather\", \"data\": {\"temperature\": \"28°C\", \"icon\": \"weather-clear\", \"forecast\":[{\"when\": \"Monday\", \"temperature\": \"13°C\", \"icon\": \"weather-clouds\"}, {\"when\": \"Tuesday\", \"temperature\": \"24°C\", \"icon\": \"overcast\"}, {\"when\": \"Wednesday\", \"temperature\": \"22°C\", \"icon\": \"weather-showers-day\"}]}}"));
0328 
0329     dataChangedSpy.wait();
0330     QCOMPARE(dataChangedSpy.count(), 3);
0331 
0332     QCOMPARE(map->keys().count(), 3);
0333     QCOMPARE(map->value(QStringLiteral("temperature")), QStringLiteral("28°C"));
0334     QCOMPARE(map->value(QStringLiteral("icon")), QStringLiteral("weather-clear"));
0335 
0336     //Verify the model contents, setting the whole list means reset of the model
0337     SessionDataModel *dm = map->value(QStringLiteral("forecast")).value<SessionDataModel *>();
0338     QVERIFY(dm);
0339     new QAbstractItemModelTester(dm, QAbstractItemModelTester::FailureReportingMode::QtTest, this);
0340     QCOMPARE(dm->rowCount(), 3);
0341 
0342     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Monday"));
0343     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("13°C"));
0344     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clouds"));
0345 
0346     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Tuesday"));
0347     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("24°C"));
0348     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("overcast"));
0349 
0350     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Wednesday"));
0351     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("22°C"));
0352     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-showers-day"));
0353 }
0354 
0355 void ServerTest::testChangeSessionData()
0356 {
0357     SessionDataMap *map = m_view->sessionDataForSkill(QStringLiteral("mycroft.weather"));
0358     QVERIFY(map);
0359     QVERIFY(!m_view->sessionDataForSkill(QStringLiteral("invalidskillid")));
0360 
0361     QSignalSpy dataChangedSpy(map, &SessionDataMap::valueChanged);
0362     QSignalSpy dataClearedSpy(map, &SessionDataMap::dataCleared);
0363 
0364     //set data for weather skill
0365     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.set\", \"namespace\": \"mycroft.weather\", \"data\": {\"temperature\": \"24°C\", \"otherproperty\": \"value\"}}"));
0366 
0367     dataChangedSpy.wait();
0368     QCOMPARE(dataChangedSpy.count(), 2);
0369 
0370     //keys are alphabetically ordered
0371     QCOMPARE(map->keys().count(), 4);
0372     QCOMPARE(map->value(QStringLiteral("temperature")), QStringLiteral("24°C"));
0373     QCOMPARE(map->value(QStringLiteral("icon")), QStringLiteral("weather-clear"));
0374     QCOMPARE(map->value(QStringLiteral("otherproperty")), QStringLiteral("value"));
0375 
0376     //remove otherproperty
0377     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.delete\", \"namespace\": \"mycroft.weather\", \"property\": \"otherproperty\"}"));
0378 
0379     dataClearedSpy.wait();
0380     QCOMPARE(dataClearedSpy.first().first(), QStringLiteral("otherproperty"));
0381     //is not possible to actually remove a key
0382     QCOMPARE(map->keys().count(), 4);
0383     QCOMPARE(map->value(QStringLiteral("temperature")), QStringLiteral("24°C"));
0384     QCOMPARE(map->value(QStringLiteral("icon")), QStringLiteral("weather-clear"));
0385     QCOMPARE(map->value(QStringLiteral("otherproperty")), QVariant());
0386 
0387     //Change a value in the model of forecasts
0388     SessionDataModel *dm = map->value(QStringLiteral("forecast")).value<SessionDataModel *>();
0389     QVERIFY(dm);
0390     QSignalSpy modelDataChangedSpy(dm, &SessionDataModel::dataChanged);
0391     QSignalSpy modelDataInsertedSpy(dm, &SessionDataModel::rowsInserted);
0392     QSignalSpy modelDataMovedSpy(dm, &SessionDataModel::rowsMoved);
0393     QSignalSpy modelDataRemovedSpy(dm, &SessionDataModel::rowsRemoved);
0394     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.update\", \"namespace\": \"mycroft.weather\", \"property\": \"forecast\", \"position\": 1, \"data\": [{\"temperature\": \"30°C\", \"icon\": \"weather-clear\", \"to_delete\": \"value to delete\"}]}"));
0395     modelDataChangedSpy.wait();
0396 
0397     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("30°C"));
0398     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clear"));
0399 
0400     //Insert a new value in the forecasts model
0401     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.insert\", \"namespace\": \"mycroft.weather\", \"property\": \"forecast\", \"position\": 1, \"data\": [{\"when\": \"Thursday\", \"temperature\": \"2°C\", \"icon\": \"weather-snow\"}, {\"when\": \"Friday\", \"temperature\": \"12°C\", \"icon\": \"weather-few-clouds\"}]}"));
0402     modelDataInsertedSpy.wait();
0403 
0404     QCOMPARE(dm->rowCount(), 5);
0405 
0406     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Monday"));
0407     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("13°C"));
0408     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clouds"));
0409 
0410     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Thursday"));
0411     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("2°C"));
0412     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-snow"));
0413 
0414     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Friday"));
0415     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("12°C"));
0416     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-few-clouds"));
0417 
0418     QCOMPARE(dm->data(dm->index(3, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Tuesday"));
0419     QCOMPARE(dm->data(dm->index(3, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("30°C"));
0420     QCOMPARE(dm->data(dm->index(3, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clear"));
0421 
0422     QCOMPARE(dm->data(dm->index(4, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Wednesday"));
0423     QCOMPARE(dm->data(dm->index(4, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("22°C"));
0424     QCOMPARE(dm->data(dm->index(4, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-showers-day"));
0425 
0426     //Move Thursday and Friday at the bottom of the list
0427     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.move\", \"namespace\": \"mycroft.weather\", \"property\": \"forecast\", \"from\": 1, \"to\": 5, \"items_number\": 2}"));
0428 
0429     modelDataMovedSpy.wait();
0430     QCOMPARE(dm->rowCount(), 5);
0431     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Monday"));
0432     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("13°C"));
0433     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clouds"));
0434 
0435     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Tuesday"));
0436     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("30°C"));
0437     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clear"));
0438 
0439     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Wednesday"));
0440     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("22°C"));
0441     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-showers-day"));
0442 
0443     QCOMPARE(dm->data(dm->index(3, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Thursday"));
0444     QCOMPARE(dm->data(dm->index(3, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("2°C"));
0445     QCOMPARE(dm->data(dm->index(3, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-snow"));
0446 
0447     QCOMPARE(dm->data(dm->index(4, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Friday"));
0448     QCOMPARE(dm->data(dm->index(4, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("12°C"));
0449     QCOMPARE(dm->data(dm->index(4, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-few-clouds"));
0450 
0451     //Remove Thursday and Friday from the
0452     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.remove\", \"namespace\": \"mycroft.weather\", \"property\": \"forecast\", \"position\": 3, \"items_number\": 2}"));
0453     modelDataRemovedSpy.wait();
0454 
0455     QCOMPARE(dm->rowCount(), 3);
0456     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Monday"));
0457     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("13°C"));
0458     QCOMPARE(dm->data(dm->index(0, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clouds"));
0459 
0460     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Tuesday"));
0461     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("30°C"));
0462     QCOMPARE(dm->data(dm->index(1, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-clear"));
0463 
0464     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("when")).toString(), QStringLiteral("Wednesday"));
0465     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("temperature")).toString(), QStringLiteral("22°C"));
0466     QCOMPARE(dm->data(dm->index(2, 0), dm->roleNames().key("icon")).toString(), QStringLiteral("weather-showers-day"));
0467 }
0468 
0469 void ServerTest::testShowGui()
0470 {
0471     QSignalSpy skillModelDataChangedSpy(m_view->activeSkills(), &ActiveSkillsModel::dataChanged);
0472 
0473     const QUrl url(QStringLiteral("file://") + QFINDTESTDATA("currentweather.qml"));
0474 
0475     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.gui.list.insert\", \"namespace\": \"mycroft.weather\", \"position\": 0, \"data\": [{\"url\": \"") + url.toString() + QStringLiteral("\"}]}"));
0476 
0477     skillModelDataChangedSpy.wait();
0478 
0479     AbstractDelegate *delegate = delegateForSkill(QStringLiteral("mycroft.weather"), url);
0480     QVERIFY(delegate);
0481     QCOMPARE(delegate->skillId(), QStringLiteral("mycroft.weather"));
0482     QCOMPARE(delegate->qmlUrl(), url);
0483 
0484     //check the delegate has the proper data associated
0485     SessionDataMap *map = delegate->sessionData();
0486     QVERIFY(map);
0487     QCOMPARE(map->keys().count(), 4);
0488     QCOMPARE(map->value(QStringLiteral("temperature")), QStringLiteral("24°C"));
0489     QCOMPARE(map->value(QStringLiteral("icon")), QStringLiteral("weather-clear"));
0490     QCOMPARE(map->value(QStringLiteral("otherproperty")), QVariant());
0491 
0492     //try to get the delegate via the model, like qml will do and check they're the same
0493     DelegatesModel *dm = m_view->activeSkills()->data(m_view->activeSkills()->index(0, 0), ActiveSkillsModel::Delegates).value<DelegatesModel *>();
0494     QVERIFY(dm);
0495     new QAbstractItemModelTester(dm, QAbstractItemModelTester::FailureReportingMode::QtTest, this);
0496     AbstractDelegate *delegate2 = dm->data(dm->index(0, 0), DelegatesModel::DelegateUi).value<AbstractDelegate *>();
0497     QCOMPARE(delegate, delegate2);
0498 }
0499 
0500 void ServerTest::testClientToServerData()
0501 {
0502     const QUrl url(QStringLiteral("file://") + QFINDTESTDATA("currentweather.qml"));
0503     AbstractDelegate *delegate = delegateForSkill(QStringLiteral("mycroft.weather"), url);
0504     QVERIFY(delegate);
0505 
0506     QSignalSpy propertySpy(m_guiWebSocket, &QWebSocket::textMessageReceived);
0507 
0508     QMetaObject::invokeMethod(delegate, "updateTemperature", Qt::DirectConnection, Q_ARG(QVariant, QStringLiteral("21 °C")));
0509     propertySpy.wait();
0510 
0511     QJsonDocument doc = QJsonDocument::fromJson(propertySpy.first().first().toString().toUtf8());
0512 
0513     QVERIFY(!doc.isEmpty());
0514     QCOMPARE(doc[QStringLiteral("type")], QStringLiteral("mycroft.session.set"));
0515     QCOMPARE(doc[QStringLiteral("namespace")], QStringLiteral("mycroft.weather"));
0516     QCOMPARE(doc[QStringLiteral("data")][QStringLiteral("temperature")], QStringLiteral("21 °C"));
0517 
0518     QMetaObject::invokeMethod(delegate, "deleteProperty");
0519     propertySpy.wait();
0520     doc = QJsonDocument::fromJson(propertySpy[1].first().toString().toUtf8());
0521 
0522     QCOMPARE(doc[QStringLiteral("type")], QStringLiteral("mycroft.session.delete"));
0523     QCOMPARE(doc[QStringLiteral("namespace")], QStringLiteral("mycroft.weather"));
0524     QCOMPARE(doc[QStringLiteral("property")], QStringLiteral("to_delete"));
0525 }
0526 
0527 void ServerTest::testShowSecondGuiPage()
0528 {
0529     QSignalSpy skillModelDataChangedSpy(m_view->activeSkills(), &ActiveSkillsModel::dataChanged);
0530 
0531     const QUrl url(QStringLiteral("file://") + QFINDTESTDATA("forecast.qml"));
0532     //wait a moment before showing it there
0533     QTest::qWait(2000);
0534     
0535     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.gui.list.insert\", \"namespace\": \"mycroft.weather\", \"position\": 1, \"data\": [{\"url\": \"") + url.toString() + QStringLiteral("\"}]}"));
0536 
0537     skillModelDataChangedSpy.wait();
0538 
0539     AbstractDelegate *delegate = delegateForSkill(QStringLiteral("mycroft.weather"), url);
0540     QVERIFY(delegate);
0541     QCOMPARE(delegate->skillId(), QStringLiteral("mycroft.weather"));
0542     QCOMPARE(delegate->qmlUrl(), url);
0543 }
0544 
0545 void ServerTest::testEventsFromServer()
0546 {
0547     AbstractDelegate *delegate = delegatesForSkill(QStringLiteral("mycroft.weather")).first();
0548     QVERIFY(delegate);
0549     QCOMPARE(delegate->skillId(), QStringLiteral("mycroft.weather"));
0550 
0551     QSignalSpy eventSpy(delegate, &AbstractDelegate::guiEvent);
0552 
0553     //An event of the weather skill
0554     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.events.triggered\", \"namespace\": \"mycroft.weather\", \"event_name\": \"show_alert\", \"data\": {\"alert_name\": \"blizzard\", \"condition\": \"severe\"}}"));
0555 
0556     eventSpy.wait();
0557     QCOMPARE(eventSpy.count(), 1);
0558     QCOMPARE(eventSpy.first().first(), QStringLiteral("show_alert"));
0559     QCOMPARE(eventSpy.first()[1].value<QVariantMap>()[QStringLiteral("alert_name")], QStringLiteral("blizzard"));
0560     QCOMPARE(eventSpy.first()[1].value<QVariantMap>()[QStringLiteral("condition")], QStringLiteral("severe"));
0561 
0562     //A system event
0563     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.events.triggered\", \"namespace\": \"system\", \"event_name\": \"system.next\", \"data\": {}}"));
0564 
0565     eventSpy.wait();
0566     QCOMPARE(eventSpy.count(), 2);
0567     QCOMPARE(eventSpy[1].first(), QStringLiteral("system.next"));
0568 
0569     //An event for the wiki skill: the weather one will never receive it
0570     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.events.triggered\", \"namespace\": \"mycroft.wiki\", \"event_name\": \"new_data\", \"data\": {}}"));
0571 
0572     eventSpy.wait(1000);
0573     QCOMPARE(eventSpy.count(), 2);
0574 
0575     //view switches again to current
0576     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.events.triggered\", \"namespace\": \"mycroft.weather\", \"event_name\": \"page_gained_focus\", \"data\": {\"number\": 0}}"));
0577 
0578     //Wait a moment before messing with the gui
0579     QTest::qWait(1000);
0580 }
0581 
0582 void ServerTest::testEventsFromClient()
0583 {
0584     AbstractDelegate *delegate = delegatesForSkill(QStringLiteral("mycroft.weather")).first();
0585     QVERIFY(delegate);
0586     QCOMPARE(delegate->skillId(), QStringLiteral("mycroft.weather"));
0587 
0588     QSignalSpy eventSpy(m_guiWebSocket, &QWebSocket::textMessageReceived);
0589 
0590     //skill own event
0591     delegate->triggerGuiEvent(QStringLiteral("mycroft.weather.refresh_forecast"), QVariantMap({{QStringLiteral("when"), QStringLiteral("Monday")}}));
0592     eventSpy.wait();
0593 
0594     QJsonDocument doc = QJsonDocument::fromJson(eventSpy.first().first().toString().toUtf8());
0595     QVERIFY(!doc.isEmpty());
0596     QCOMPARE(doc[QStringLiteral("type")], QStringLiteral("mycroft.events.triggered"));
0597     QCOMPARE(doc[QStringLiteral("namespace")], QStringLiteral("mycroft.weather"));
0598     QCOMPARE(doc[QStringLiteral("event_name")], QStringLiteral("mycroft.weather.refresh_forecast"));
0599     QCOMPARE(doc[QStringLiteral("parameters")][QStringLiteral("when")], QStringLiteral("Monday"));
0600 
0601     //system event
0602     delegate->triggerGuiEvent(QStringLiteral("system.next"), QVariantMap());
0603     eventSpy.wait();
0604 
0605     doc = QJsonDocument::fromJson(eventSpy[1].first().toString().toUtf8());
0606     QVERIFY(!doc.isEmpty());
0607     QCOMPARE(doc[QStringLiteral("type")], QStringLiteral("mycroft.events.triggered"));
0608     QCOMPARE(doc[QStringLiteral("event_name")], QStringLiteral("system.next"));
0609     QCOMPARE(doc[QStringLiteral("namespace")], QStringLiteral("system"));
0610 
0611     QCOMPARE(eventSpy.count(), 2);
0612     //Wait a moment before messing with the gui
0613     QTest::qWait(1000);
0614 }
0615 
0616 
0617 void ServerTest::testMoveGuiPage()
0618 {
0619     QUrl currentUrl = QUrl::fromLocalFile(QFINDTESTDATA("currentweather.qml"));
0620     QUrl forecastUrl = QUrl::fromLocalFile(QFINDTESTDATA("forecast.qml"));
0621 
0622     DelegatesModel *delegatesModel = m_view->activeSkills()->delegatesModelForSkill(QStringLiteral("mycroft.weather"));
0623     QVERIFY(delegatesModel);
0624     
0625     QSignalSpy rowsMovedSpy(delegatesModel, &DelegatesModel::rowsMoved);
0626 
0627     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.gui.list.move\", \"namespace\": \"mycroft.weather\", \"items_number\": 1, \"from\": 1, \"to\": 0}"));
0628     QTest::qWait(1000);
0629 
0630     rowsMovedSpy.wait();
0631 
0632     AbstractDelegate *delegate = delegatesModel->data(delegatesModel->index(0,0), DelegatesModel::DelegateUi).value<AbstractDelegate *>();
0633     QVERIFY(delegate);
0634     QCOMPARE(delegate->qmlUrl(), forecastUrl);
0635 
0636     AbstractDelegate *delegate2 = delegatesModel->data(delegatesModel->index(1,0), DelegatesModel::DelegateUi).value<AbstractDelegate *>();
0637     QVERIFY(delegate2);
0638     QCOMPARE(delegate2->qmlUrl(), currentUrl);
0639 }
0640 
0641 void ServerTest::testRemoveGuiPage()
0642 {
0643     QUrl currentUrl = QUrl::fromLocalFile(QFINDTESTDATA("currentweather.qml"));
0644     QUrl forecastUrl = QUrl::fromLocalFile(QFINDTESTDATA("forecast.qml"));
0645 
0646     AbstractDelegate *delegate = delegateForSkill(QStringLiteral("mycroft.weather"), forecastUrl);
0647 
0648     DelegatesModel *delegatesModel = m_view->activeSkills()->delegatesModelForSkill(QStringLiteral("mycroft.weather"));
0649     QVERIFY(delegatesModel);
0650     QCOMPARE(delegatesModel->rowCount(), 2);
0651     
0652     QSignalSpy rowsRemovedSpy(delegatesModel, &DelegatesModel::rowsRemoved);
0653     QSignalSpy destroyedSpy(delegate, &QObject::destroyed);
0654 
0655     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.gui.list.remove\", \"namespace\": \"mycroft.weather\", \"items_number\": 1, \"position\": 0}"));
0656     QTest::qWait(1000);
0657 
0658     rowsRemovedSpy.wait();
0659 
0660     QCOMPARE(delegatesModel->rowCount(), 1);
0661     delegate = delegatesModel->data(delegatesModel->index(0,0), DelegatesModel::DelegateUi).value<AbstractDelegate *>();
0662     QVERIFY(delegate);
0663     QCOMPARE(delegate->qmlUrl(), currentUrl);
0664 
0665     destroyedSpy.wait();
0666     QCOMPARE(destroyedSpy.count(), 1);
0667 }
0668 
0669 void ServerTest::testSwitchSkill()
0670 {
0671     SessionDataMap *map = m_view->sessionDataForSkill(QStringLiteral("mycroft.wiki"));
0672     QVERIFY(map);
0673 
0674     DelegatesModel *delegatesModel = m_view->activeSkills()->delegatesModelForSkill(QStringLiteral("mycroft.wiki"));
0675     QVERIFY(delegatesModel);
0676 
0677     QSignalSpy dataChangedSpy(map, &SessionDataMap::valueChanged);
0678     QSignalSpy skillMovedSpy(m_view->activeSkills(), &ActiveSkillsModel::rowsMoved);
0679     QSignalSpy delegateInsertedSpy(delegatesModel, &DelegatesModel::rowsInserted);
0680 
0681     QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("wiki.qml"));
0682 
0683     //set wiki skill as active
0684      m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.list.move\", \"namespace\": \"mycroft.system.active_skills\", \"from\": 2, \"to\": 0, \"items_number\": 1}"));
0685     skillMovedSpy.wait();
0686 
0687     //set data for wiki skill
0688     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.session.set\", \"namespace\": \"mycroft.wiki\", \"data\": {\"title\": \"Mycroft\", \"text\": \"Mycroft is a free and open-source voice assistant for Linux-based operating systems that uses a natural language user interface.\", \"image\":\"https://upload.wikimedia.org/wikipedia/en/f/f1/Mycroft_logo.png\"}}"));
0689     dataChangedSpy.wait();
0690 
0691     //show wiki gui
0692     m_guiWebSocket->sendTextMessage(QStringLiteral("{\"type\": \"mycroft.gui.list.insert\", \"namespace\": \"mycroft.wiki\", \"position\": 0, \"data\": [{\"url\": \"") + url.toString() + QStringLiteral("\"}]}"));
0693     delegateInsertedSpy.wait();
0694     
0695 
0696     //wait a moment before quitting
0697     QTest::qWait(3000);
0698 }
0699 
0700 QTEST_MAIN(ServerTest);
0701 
0702 #include "servertest.moc"