/* SPDX-FileCopyrightText: 2023 Fushan Wen SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #define QT_FORCE_ASSERTS 1 #include #include #include #include #include #include "common.h" #include "waylandtasksmodel.h" using namespace TaskManager; using namespace Qt::StringLiterals; class WaylandTasksModelTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void cleanup(); void test_openCloseWindow(); void test_modelData(); void test_fullscreen(); void test_geometry(); void test_stackingOrder(); void test_modelDataFromDesktopFile(); void test_request(); // plasmashell runs out of file descriptors when emacs visits lots of files void test_bug478831(); // Sometimes non-modal windows can be missing from the taskbar void test_transientWindow(); private: WaylandTasksModel m_model; QProcess gtkWindow; }; void WaylandTasksModelTest::initTestCase() { TestUtils::initTestCase(); if (!KWindowSystem::isPlatformWayland()) { QSKIP("Test is not running on Wayland."); } QGuiApplication::setQuitOnLastWindowClosed(false); QStandardPaths::setTestModeEnabled(true); const QString applicationDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + QStringLiteral("applications"); QDir dir; if (!dir.exists(applicationDir)) { dir.mkpath(applicationDir); } QPlatformNativeInterface *const native = qGuiApp->platformNativeInterface(); wl_display_roundtrip(static_cast(native->nativeResourceForIntegration("wl_display"))); } void WaylandTasksModelTest::cleanupTestCase() { if (gtkWindow.state() == QProcess::Running) { gtkWindow.kill(); } TestUtils::cleanupTestCase(); } void WaylandTasksModelTest::cleanup() { if (gtkWindow.state() == QProcess::Running) { gtkWindow.kill(); } } void WaylandTasksModelTest::test_openCloseWindow() { TestUtils::testOpenCloseWindow(m_model); } void WaylandTasksModelTest::test_modelData() { const QString title = QStringLiteral("__testwindow__%1").arg(QDateTime::currentDateTime().toString()); QModelIndex index; auto window = TestUtils::createSingleWindow(m_model, title, index); // See WaylandTasksModel::data for available roles // Icon is unavailable on Wayland QTRY_COMPARE(index.data(AbstractTasksModel::WinIdList).toList().size(), 1); QVERIFY(index.data(AbstractTasksModel::MimeType).toString().startsWith(QLatin1String("windowsystem/winid"))); QTRY_VERIFY(index.data(AbstractTasksModel::IsWindow).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsActive).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsClosable).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsMovable).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsResizable).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsMaximizable).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::IsMaximized).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsMinimizable).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::IsKeepAbove).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::IsKeepBelow).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsFullScreenable).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::IsFullScreen).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::IsShadeable).toBool()); // It's intentionally not implemented on wayland QTRY_VERIFY(!index.data(AbstractTasksModel::IsShaded).toBool()); QTRY_VERIFY(index.data(AbstractTasksModel::IsVirtualDesktopsChangeable).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool()); // Due to window decoration, the size of a window can't be determined accurately const QRect screenGeometry = index.data(AbstractTasksModel::ScreenGeometry).toRect(); QVERIFY(screenGeometry.width() > 0 && screenGeometry.height() > 0); QTRY_VERIFY(!index.data(AbstractTasksModel::IsDemandingAttention).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::SkipTaskbar).toBool()); QTRY_VERIFY(!index.data(AbstractTasksModel::SkipPager).toBool()); // AbstractTasksModel::AppPid QVERIFY(index.data(AbstractTasksModel::CanLaunchNewInstance).toBool()); } void WaylandTasksModelTest::test_fullscreen() { TestUtils::testFullscreen(m_model); } void WaylandTasksModelTest::test_geometry() { TestUtils::testGeometry(m_model); } void WaylandTasksModelTest::test_stackingOrder() { TestUtils::testStackingOrder(m_model); } void WaylandTasksModelTest::test_modelDataFromDesktopFile() { TestUtils::testModelDataFromDesktopFile(m_model); } void WaylandTasksModelTest::test_request() { TestUtils::testRequest(m_model); } void WaylandTasksModelTest::test_bug478831() { QProcess lsof; lsof.setProgram(u"lsof"_s); lsof.setArguments({u"-p"_s, QString::number(getpid())}); lsof.setReadChannel(QProcess::StandardOutput); lsof.start(QIODeviceBase::ReadOnly); lsof.waitForFinished(); const QByteArray fdCountBeforeBA = lsof.readAllStandardOutput(); const int fdCountBefore = fdCountBeforeBA.count('\n'); QSignalSpy rowsInsertedSpy(&m_model, &AbstractWindowTasksModel::rowsInserted); gtkWindow.setProgram(u"python3"_s); QProcessEnvironment newEnv = QProcessEnvironment::systemEnvironment(); newEnv.insert(u"GDK_BACKEND"_s, u"x11"_s); newEnv.insert(u"NO_AT_BRIDGE"_s, u"1"_s); // Otherwise following tests will fail gtkWindow.setProcessEnvironment(newEnv); gtkWindow.setArguments({QFINDTESTDATA(u"data/windows/bug478831.py"_s)}); gtkWindow.start(QIODeviceBase::ReadOnly); if (rowsInsertedSpy.empty()) { QVERIFY(rowsInsertedSpy.wait()); } const auto results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"flash"_s); QCOMPARE(results.size(), 1); QTest::qWait(10000); lsof.start(QIODeviceBase::ReadOnly); lsof.waitForFinished(); const QByteArray fdCountAfterBA = lsof.readAllStandardOutput(); const int fdCountAfter = fdCountAfterBA.count('\n'); gtkWindow.terminate(); gtkWindow.waitForFinished(); qDebug() << fdCountBefore << fdCountAfter << getpid(); QVERIFY(fdCountBefore > 10); QVERIFY(fdCountAfter > 10); QVERIFY(fdCountAfter - fdCountBefore < 10); } void WaylandTasksModelTest::test_transientWindow() { QSignalSpy rowsInsertedSpy(&m_model, &AbstractWindowTasksModel::rowsInserted); gtkWindow.setProgram(u"python3"_s); QProcessEnvironment newEnv = QProcessEnvironment::systemEnvironment(); newEnv.insert(u"GDK_BACKEND"_s, u"x11"_s); // Otherwise the following tests will fail newEnv.insert(u"NO_AT_BRIDGE"_s, u"1"_s); newEnv.insert(u"GTK_A11Y"_s, u"none"_s); // Avoid temporary files newEnv.insert(u"GSETTINGS_BACKEND"_s, u"memory"_s); newEnv.insert(u"GVFS_DISABLE_FUSE"_s, u"1"_s); newEnv.insert(u"GIO_USE_VFS"_s, u"local"_s); gtkWindow.setProcessEnvironment(newEnv); gtkWindow.setArguments({QFINDTESTDATA(u"data/windows/bug484647.py"_s)}); gtkWindow.start(QIODeviceBase::ReadOnly); QVERIFY(!rowsInsertedSpy.empty() || rowsInsertedSpy.wait()); QModelIndexList results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame1"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame2"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Dialog1"_s); QCOMPARE(results.size(), 1); QVERIFY(get(results[0].data(AbstractTasksModel::SkipTaskbar))); // Change the transient parent { QDBusMessage msg = QDBusMessage::createMethodCall(u"org.kde.bug484647"_s, u"/484647"_s, u"org.kde.bug484647"_s, u"ChangeLeader1"_s); QDBusMessage::MessageType type = QDBusConnection::sessionBus().call(msg).type(); QVERIFY(type != QDBusMessage::ErrorMessage || type != QDBusMessage::InvalidMessage); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame1"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame2"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Dialog1"_s); QCOMPARE(results.size(), 1); QVERIFY(get(results[0].data(AbstractTasksModel::SkipTaskbar))); } // Close the transient window { QSignalSpy rowsRemovedSpy(&m_model, &AbstractWindowTasksModel::rowsRemoved); QDBusMessage msg = QDBusMessage::createMethodCall(u"org.kde.bug484647"_s, u"/484647"_s, u"org.kde.bug484647"_s, u"CloseTransientWindow"_s); QDBusMessage::MessageType type = QDBusConnection::sessionBus().call(msg).type(); QVERIFY(type != QDBusMessage::ErrorMessage || type != QDBusMessage::InvalidMessage); QVERIFY(!rowsRemovedSpy.empty() || rowsRemovedSpy.wait()); // No crash: BUG 488596 results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame1"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame2"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Dialog1"_s); QVERIFY(results.empty()); } // Show the transient window again { QSignalSpy rowsInsertedSpy(&m_model, &AbstractWindowTasksModel::rowsInserted); QDBusMessage msg = QDBusMessage::createMethodCall(u"org.kde.bug484647"_s, u"/484647"_s, u"org.kde.bug484647"_s, u"ShowTransientWindow"_s); QDBusMessage::MessageType type = QDBusConnection::sessionBus().call(msg).type(); QVERIFY(type != QDBusMessage::ErrorMessage || type != QDBusMessage::InvalidMessage); QVERIFY(!rowsInsertedSpy.empty() || rowsInsertedSpy.wait()); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Dialog1"_s); QCOMPARE(results.size(), 1); QVERIFY(get(results[0].data(AbstractTasksModel::SkipTaskbar))); } // Unset the transient parent { QSignalSpy dataChangedSpy(&m_model, &AbstractWindowTasksModel::dataChanged); QDBusMessage msg = QDBusMessage::createMethodCall(u"org.kde.bug484647"_s, u"/484647"_s, u"org.kde.bug484647"_s, u"UnsetLeader"_s); QDBusMessage::MessageType type = QDBusConnection::sessionBus().call(msg).type(); QVERIFY(type != QDBusMessage::ErrorMessage || type != QDBusMessage::InvalidMessage); QVERIFY(!dataChangedSpy.empty() || dataChangedSpy.wait()); QTRY_VERIFY(std::any_of(dataChangedSpy.cbegin(), dataChangedSpy.cend(), [](const QVariantList &list) { return list.at(2).value>().contains(AbstractTasksModel::SkipTaskbar); })); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame1"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Frame2"_s); QCOMPARE(results.size(), 1); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, u"Dialog1"_s); QVERIFY(!get(results[0].data(AbstractTasksModel::SkipTaskbar))); } gtkWindow.kill(); } QTEST_MAIN(WaylandTasksModelTest) #include "waylandtasksmodeltest.moc"