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"