/* SPDX-FileCopyrightText: 2025 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "config-kwin.h" #include "utils/socketpair.h" #include "wayland/display.h" #include "wayland_server.h" #include "workspace.h" #if KWIN_BUILD_GLOBALSHORTCUTS #include #endif #include #include #include #include #include #include #include #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_kbd_input-0"); struct Connection { static std::unique_ptr create() { auto socketPair = SocketPair::create(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (!socketPair) { return nullptr; } waylandServer()->display()->createClient(socketPair->fds[0].take()); auto connection = std::make_unique(); connection->connection = new KWayland::Client::ConnectionThread; QSignalSpy connectedSpy(connection->connection, &KWayland::Client::ConnectionThread::connected); connection->connection->setSocketFd(socketPair->fds[1].take()); connection->thread = new QThread(kwinApp()); connection->connection->moveToThread(connection->thread); connection->thread->start(); connection->connection->initConnection(); if (!connectedSpy.wait()) { return nullptr; } connection->queue = new KWayland::Client::EventQueue; connection->queue->setup(connection->connection); if (!connection->queue->isValid()) { return nullptr; } connection->registry = new KWayland::Client::Registry; connection->registry->setEventQueue(connection->queue); QObject::connect(connection->registry, &KWayland::Client::Registry::interfaceAnnounced, [&](const QByteArray &interface, quint32 name, quint32 version) { if (interface == wl_compositor_interface.name) { connection->compositor = connection->registry->createCompositor(name, version); } else if (interface == wl_shm_interface.name) { connection->shm = connection->registry->createShmPool(name, version); } else if (interface == wl_seat_interface.name) { connection->seat = connection->registry->createSeat(name, version); } else if (interface == xdg_wm_base_interface.name) { connection->xdgShell = new Test::XdgShell(); connection->xdgShell->init(*connection->registry, name, version); } }); connection->registry->create(connection->connection); if (!connection->registry->isValid()) { return nullptr; } connection->registry->setup(); QSignalSpy allAnnounced(connection->registry, &KWayland::Client::Registry::interfacesAnnounced); if (!allAnnounced.wait()) { return nullptr; } return connection; } ~Connection() { delete compositor; compositor = nullptr; delete seat; seat = nullptr; delete xdgShell; xdgShell = nullptr; delete shm; shm = nullptr; delete registry; registry = nullptr; delete queue; // Must be destroyed last if (thread) { connection->deleteLater(); thread->quit(); thread->wait(); delete thread; } } QThread *thread = nullptr; KWayland::Client::ConnectionThread *connection = nullptr; KWayland::Client::EventQueue *queue = nullptr; KWayland::Client::Registry *registry = nullptr; Test::XdgShell *xdgShell = nullptr; KWayland::Client::Compositor *compositor = nullptr; KWayland::Client::Seat *seat = nullptr; KWayland::Client::ShmPool *shm = nullptr; }; class KeyboardInputTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void implicitGrab(); void implicitGrabByClosedWindow(); void globalShortcut(); private: std::unique_ptr m_firstConnection; std::unique_ptr m_firstKeyboard; std::unique_ptr m_firstSurface; std::unique_ptr m_firstShellSurface; QPointer m_firstWindow; std::unique_ptr m_secondConnection; std::unique_ptr m_secondKeyboard; std::unique_ptr m_secondSurface; std::unique_ptr m_secondShellSurface; QPointer m_secondWindow; }; void KeyboardInputTest::initTestCase() { QVERIFY(waylandServer()->init(s_socketName)); Test::setOutputConfig({ QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024), }); kwinApp()->start(); } void KeyboardInputTest::init() { m_firstConnection = Connection::create(); QVERIFY(Test::waitForWaylandKeyboard(m_firstConnection->seat)); m_firstKeyboard = std::unique_ptr(m_firstConnection->seat->createKeyboard()); m_firstSurface = Test::createSurface(m_firstConnection->compositor); m_firstShellSurface = Test::createXdgToplevelSurface(m_firstConnection->xdgShell, m_firstSurface.get()); m_firstWindow = Test::renderAndWaitForShown(m_firstConnection->shm, m_firstSurface.get(), QSize(100, 100), Qt::cyan); QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); QVERIFY(firstEnteredSpy.wait()); m_secondConnection = Connection::create(); QVERIFY(Test::waitForWaylandKeyboard(m_secondConnection->seat)); m_secondKeyboard = std::unique_ptr(m_secondConnection->seat->createKeyboard()); m_secondSurface = Test::createSurface(m_secondConnection->compositor); m_secondShellSurface = Test::createXdgToplevelSurface(m_secondConnection->xdgShell, m_secondSurface.get()); m_secondWindow = Test::renderAndWaitForShown(m_secondConnection->shm, m_secondSurface.get(), QSize(100, 100), Qt::cyan); QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); QVERIFY(secondEnteredSpy.wait()); } void KeyboardInputTest::cleanup() { m_firstShellSurface.reset(); m_firstSurface.reset(); m_firstKeyboard.reset(); m_firstConnection.reset(); m_secondShellSurface.reset(); m_secondSurface.reset(); m_secondKeyboard.reset(); m_secondConnection.reset(); } void KeyboardInputTest::implicitGrab() { QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy firstKeyChangedSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy secondKeyChangedSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); quint32 timestamp = 0; Test::keyboardKeyPressed(KEY_Q, timestamp++); QVERIFY(secondKeyChangedSpy.wait()); QCOMPARE(secondKeyChangedSpy.last().at(0).value(), KEY_Q); QCOMPARE(secondKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); workspace()->activateWindow(m_firstWindow); QVERIFY(firstEnteredSpy.wait()); // TODO: perhaps we should not receive the enter event until the q key is released QCOMPARE(m_firstKeyboard->enteredKeys(), (QList{KEY_Q})); Test::keyboardKeyReleased(KEY_Q, timestamp++); QVERIFY(firstKeyChangedSpy.wait()); QCOMPARE(firstKeyChangedSpy.last().at(0).value(), KEY_Q); QCOMPARE(firstKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Released); } void KeyboardInputTest::implicitGrabByClosedWindow() { // This test verifies that an implicit grab is preserved even after the window is closed. Note: // currently it is not the case, but it should be. QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy firstKeyChangedSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy secondKeyChangedSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); quint32 timestamp = 0; Test::keyboardKeyPressed(KEY_Q, timestamp++); QVERIFY(secondKeyChangedSpy.wait()); QCOMPARE(secondKeyChangedSpy.last().at(0).value(), KEY_Q); QCOMPARE(secondKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); m_secondShellSurface.reset(); m_secondSurface.reset(); QVERIFY(firstEnteredSpy.wait()); // TODO: perhaps we should not receive the enter event until the q key is released QCOMPARE(m_firstKeyboard->enteredKeys(), (QList{KEY_Q})); Test::keyboardKeyReleased(KEY_Q, timestamp++); QVERIFY(firstKeyChangedSpy.wait()); QCOMPARE(firstKeyChangedSpy.last().at(0).value(), KEY_Q); QCOMPARE(firstKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Released); } void KeyboardInputTest::globalShortcut() { // This test verifies that keys are not leaked to the clients when pressing a global shortcut. #if KWIN_BUILD_GLOBALSHORTCUTS QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy firstKeyChangedSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy secondKeyChangedSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); auto action = std::make_unique(); action->setObjectName(QStringLiteral("test")); action->setProperty("componentName", QStringLiteral("test")); KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::META | Qt::Key_Space}, KGlobalAccel::NoAutoloading); QSignalSpy actionTriggeredSpy(action.get(), &QAction::triggered); // the client should not see the space key being pressed or released quint32 timestamp = 0; Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); QVERIFY(secondKeyChangedSpy.wait()); Test::keyboardKeyPressed(KEY_SPACE, timestamp++); QVERIFY(!secondKeyChangedSpy.wait(10)); QCOMPARE(actionTriggeredSpy.count(), 1); Test::keyboardKeyReleased(KEY_SPACE, timestamp++); QVERIFY(!secondKeyChangedSpy.wait(10)); Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); QVERIFY(secondKeyChangedSpy.wait()); // the space key should not be leaked even if the focused surface changes between pressing and releasing the space key Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); QVERIFY(secondKeyChangedSpy.wait()); Test::keyboardKeyPressed(KEY_SPACE, timestamp++); QVERIFY(!secondKeyChangedSpy.wait(10)); QCOMPARE(actionTriggeredSpy.count(), 2); m_secondShellSurface.reset(); m_secondSurface.reset(); QVERIFY(firstEnteredSpy.wait()); QCOMPARE(m_firstKeyboard->enteredKeys(), (QList{KEY_LEFTMETA})); Test::keyboardKeyReleased(KEY_SPACE, timestamp++); QVERIFY(!firstKeyChangedSpy.wait(10)); Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); QVERIFY(firstKeyChangedSpy.wait()); #endif } } // namespace KWin WAYLANDTEST_MAIN(KWin::KeyboardInputTest) #include "keyboard_input_test.moc"