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