File indexing completed on 2024-04-21 14:47:20
0001 /* KStars UI tests 0002 SPDX-FileCopyrightText: 2018, 2020 Csaba Kertesz <csaba.kertesz@gmail.com> 0003 SPDX-FileCopyrightText: Jasem Mutlaq <knro@ikarustech.com> 0004 SPDX-FileCopyrightText: Eric Dejouhanet <eric.dejouhanet@gmail.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "test_ekos.h" 0010 0011 #if defined(HAVE_INDI) 0012 0013 #include "kstars_ui_tests.h" 0014 #include "test_kstars_startup.h" 0015 0016 #include "ekos/manager.h" 0017 #include "ekos/profileeditor.h" 0018 #include "kstars.h" 0019 #include "auxiliary/ksmessagebox.h" 0020 0021 #include <KActionCollection> 0022 #include <KTipDialog> 0023 #include <KCrash/KCrash> 0024 0025 #include <QFuture> 0026 #include <QtConcurrentRun> 0027 #include <QTest> 0028 #include <QPoint> 0029 #include <QModelIndex> 0030 0031 #include <ctime> 0032 #include <unistd.h> 0033 0034 TestEkos::TestEkos(QObject *parent): QObject(parent) 0035 { 0036 0037 } 0038 0039 void TestEkos::initTestCase() 0040 { 0041 } 0042 0043 void TestEkos::cleanupTestCase() 0044 { 0045 } 0046 0047 void TestEkos::init() 0048 { 0049 KTEST_BEGIN(); 0050 KTRY_OPEN_EKOS(); 0051 KVERIFY_EKOS_IS_OPENED(); 0052 } 0053 0054 void TestEkos::cleanup() 0055 { 0056 foreach (QDialog * d, KStars::Instance()->findChildren<QDialog*>()) 0057 if (d->isVisible()) 0058 d->hide(); 0059 0060 KTRY_CLOSE_EKOS(); 0061 KVERIFY_EKOS_IS_HIDDEN(); 0062 KTEST_END(); 0063 } 0064 0065 void TestEkos::testOpenClose() 0066 { 0067 /* No-op, we just use init+cleanup */ 0068 } 0069 0070 void TestEkos::testSimulatorProfile() 0071 { 0072 Ekos::Manager * const ekos = Ekos::Manager::Instance(); 0073 0074 // --------- First step: selecting the Simulators profile 0075 0076 // Verify that the test profile exists, and select it 0077 QString const p("Simulators"); 0078 QComboBox* profileCBox = Ekos::Manager::Instance()->findChild<QComboBox*>("profileCombo"); 0079 QVERIFY(profileCBox != nullptr); 0080 profileCBox->setCurrentText(p); 0081 QTRY_COMPARE(profileCBox->currentText(), p); 0082 0083 // --------- Second step: starting Ekos with the Simulators profile 0084 0085 QString const buttonReadyToStart("media-playback-start"); 0086 QString const buttonReadyToStop("media-playback-stop"); 0087 0088 // Check the start button icon as visual feedback about Ekos state, and click to start Ekos 0089 QPushButton* startEkos = ekos->findChild<QPushButton*>("processINDIB"); 0090 QVERIFY(startEkos != nullptr); 0091 QVERIFY(!buttonReadyToStart.compare(startEkos->icon().name())); 0092 QTest::mouseClick(startEkos, Qt::LeftButton); 0093 0094 // --------- Third step: waiting for Ekos to finish startup 0095 0096 // The INDI property pages automatically raised on top, but as we got a handle to the button, we continue to test as is 0097 0098 // Wait until Ekos gives feedback on the INDI client startup - button changes to symbol "stop" 0099 QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStop.compare(startEkos->icon().name()), 7000); 0100 0101 // It might be that our Simulators profile is not auto-connecting and we should do that manually 0102 // We assume that by default it's not the case 0103 0104 // We have no feedback on whether all our devices are connected, so we need to hack a delay in... 0105 QWARN("HACK HACK HACK adding delay here for devices to connect"); 0106 QTest::qWait(5000); 0107 0108 // Verify the device connection button is unavailable 0109 QPushButton * connectDevices = ekos->findChild<QPushButton*>("connectB"); 0110 QVERIFY(connectDevices != nullptr); 0111 QVERIFY(!connectDevices->isEnabled()); 0112 0113 #if QT_VERSION >= 0x050800 0114 QEXPECT_FAIL("", "Ekos resets the simulation clock when starting a profile.", Continue); 0115 QCOMPARE(llround(KStars::Instance()->data()->clock()->utc().toLocalTime().toMSecsSinceEpoch()/1000.0), TestKStarsStartup::m_InitialConditions.dateTime.toSecsSinceEpoch()); 0116 #endif 0117 0118 QEXPECT_FAIL("", "Ekos resumes the simulation clock when starting a profile.", Continue); 0119 QVERIFY(!KStars::Instance()->data()->clock()->isActive()); 0120 0121 // --------- Fourth step: waiting for Ekos to finish stopping 0122 0123 // Start button that became a stop button is now disabled - we need to disconnect devices first 0124 QVERIFY(!startEkos->isEnabled()); 0125 0126 // Verify the device disconnection button is available, disconnect devices 0127 QPushButton * const b = ekos->findChild<QPushButton*>("disconnectB"); 0128 QVERIFY(b != nullptr); 0129 QVERIFY(b->isEnabled()); 0130 QTimer::singleShot(200, Ekos::Manager::Instance(), [&] 0131 { 0132 QTest::mouseClick(b, Qt::LeftButton); 0133 }); 0134 0135 // --------- Fifth step: waiting for Ekos to finish stopping 0136 0137 // Start button that became a stop button has to be available 0138 QTRY_VERIFY_WITH_TIMEOUT(startEkos->isEnabled(), 10000); 0139 0140 // Hang INDI client up 0141 QTimer::singleShot(200, ekos, [&] 0142 { 0143 QTest::mouseClick(startEkos, Qt::LeftButton); 0144 }); 0145 QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStart.compare(startEkos->icon().name()), 10000); 0146 } 0147 0148 void TestEkos::testManipulateProfiles() 0149 { 0150 // Because we don't want to manage the order of tests, we do the profile manipulation in three steps of the same test 0151 // We use that poor man's shared variable to hold the result of the first (creation) and second (edition) test step. 0152 // The ProfileEditor is exec()'d, so test code must be made asynchronous, and QTimer::singleShot is an easy way to do that. 0153 // We use two timers, one to run the end-user test, which eventually will close the dialog, and a second one to really close if the test step fails. 0154 bool testIsSuccessful = false; 0155 0156 Ekos::Manager * const ekos = Ekos::Manager::Instance(); 0157 QString testProfileName = QString("testUI%1").arg(rand() % 100000); // FIXME: Move this to fixtures 0158 0159 // --------- First step: creating the profile 0160 0161 // Because the dialog is modal, the remainder of the test is made asynchronous 0162 QTimer::singleShot(1000, ekos, [&] 0163 { 0164 // Find the Profile Editor dialog 0165 ProfileEditor* profileEditor = ekos->findChild<ProfileEditor*>("profileEditorDialog"); 0166 /* 0167 // If the name of the widget is unknown, use this snippet to look it up from its class 0168 if (profileEditor == nullptr) 0169 foreach (QWidget *w, QApplication::topLevelWidgets()) 0170 if (w->inherits("ProfileEditor")) 0171 profileEditor = qobject_cast <ProfileEditor*> (w); 0172 */ 0173 QVERIFY(profileEditor != nullptr); 0174 0175 // Create a test profile 0176 QLineEdit * const profileNameLE = profileEditor->findChild<QLineEdit*>("profileIN"); 0177 QVERIFY(nullptr != profileNameLE); 0178 profileNameLE->setText(testProfileName); 0179 QCOMPARE(profileNameLE->text(), testProfileName); 0180 0181 // Setting an item programmatically in a treeview combobox... 0182 QComboBox * const mountCBox = profileEditor->findChild<QComboBox*>("mountCombo"); 0183 QVERIFY(nullptr != mountCBox); 0184 QString lookup("Telescope Simulator"); // FIXME: Move this to fixtures 0185 // Match the text recursively in the model, this results in a model index with a parent 0186 QModelIndexList const list = mountCBox->model()->match(mountCBox->model()->index(0, 0), Qt::DisplayRole, QVariant::fromValue(lookup), 1, Qt::MatchRecursive); 0187 QVERIFY(0 < list.count()); 0188 QModelIndex const &item = list.first(); 0189 //QWARN(QString("Found text '%1' at #%2, parent at #%3").arg(item.data().toString()).arg(item.row()).arg(item.parent().row()).toStdString().data()); 0190 QCOMPARE(list.value(0).data().toString(), lookup); 0191 QVERIFY(!item.parent().parent().isValid()); 0192 // Now set the combobox model root to the match's parent 0193 mountCBox->setRootModelIndex(item.parent()); 0194 // And set the text as if the end-user had selected it 0195 mountCBox->setCurrentText(lookup); 0196 QCOMPARE(mountCBox->currentText(), lookup); 0197 0198 // Same, with a macro helper 0199 KTRY_PROFILEEDITOR_TREE_COMBOBOX(ccdCombo, "CCD Simulator"); 0200 0201 // Save the profile using the "Save" button 0202 QDialogButtonBox* buttons = profileEditor->findChild<QDialogButtonBox*>("dialogButtons"); 0203 QVERIFY(nullptr != buttons); 0204 QTest::mouseClick(buttons->button(QDialogButtonBox::Save), Qt::LeftButton); 0205 0206 testIsSuccessful = true; 0207 }); 0208 0209 // Cancel the Profile Editor dialog if the test failed - this will happen after pushing the add button below 0210 QTimer * closeDialog = new QTimer(this); 0211 closeDialog->setSingleShot(true); 0212 closeDialog->setInterval(1000); 0213 ekos->connect(closeDialog, &QTimer::timeout, [&] 0214 { 0215 ProfileEditor* profileEditor = ekos->findChild<ProfileEditor*>("profileEditorDialog"); 0216 if (profileEditor != nullptr) 0217 profileEditor->reject(); 0218 }); 0219 0220 // Click on "Add profile" button, and let the async tests run on the modal dialog 0221 QPushButton* addButton = ekos->findChild<QPushButton*>("addProfileB"); 0222 QVERIFY(addButton != nullptr); 0223 QTest::mouseClick(addButton, Qt::LeftButton); 0224 0225 // Click handler returned, stop the timer closing the dialog on failure 0226 closeDialog->stop(); 0227 delete closeDialog; 0228 0229 // Verification of the first test step 0230 QVERIFY(testIsSuccessful); 0231 testIsSuccessful = false; 0232 0233 // --------- Second step: editing and verifying the profile 0234 0235 // Verify that the test profile exists, and select it 0236 QComboBox* profileCBox = ekos->findChild<QComboBox*>("profileCombo"); 0237 QVERIFY(profileCBox != nullptr); 0238 profileCBox->setCurrentText(testProfileName); 0239 QCOMPARE(profileCBox->currentText(), testProfileName); 0240 0241 // Because the dialog is modal, the remainder of the test is made asynchronous 0242 QTimer::singleShot(200, ekos, [&] 0243 { 0244 // Find the Profile Editor dialog 0245 ProfileEditor* profileEditor = ekos->findChild<ProfileEditor*>("profileEditorDialog"); 0246 QVERIFY(profileEditor != nullptr); 0247 0248 // Verify the values set by addEkosProfileTest 0249 QLineEdit* profileNameLE = ekos->findChild<QLineEdit*>("profileIN"); 0250 QVERIFY(profileNameLE != nullptr); 0251 QCOMPARE(profileNameLE->text(), profileCBox->currentText()); 0252 0253 QComboBox* mountCBox = ekos->findChild<QComboBox*>("mountCombo"); 0254 QVERIFY(mountCBox != nullptr); 0255 QCOMPARE(mountCBox->currentText(), QString("Telescope Simulator")); 0256 0257 QComboBox* ccdCBox = ekos->findChild<QComboBox*>("ccdCombo"); 0258 QVERIFY(ccdCBox != nullptr); 0259 QCOMPARE(ccdCBox->currentText(), QString("CCD Simulator")); 0260 0261 // Cancel the dialog using the "Close" button 0262 QDialogButtonBox* buttons = profileEditor->findChild<QDialogButtonBox*>("dialogButtons"); 0263 QVERIFY(nullptr != buttons); 0264 QTest::mouseClick(buttons->button(QDialogButtonBox::Close), Qt::LeftButton); 0265 0266 testIsSuccessful = true; 0267 }); 0268 0269 // Cancel the Profile Editor dialog if the test failed - this will happen after pushing the edit button below 0270 closeDialog = new QTimer(this); 0271 closeDialog->setSingleShot(true); 0272 closeDialog->setInterval(1000); 0273 ekos->connect(closeDialog, &QTimer::timeout, [&] 0274 { 0275 ProfileEditor* profileEditor = ekos->findChild<ProfileEditor*>("profileEditorDialog"); 0276 if (profileEditor != nullptr) 0277 profileEditor->reject(); 0278 }); 0279 0280 // Click on "Edit profile" button, and let the async tests run on the modal dialog 0281 QPushButton* editButton = ekos->findChild<QPushButton*>("editProfileB"); 0282 QVERIFY(editButton != nullptr); 0283 QTest::mouseClick(editButton, Qt::LeftButton); 0284 0285 // Click handler returned, stop the timer closing the dialog on failure 0286 closeDialog->stop(); 0287 delete closeDialog; 0288 0289 // Verification of the second test step 0290 QVERIFY(testIsSuccessful); 0291 testIsSuccessful = false; 0292 0293 // Verify that the test profile still exists, and select it 0294 profileCBox = ekos->findChild<QComboBox*>("profileCombo"); 0295 QVERIFY(profileCBox != nullptr); 0296 profileCBox->setCurrentText(testProfileName); 0297 QCOMPARE(profileCBox->currentText(), testProfileName); 0298 0299 // --------- Third step: deleting the profile 0300 0301 // The yes/no modal dialog is not really used as a blocking question, but let's keep the remainder of the test is made asynchronous 0302 QTimer::singleShot(200, ekos, [&] 0303 { 0304 // This trick is from https://stackoverflow.com/questions/38596785 0305 QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() != nullptr, 1000); 0306 // This is not a regular dialog but a KStars-customized dialog, so we can't search for a yes button from QDialogButtonBox 0307 KSMessageBox * const dialog = qobject_cast <KSMessageBox*> (QApplication::activeModalWidget()); 0308 QVERIFY(dialog != nullptr); 0309 emit dialog->accept(); 0310 0311 testIsSuccessful = true; 0312 }); 0313 0314 // Click on "Remove profile" button - this will display a modal yes/no dialog 0315 QPushButton* removeButton = ekos->findChild<QPushButton*>("deleteProfileB"); 0316 QVERIFY(removeButton != nullptr); 0317 QTest::mouseClick(removeButton, Qt::LeftButton); 0318 // Pressing delete-profile triggers the open() function of the yes/no modal dialog, which returns immediately, so wait for it to disappear 0319 QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() == nullptr, 1000); 0320 0321 // Verification of the third test step 0322 QVERIFY(testIsSuccessful); 0323 testIsSuccessful = false; 0324 0325 // Verify that the test profile doesn't exist anymore 0326 profileCBox = ekos->findChild<QComboBox*>("profileCombo"); 0327 QVERIFY(profileCBox != nullptr); 0328 profileCBox->setCurrentText(testProfileName); 0329 QVERIFY(profileCBox->currentText() != testProfileName); 0330 } 0331 0332 QTEST_KSTARS_MAIN(TestEkos) 0333 0334 #endif // HAVE_INDI