/* SPDX-FileCopyrightText: 2025 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "core/outputconfiguration.h" #include "pointer_input.h" #include "virtualdesktops.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgsession-0"); class TestXdgSession : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void addToplevelSession(); void addMappedToplevelSession(); void restoreToplevelSession(); void restoreMappedToplevelSession(); void replaceSession(); void restorePosition(); void restoreOffscreenPosition(); void restoreSize(); void restoreKeepAbove(); void restoreKeepBelow(); void restoreSkipSwitcher(); void restoreSkipPager(); void restoreSkipTaskbar(); void restoreMaximized_data(); void restoreMaximized(); void restoreFullScreen(); void restoreMinimized(); void restoreNoBorder(); void restoreShortcut(); void restoreDesktops(); void restoreUnknownDesktops(); private: struct RestoreFuncs { std::function setup; std::function between; std::function restore; }; void restoreTemplate(RestoreFuncs funcs); }; void TestXdgSession::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qputenv("KWIN_WAYLAND_SUPPORT_XX_SESSION_MANAGER", "1"); QVERIFY(waylandServer()->init(s_socketName)); kwinApp()->start(); Test::setOutputConfig({ Rect(0, 0, 1280, 1024), Rect(1280, 0, 1280, 1024), }); const auto outputs = workspace()->outputs(); QCOMPARE(outputs.count(), 2); QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); } void TestXdgSession::init() { Test::setOutputConfig({ Rect(0, 0, 1280, 1024), Rect(1280, 0, 1280, 1024), }); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgSessionV1)); workspace()->setActiveOutput(QPoint(640, 512)); input()->pointer()->warp(QPoint(640, 512)); VirtualDesktopManager::self()->setCount(2); } void TestXdgSession::cleanup() { Test::destroyWaylandConnection(); } void TestXdgSession::addToplevelSession() { std::unique_ptr session(Test::createXdgSessionV1(Test::XdgSessionManagerV1::reason_launch)); QSignalSpy sessionCreatedSpy(session.get(), &Test::XdgSessionV1::created); QVERIFY(sessionCreatedSpy.wait()); // If the toplevel is added to the session for the first time, we should receive a created event, but no restored event. { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->add(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(sessionCreatedSpy.count(), 1); QCOMPARE(toplevelSessionRestoredSpy.count(), 0); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } // If the toplevel is added to the session again, no created and restored events should be sent. { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->add(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(sessionCreatedSpy.count(), 1); QCOMPARE(toplevelSessionRestoredSpy.count(), 0); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } } void TestXdgSession::addMappedToplevelSession() { // This test verifies that no protocol error will be posted if a mapped toplevel is added to the session. std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr session(Test::createXdgSessionV1(Test::XdgSessionManagerV1::reason_launch)); std::unique_ptr toplevelSession(session->add(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy sessionCreatedSpy(session.get(), &Test::XdgSessionV1::created); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QVERIFY(sessionCreatedSpy.wait()); QCOMPARE(sessionCreatedSpy.count(), 1); QCOMPARE(toplevelSessionRestoredSpy.count(), 0); } void TestXdgSession::restoreToplevelSession() { std::unique_ptr session(Test::createXdgSessionV1(Test::XdgSessionManagerV1::reason_launch)); QSignalSpy sessionCreatedSpy(session.get(), &Test::XdgSessionV1::created); QVERIFY(sessionCreatedSpy.wait()); // Add the toplevel to the session. { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->add(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelSessionRestoredSpy.count(), 0); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } // Restore the toplevel session. { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->restore(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelSessionRestoredSpy.count(), 1); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } // Restore a toplevel session that does not exist in the session storage. { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->restore(shellSurface.get(), QStringLiteral("bar"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelSessionRestoredSpy.count(), 0); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } } void TestXdgSession::restoreMappedToplevelSession() { // This test verifies that a protocol error will be posted if a client attempts to restore a mapped toplevel. std::unique_ptr session(Test::createXdgSessionV1(Test::XdgSessionManagerV1::reason_launch)); QSignalSpy sessionCreatedSpy(session.get(), &Test::XdgSessionV1::created); QVERIFY(sessionCreatedSpy.wait()); std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); std::unique_ptr toplevelSession(session->restore(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy connectionErrorSpy(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); QVERIFY(connectionErrorSpy.wait()); } void TestXdgSession::replaceSession() { // A naughty client attempts to create a session twice -> a protocol error and lots of spanking. { auto connection = Test::Connection::setup(Test::AdditionalWaylandInterface::XdgSessionV1); std::unique_ptr firstSession = Test::createXdgSessionV1(connection->sessionManager.get(), Test::XdgSessionManagerV1::reason_launch); QSignalSpy sessionCreatedSpy(firstSession.get(), &Test::XdgSessionV1::created); QVERIFY(sessionCreatedSpy.wait()); const QString sessionId = sessionCreatedSpy.last().at(0).toString(); std::unique_ptr secondSession = Test::createXdgSessionV1(connection->sessionManager.get(), Test::XdgSessionManagerV1::reason_launch, sessionId); QSignalSpy connectionErrorSpy(connection->connection, &KWayland::Client::ConnectionThread::errorOccurred); QVERIFY(connectionErrorSpy.wait()); } // A client creates a session that's in use by another client -> send the replaced event to the other client. { auto firstConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::XdgSessionV1); auto secondConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::XdgSessionV1); std::unique_ptr firstSession = Test::createXdgSessionV1(firstConnection->sessionManager.get(), Test::XdgSessionManagerV1::reason_launch); QSignalSpy firstSessionCreatedSpy(firstSession.get(), &Test::XdgSessionV1::created); QSignalSpy firstSessionReplacedSpy(firstSession.get(), &Test::XdgSessionV1::replaced); QVERIFY(firstSessionCreatedSpy.wait()); QCOMPARE(firstSessionReplacedSpy.count(), 0); QCOMPARE(firstSessionReplacedSpy.count(), 0); const QString sessionId = firstSessionCreatedSpy.last().at(0).toString(); std::unique_ptr secondSession = Test::createXdgSessionV1(secondConnection->sessionManager.get(), Test::XdgSessionManagerV1::reason_launch, sessionId); QSignalSpy secondSessionCreatedSpy(secondSession.get(), &Test::XdgSessionV1::created); QSignalSpy secondSessionReplacedSpy(secondSession.get(), &Test::XdgSessionV1::replaced); QVERIFY(secondSessionCreatedSpy.wait()); QCOMPARE(secondSessionReplacedSpy.count(), 0); // The first and the second connection can process events in different order, the sync is needed // to guarantee that the first connection has processed all events before checking the signal spies. // Without the sync, the test will be flaky. QVERIFY(firstConnection->sync()); QCOMPARE(firstSessionCreatedSpy.count(), 1); QCOMPARE(firstSessionReplacedSpy.count(), 1); } } void TestXdgSession::restoreTemplate(RestoreFuncs funcs) { std::unique_ptr session(Test::createXdgSessionV1(Test::XdgSessionManagerV1::reason_launch)); QSignalSpy sessionCreatedSpy(session.get(), &Test::XdgSessionV1::created); QVERIFY(sessionCreatedSpy.wait()); { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->add(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelSessionRestoredSpy.count(), 0); const QSize surfaceSize = toplevelConfigureRequestedSpy.last().at(0).toSize(); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), surfaceSize.isEmpty() ? QSize(100, 50) : surfaceSize, Qt::blue); funcs.setup(window); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } if (funcs.between) { funcs.between(); } { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); std::unique_ptr toplevelSession(session->restore(shellSurface.get(), QStringLiteral("foo"))); QSignalSpy toplevelSessionRestoredSpy(toplevelSession.get(), &Test::XdgToplevelSessionV1::restored); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelSessionRestoredSpy.count(), 1); const QSize surfaceSize = toplevelConfigureRequestedSpy.last().at(0).toSize(); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto window = Test::renderAndWaitForShown(surface.get(), surfaceSize.isEmpty() ? QSize(100, 50) : surfaceSize, Qt::blue); funcs.restore(window); shellSurface.reset(); QVERIFY(Test::waitForWindowClosed(window)); } } void TestXdgSession::restorePosition() { restoreTemplate({ .setup = [](Window *window) { window->move(QPointF(42, 84)); }, .restore = [](Window *window) { QCOMPARE(window->pos(), QPointF(42, 84)); }, }); } void TestXdgSession::restoreOffscreenPosition() { const auto outputs = workspace()->outputs(); restoreTemplate({ .setup = [outputs](Window *window) { QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); window->move(QPointF(1280 + 42, 42)); QCOMPARE(window->output(), outputs[1]); }, .between = [outputs]() { OutputConfiguration configuration; { auto changeSet = configuration.changeSet(outputs[1]->backendOutput()); changeSet->enabled = false; } workspace()->applyOutputConfiguration(configuration); }, .restore = [outputs](Window *window) { QCOMPARE(window->pos(), QPointF(0, 0)); QCOMPARE(window->output(), outputs[0]); }, }); } void TestXdgSession::restoreSize() { restoreTemplate({ .setup = [](Window *window) { window->resize(QSizeF(300, 250)); }, .restore = [](Window *window) { QCOMPARE(window->size(), QSizeF(300, 250)); }, }); } void TestXdgSession::restoreKeepAbove() { restoreTemplate({ .setup = [](Window *window) { window->setKeepAbove(true); }, .restore = [](Window *window) { QVERIFY(window->keepAbove()); }, }); } void TestXdgSession::restoreKeepBelow() { restoreTemplate({ .setup = [](Window *window) { window->setKeepBelow(true); }, .restore = [](Window *window) { QVERIFY(window->keepBelow()); }, }); } void TestXdgSession::restoreSkipSwitcher() { restoreTemplate({ .setup = [](Window *window) { window->setSkipSwitcher(true); }, .restore = [](Window *window) { QVERIFY(window->skipSwitcher()); }, }); } void TestXdgSession::restoreSkipPager() { restoreTemplate({ .setup = [](Window *window) { window->setSkipPager(true); }, .restore = [](Window *window) { QVERIFY(window->skipPager()); }, }); } void TestXdgSession::restoreSkipTaskbar() { restoreTemplate({ .setup = [](Window *window) { window->setSkipTaskbar(true); }, .restore = [](Window *window) { QVERIFY(window->skipTaskbar()); }, }); } void TestXdgSession::restoreMaximized_data() { QTest::addColumn("maximizeMode"); QTest::addRow("none") << MaximizeMode::MaximizeRestore; QTest::addRow("horizontal") << MaximizeMode::MaximizeHorizontal; QTest::addRow("vertical") << MaximizeMode::MaximizeVertical; QTest::addRow("full") << MaximizeMode::MaximizeFull; } void TestXdgSession::restoreMaximized() { QFETCH(MaximizeMode, maximizeMode); restoreTemplate({ .setup = [maximizeMode](Window *window) { window->maximize(maximizeMode); }, .restore = [maximizeMode](Window *window) { QCOMPARE(window->maximizeMode(), maximizeMode); QCOMPARE(window->requestedMaximizeMode(), maximizeMode); }, }); } void TestXdgSession::restoreFullScreen() { restoreTemplate({ .setup = [](Window *window) { window->setFullScreen(true); }, .restore = [](Window *window) { QVERIFY(window->isFullScreen()); QVERIFY(window->isRequestedFullScreen()); }, }); } void TestXdgSession::restoreMinimized() { restoreTemplate({ .setup = [](Window *window) { window->setMinimized(true); }, .restore = [](Window *window) { QVERIFY(window->isMinimized()); }, }); } void TestXdgSession::restoreNoBorder() { restoreTemplate({ .setup = [](Window *window) { window->setNoBorder(true); }, .restore = [](Window *window) { QVERIFY(window->noBorder()); }, }); } void TestXdgSession::restoreShortcut() { restoreTemplate({ .setup = [](Window *window) { window->setShortcut(QStringLiteral("Ctrl+Meta+T")); }, .restore = [](Window *window) { QCOMPARE(window->shortcut(), QKeySequence(Qt::ControlModifier | Qt::MetaModifier | Qt::Key_T)); }, }); } void TestXdgSession::restoreDesktops() { const auto desktops = VirtualDesktopManager::self()->desktops(); restoreTemplate({ .setup = [desktops](Window *window) { QVERIFY(window->isOnDesktop(desktops[0])); QVERIFY(!window->isOnDesktop(desktops[1])); window->setDesktops({desktops[1]}); }, .restore = [desktops](Window *window) { QVERIFY(!window->isOnDesktop(desktops[0])); QVERIFY(window->isOnDesktop(desktops[1])); }, }); } void TestXdgSession::restoreUnknownDesktops() { auto vds = VirtualDesktopManager::self(); vds->setCount(3); // The window is on desktops 2 and 3. If desktop 3 is removed, the window should be restored only on desktop 2. const auto desktops = VirtualDesktopManager::self()->desktops(); restoreTemplate({ .setup = [desktops](Window *window) { window->setDesktops({desktops[1], desktops[2]}); }, .between = [vds, desktops]() { vds->removeVirtualDesktop(desktops[2]); }, .restore = [desktops](Window *window) { QCOMPARE(window->desktops(), (QList{desktops[1]})); }, }); // The window is on desktop 2. If desktop 2 is removed, the window should be restored only on desktop 1, // it should not be displayed on all desktops. restoreTemplate({ .setup = [desktops](Window *window) { window->setDesktops({desktops[1]}); }, .between = [vds, desktops]() { vds->removeVirtualDesktop(desktops[1]); }, .restore = [desktops](Window *window) { QCOMPARE(window->desktops(), (QList{desktops[0]})); }, }); } WAYLANDTEST_MAIN(TestXdgSession) #include "xdgsession_test.moc"