/* SPDX-FileCopyrightText: 2025 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "core/output.h" #include "pointer_input.h" #include "utils/pipe.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_selection-0"); template static QFuture readMimeTypeData(const Offer &offer, const QMimeType &mimeType) { auto pipe = Pipe::create(O_CLOEXEC); if (!pipe) { return QFuture(); } offer->receive(mimeType.name(), pipe->writeEndpoint.get()); return QtConcurrent::run([fd = std::move(pipe->readEndpoint)] { QFile file; if (!file.open(fd.get(), QFile::ReadOnly | QFile::Text)) { return QByteArray(); } return file.readAll(); }); } template static bool waitFuture(const QFuture &future) { QFutureWatcher watcher; QSignalSpy finishedSpy(&watcher, &QFutureWatcher::finished); watcher.setFuture(future); return finishedSpy.wait(); } static QMimeType plainText() { static QMimeType mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/plain")); return mimeType; } static QMimeType htmlText() { static QMimeType mimeType = QMimeDatabase().mimeTypeForName(QStringLiteral("text/html")); return mimeType; } class SelectionTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void selection(); void internalSelection(); void destroySelection(); void invalidSerialForSelection(); void unsetSupersededSelection(); void receiveFromWithdrawnSelectionOffer(); void singleSelectionPerClient(); void primarySelection(); void internalPrimarySelection(); void destroyPrimarySelection(); void invalidSerialForPrimarySelection(); void unsetSupersededPrimarySelection(); void receiveFromWithdrawnPrimarySelectionOffer(); void singlePrimarySelectionPerClient(); }; void SelectionTest::initTestCase() { qRegisterMetaType(); 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 SelectionTest::init() { workspace()->setActiveOutput(QPoint(640, 512)); input()->pointer()->warp(QPoint(640, 512)); } void SelectionTest::selection() { // This test verifies that the clipboard works as expected between two clients. auto sourceConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); auto targetConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); // Setup the source side. std::unique_ptr sourceDataDevice(sourceConnection->dataDeviceManager->getDataDevice(sourceConnection->seat)); std::unique_ptr dataSource(sourceConnection->dataDeviceManager->createDataSource()); dataSource->offer(plainText()); connect(dataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto sourceSurface = Test::createSurface(sourceConnection->compositor); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceConnection->xdgShell, sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceConnection->shm, sourceSurface.get(), QSize(100, 100), Qt::red); // Setup the target side. std::unique_ptr targetDataDevice(targetConnection->dataDeviceManager->getDataDevice(targetConnection->seat)); auto targetSurface = Test::createSurface(targetConnection->compositor); auto targetShellSurface = Test::createXdgToplevelSurface(targetConnection->xdgShell, targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetConnection->shm, targetSurface.get(), QSize(100, 100), Qt::blue); // Activate the source window. std::unique_ptr sourceKeyboard(sourceConnection->seat->createKeyboard()); QSignalSpy sourceKeyboardEnteredSpy(sourceKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(sourceWindow); QVERIFY(sourceKeyboardEnteredSpy.wait()); const quint32 sourceEnteredSerial = sourceKeyboardEnteredSpy.last().at(0).value(); // Set the selection. sourceDataDevice->setSelection(sourceEnteredSerial, dataSource.get()); // The target device shouldn't receive an offer while it's not focused. QSignalSpy targetDataDeviceSelectionOfferedSpy(targetDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); QVERIFY(!targetDataDeviceSelectionOfferedSpy.wait(100)); workspace()->activateWindow(targetWindow); QVERIFY(targetDataDeviceSelectionOfferedSpy.wait()); KWayland::Client::DataOffer *offer = targetDataDevice->offeredSelection(); QCOMPARE(offer->offeredMimeTypes(), QList{plainText()}); // Ask for data. const QFuture plainData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(offer, htmlText()); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Clear selection. QSignalSpy targetDataDeviceSelectionClearedSpy(targetDataDevice.get(), &KWayland::Client::DataDevice::selectionCleared); sourceDataDevice->setSelection(sourceEnteredSerial, nullptr); QVERIFY(targetDataDeviceSelectionClearedSpy.wait()); } void SelectionTest::internalSelection() { // This test verifies that the source client also receives a wl_data_offer for its own selection. auto connection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); std::unique_ptr dataDevice(connection->dataDeviceManager->getDataDevice(connection->seat)); std::unique_ptr dataSource(connection->dataDeviceManager->createDataSource()); dataSource->offer(plainText()); connect(dataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto surface = Test::createSurface(connection->compositor); auto shellSurface = Test::createXdgToplevelSurface(connection->xdgShell, surface.get()); auto window = Test::renderAndWaitForShown(connection->shm, surface.get(), QSize(100, 100), Qt::red); std::unique_ptr keyboard(connection->seat->createKeyboard()); QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(window); QVERIFY(keyboardEnteredSpy.wait()); const quint32 enteredSerial = keyboardEnteredSpy.last().at(0).value(); // Set the selection. dataDevice->setSelection(enteredSerial, dataSource.get()); QSignalSpy dataDeviceSelectionOfferedSpy(dataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); QVERIFY(dataDeviceSelectionOfferedSpy.wait()); KWayland::Client::DataOffer *offer = dataDevice->offeredSelection(); QCOMPARE(offer->offeredMimeTypes(), QList{plainText()}); // Ask for data. const QFuture plainData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(offer, htmlText()); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Clear selection. QSignalSpy selectionClearedSpy(dataDevice.get(), &KWayland::Client::DataDevice::selectionCleared); dataDevice->setSelection(enteredSerial, nullptr); QVERIFY(selectionClearedSpy.wait()); } void SelectionTest::destroySelection() { // This test verifies that the wl_data_offer will be withdrawn if the associated data source is destroyed. auto sourceConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); auto targetConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); // Setup the source side. std::unique_ptr sourceDataDevice(sourceConnection->dataDeviceManager->getDataDevice(sourceConnection->seat)); std::unique_ptr dataSource(sourceConnection->dataDeviceManager->createDataSource()); dataSource->offer(plainText()); connect(dataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto sourceSurface = Test::createSurface(sourceConnection->compositor); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceConnection->xdgShell, sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceConnection->shm, sourceSurface.get(), QSize(100, 100), Qt::red); // Setup the target side. std::unique_ptr targetDataDevice(targetConnection->dataDeviceManager->getDataDevice(targetConnection->seat)); auto targetSurface = Test::createSurface(targetConnection->compositor); auto targetShellSurface = Test::createXdgToplevelSurface(targetConnection->xdgShell, targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetConnection->shm, targetSurface.get(), QSize(100, 100), Qt::blue); // Activate the source window. std::unique_ptr sourceKeyboard(sourceConnection->seat->createKeyboard()); QSignalSpy sourceKeyboardEnteredSpy(sourceKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(sourceWindow); QVERIFY(sourceKeyboardEnteredSpy.wait()); const quint32 sourceEnteredSerial = sourceKeyboardEnteredSpy.last().at(0).value(); // Set the selection. sourceDataDevice->setSelection(sourceEnteredSerial, dataSource.get()); // Activate the target window and wait for the data offer. QSignalSpy targetDataDeviceSelectionOfferedSpy(targetDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); workspace()->activateWindow(targetWindow); QVERIFY(targetDataDeviceSelectionOfferedSpy.wait()); KWayland::Client::DataOffer *offer = targetDataDevice->offeredSelection(); QCOMPARE(offer->offeredMimeTypes(), QList{plainText()}); // Destroy the data source. QSignalSpy targetDataDeviceSelectionClearedSpy(targetDataDevice.get(), &KWayland::Client::DataDevice::selectionCleared); dataSource.reset(); QVERIFY(targetDataDeviceSelectionClearedSpy.wait()); } void SelectionTest::invalidSerialForSelection() { // This test verifies that a set_selection request will be ignored if the specified serial is invalid. auto connection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); std::unique_ptr dataDevice(connection->dataDeviceManager->getDataDevice(connection->seat)); std::unique_ptr dataSource(connection->dataDeviceManager->createDataSource()); dataSource->offer(QStringLiteral("text/plain")); connect(dataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto surface = Test::createSurface(connection->compositor); auto shellSurface = Test::createXdgToplevelSurface(connection->xdgShell, surface.get()); Test::renderAndWaitForShown(connection->shm, surface.get(), QSize(100, 100), Qt::red); std::unique_ptr keyboard(connection->seat->createKeyboard()); QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); QVERIFY(keyboardEnteredSpy.wait()); const quint32 enteredSerial = keyboardEnteredSpy.last().at(0).value(); // Set the selection. QSignalSpy dataSourceCancelledSpy(dataSource.get(), &KWayland::Client::DataSource::cancelled); dataDevice->setSelection(enteredSerial - 100, dataSource.get()); QVERIFY(dataSourceCancelledSpy.wait(100)); } void SelectionTest::unsetSupersededSelection() { // This test verifies that the current selection won't be unset if the previous selection is unset with set_selection(null). // Setup the first client. auto firstConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); QVERIFY(Test::waitForWaylandKeyboard(firstConnection->seat)); std::unique_ptr firstKeyboard(firstConnection->seat->createKeyboard()); std::unique_ptr firstDataDevice(firstConnection->dataDeviceManager->getDataDevice(firstConnection->seat)); std::unique_ptr firstDataSource(firstConnection->dataDeviceManager->createDataSource()); firstDataSource->offer(plainText()); connect(firstDataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto firstSurface = Test::createSurface(firstConnection->compositor); auto firstShellSurface = Test::createXdgToplevelSurface(firstConnection->xdgShell, firstSurface.get()); auto firstWindow = Test::renderAndWaitForShown(firstConnection->shm, firstSurface.get(), QSize(100, 100), Qt::red); // Setup the second client. auto secondConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); QVERIFY(Test::waitForWaylandKeyboard(secondConnection->seat)); std::unique_ptr secondKeyboard(secondConnection->seat->createKeyboard()); std::unique_ptr secondDataDevice(secondConnection->dataDeviceManager->getDataDevice(secondConnection->seat)); std::unique_ptr secondDataSource(secondConnection->dataDeviceManager->createDataSource()); secondDataSource->offer(htmlText()); connect(secondDataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("bar"); write(fd, data.data(), data.size()); close(fd); }); auto secondSurface = Test::createSurface(secondConnection->compositor); auto secondShellSurface = Test::createXdgToplevelSurface(secondConnection->xdgShell, secondSurface.get()); auto secondWindow = Test::renderAndWaitForShown(secondConnection->shm, secondSurface.get(), QSize(100, 100), Qt::blue); // Activate the first window and set the selection. QSignalSpy firstKeyboardEnteredSpy(firstKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy firstDataDeviceSelectionOfferedSpy(firstDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); workspace()->activateWindow(firstWindow); QVERIFY(firstKeyboardEnteredSpy.wait()); const quint32 firstEnteredSerial = firstKeyboardEnteredSpy.last().at(0).value(); firstDataDevice->setSelection(firstEnteredSerial, firstDataSource.get()); QVERIFY(firstDataDeviceSelectionOfferedSpy.wait()); // Activate the second window and set the selection. QSignalSpy secondKeyboardEnteredSpy(secondKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy secondDataDeviceSelectionOfferedSpy(secondDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); workspace()->activateWindow(secondWindow); QVERIFY(secondKeyboardEnteredSpy.wait()); const quint32 secondEnteredSerial = secondKeyboardEnteredSpy.last().at(0).value(); secondDataDevice->setSelection(secondEnteredSerial, secondDataSource.get()); QVERIFY(secondDataDeviceSelectionOfferedSpy.wait()); // Attempt to unset the first selection. QSignalSpy secondDataDeviceSelectionClearedSpy(secondDataDevice.get(), &KWayland::Client::DataDevice::selectionCleared); firstDataDevice->setSelection(firstEnteredSerial, nullptr); QVERIFY(!secondDataDeviceSelectionClearedSpy.wait(100)); // Attempt to unset the second selection. secondDataDevice->setSelection(secondEnteredSerial, nullptr); QVERIFY(secondDataDeviceSelectionClearedSpy.wait()); } void SelectionTest::receiveFromWithdrawnSelectionOffer() { // This test verifies that a client cannot receive data from a withdrawn selection offer. auto sourceConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); auto targetConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); // Setup the source side. std::unique_ptr sourceDataDevice(sourceConnection->dataDeviceManager->getDataDevice(sourceConnection->seat)); std::unique_ptr dataSource(sourceConnection->dataDeviceManager->createDataSource()); dataSource->offer(plainText()); connect(dataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto sourceSurface = Test::createSurface(sourceConnection->compositor); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceConnection->xdgShell, sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceConnection->shm, sourceSurface.get(), QSize(100, 100), Qt::red); // Setup the target side. std::unique_ptr targetDataDevice(targetConnection->dataDeviceManager->getDataDevice(targetConnection->seat)); auto targetSurface = Test::createSurface(targetConnection->compositor); auto targetShellSurface = Test::createXdgToplevelSurface(targetConnection->xdgShell, targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetConnection->shm, targetSurface.get(), QSize(100, 100), Qt::blue); // Focus the source window, and set the selection. std::unique_ptr sourceKeyboard(sourceConnection->seat->createKeyboard()); QSignalSpy sourceKeyboardEnteredSpy(sourceKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(sourceWindow); QVERIFY(sourceKeyboardEnteredSpy.wait()); const quint32 sourceEnteredSerial = sourceKeyboardEnteredSpy.last().at(0).value(); sourceDataDevice->setSelection(sourceEnteredSerial, dataSource.get()); // The target client should be offered the selection when its window gets focused. QSignalSpy targetDataDeviceSelectionOfferedSpy(targetDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); workspace()->activateWindow(targetWindow); QVERIFY(targetDataDeviceSelectionOfferedSpy.wait()); std::unique_ptr offer = targetDataDevice->takeOfferedSelection(); QCOMPARE(offer->offeredMimeTypes(), QList{plainText()}); // Ask for data. const QFuture plainData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // Remove focus from the target window, it should not be able to receive data anymore. workspace()->activateWindow(nullptr); const QFuture emptyData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(emptyData)); QCOMPARE(emptyData.result(), QByteArray()); } void SelectionTest::singleSelectionPerClient() { // This test verifies that only one selection event will be sent when switching between two // surfaces that belong to the same client. // Setup the first client. auto firstConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); QVERIFY(Test::waitForWaylandKeyboard(firstConnection->seat)); std::unique_ptr firstKeyboard(firstConnection->seat->createKeyboard()); std::unique_ptr firstDataDevice(firstConnection->dataDeviceManager->getDataDevice(firstConnection->seat)); std::unique_ptr firstDataSource(firstConnection->dataDeviceManager->createDataSource()); firstDataSource->offer(plainText()); connect(firstDataSource.get(), &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto firstSurface = Test::createSurface(firstConnection->compositor); auto firstShellSurface = Test::createXdgToplevelSurface(firstConnection->xdgShell, firstSurface.get()); auto firstWindow = Test::renderAndWaitForShown(firstConnection->shm, firstSurface.get(), QSize(100, 100), Qt::red); // Setup the second client. auto secondConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager); QVERIFY(Test::waitForWaylandKeyboard(secondConnection->seat)); std::unique_ptr secondKeyboard(secondConnection->seat->createKeyboard()); std::unique_ptr secondDataDevice(secondConnection->dataDeviceManager->getDataDevice(secondConnection->seat)); auto secondSurface = Test::createSurface(secondConnection->compositor); auto secondShellSurface = Test::createXdgToplevelSurface(secondConnection->xdgShell, secondSurface.get()); auto secondWindow = Test::renderAndWaitForShown(secondConnection->shm, secondSurface.get(), QSize(100, 100), Qt::blue); auto thirdSurface = Test::createSurface(secondConnection->compositor); auto thirdShellSurface = Test::createXdgToplevelSurface(secondConnection->xdgShell, thirdSurface.get()); auto thirdWindow = Test::renderAndWaitForShown(secondConnection->shm, thirdSurface.get(), QSize(100, 100), Qt::green); // Activate the first window and set the selection. QSignalSpy firstKeyboardEnteredSpy(firstKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(firstWindow); QVERIFY(firstKeyboardEnteredSpy.wait()); const quint32 firstEnteredSerial = firstKeyboardEnteredSpy.last().at(0).value(); firstDataDevice->setSelection(firstEnteredSerial, firstDataSource.get()); QSignalSpy firstDataDeviceSelectionOfferedSpy(firstDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); QSignalSpy firstDataDeviceSelectionClearedSpy(firstDataDevice.get(), &KWayland::Client::DataDevice::selectionCleared); QSignalSpy secondDataDeviceSelectionOfferedSpy(secondDataDevice.get(), &KWayland::Client::DataDevice::selectionOffered); QSignalSpy secondDataDeviceSelectionClearedSpy(secondDataDevice.get(), &KWayland::Client::DataDevice::selectionCleared); QVERIFY(firstDataDeviceSelectionOfferedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 2nd window. workspace()->activateWindow(secondWindow); QVERIFY(secondDataDeviceSelectionOfferedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 3rd window (the 2nd and the 3rd window belong to the same client). workspace()->activateWindow(thirdWindow); QVERIFY(!secondDataDeviceSelectionOfferedSpy.wait(100)); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 2nd window (the 2nd and the 3rd window belong to the same client). workspace()->activateWindow(secondWindow); QVERIFY(!secondDataDeviceSelectionOfferedSpy.wait(100)); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 1st window (different connection from the 2nd and 3rd window). workspace()->activateWindow(firstWindow); QVERIFY(firstDataDeviceSelectionOfferedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 2); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Clear the selection. firstDataDevice->setSelection(firstEnteredSerial, nullptr); QVERIFY(firstDataDeviceSelectionClearedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 2); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 2nd window (different connection from the 1st window). workspace()->activateWindow(secondWindow); QVERIFY(!secondDataDeviceSelectionOfferedSpy.wait(100)); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 2); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 1); // 1 because a null selection is sent } void SelectionTest::primarySelection() { // This test verifies that the primary selection works as expected between two clients. auto sourceConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); auto targetConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); // Setup the source side. std::unique_ptr sourceDataDevice(sourceConnection->primarySelectionManager->getDevice(sourceConnection->seat)); std::unique_ptr dataSource(sourceConnection->primarySelectionManager->createSource()); dataSource->offer(plainText().name()); connect(dataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto sourceSurface = Test::createSurface(sourceConnection->compositor); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceConnection->xdgShell, sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceConnection->shm, sourceSurface.get(), QSize(100, 100), Qt::red); // Setup the target side. std::unique_ptr targetDataDevice(targetConnection->primarySelectionManager->getDevice(targetConnection->seat)); auto targetSurface = Test::createSurface(targetConnection->compositor); auto targetShellSurface = Test::createXdgToplevelSurface(targetConnection->xdgShell, targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetConnection->shm, targetSurface.get(), QSize(100, 100), Qt::blue); // Activate the source window. std::unique_ptr sourceKeyboard(sourceConnection->seat->createKeyboard()); QSignalSpy sourceKeyboardEnteredSpy(sourceKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(sourceWindow); QVERIFY(sourceKeyboardEnteredSpy.wait()); const quint32 sourceEnteredSerial = sourceKeyboardEnteredSpy.last().at(0).value(); // Set the selection. sourceDataDevice->set_selection(dataSource->object(), sourceEnteredSerial); // The target device shouldn't receive an offer while it's not focused. QSignalSpy targetDataDeviceSelectionOfferedSpy(targetDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); QVERIFY(!targetDataDeviceSelectionOfferedSpy.wait(100)); workspace()->activateWindow(targetWindow); QVERIFY(targetDataDeviceSelectionOfferedSpy.wait()); Test::WpPrimarySelectionOfferV1 *offer = targetDataDevice->offer(); QCOMPARE(offer->mimeTypes(), QList{plainText()}); // Ask for data. const QFuture plainData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(offer, htmlText()); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Clear selection. QSignalSpy targetDataDeviceSelectionClearedSpy(targetDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionCleared); sourceDataDevice->set_selection(nullptr, sourceEnteredSerial); QVERIFY(targetDataDeviceSelectionClearedSpy.wait()); } void SelectionTest::internalPrimarySelection() { // This test verifies that the source client also receives a wp_primary_selection_offer_v1 for its own selection. auto connection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); std::unique_ptr dataDevice(connection->primarySelectionManager->getDevice(connection->seat)); std::unique_ptr dataSource(connection->primarySelectionManager->createSource()); dataSource->offer(plainText().name()); connect(dataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto surface = Test::createSurface(connection->compositor); auto shellSurface = Test::createXdgToplevelSurface(connection->xdgShell, surface.get()); auto window = Test::renderAndWaitForShown(connection->shm, surface.get(), QSize(100, 100), Qt::red); std::unique_ptr keyboard(connection->seat->createKeyboard()); QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(window); QVERIFY(keyboardEnteredSpy.wait()); const quint32 enteredSerial = keyboardEnteredSpy.last().at(0).value(); // Set the selection. dataDevice->set_selection(dataSource->object(), enteredSerial); QSignalSpy dataDeviceSelectionOfferedSpy(dataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); QVERIFY(dataDeviceSelectionOfferedSpy.wait()); Test::WpPrimarySelectionOfferV1 *offer = dataDevice->offer(); QCOMPARE(offer->mimeTypes(), QList{plainText()}); // Ask for data. const QFuture plainData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(offer, htmlText()); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Clear selection. QSignalSpy selectionClearedSpy(dataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionCleared); dataDevice->set_selection(nullptr, enteredSerial); QVERIFY(selectionClearedSpy.wait()); } void SelectionTest::destroyPrimarySelection() { // This test verifies that the wp_primary_selection_offer_v1 will be withdrawn if the associated data source is destroyed. auto sourceConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); auto targetConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); // Setup the source side. std::unique_ptr sourceDataDevice(sourceConnection->primarySelectionManager->getDevice(sourceConnection->seat)); std::unique_ptr dataSource(sourceConnection->primarySelectionManager->createSource()); dataSource->offer(plainText().name()); connect(dataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto sourceSurface = Test::createSurface(sourceConnection->compositor); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceConnection->xdgShell, sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceConnection->shm, sourceSurface.get(), QSize(100, 100), Qt::red); // Setup the target side. std::unique_ptr targetDataDevice(targetConnection->primarySelectionManager->getDevice(targetConnection->seat)); auto targetSurface = Test::createSurface(targetConnection->compositor); auto targetShellSurface = Test::createXdgToplevelSurface(targetConnection->xdgShell, targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetConnection->shm, targetSurface.get(), QSize(100, 100), Qt::blue); // Activate the source window. std::unique_ptr sourceKeyboard(sourceConnection->seat->createKeyboard()); QSignalSpy sourceKeyboardEnteredSpy(sourceKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(sourceWindow); QVERIFY(sourceKeyboardEnteredSpy.wait()); const quint32 sourceEnteredSerial = sourceKeyboardEnteredSpy.last().at(0).value(); // Set the selection. sourceDataDevice->set_selection(dataSource->object(), sourceEnteredSerial); // Activate the target window and wait for the data offer. QSignalSpy targetDataDeviceSelectionOfferedSpy(targetDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); workspace()->activateWindow(targetWindow); QVERIFY(targetDataDeviceSelectionOfferedSpy.wait()); Test::WpPrimarySelectionOfferV1 *offer = targetDataDevice->offer(); QCOMPARE(offer->mimeTypes(), QList{plainText()}); // Destroy the data source. QSignalSpy targetDataDeviceSelectionClearedSpy(targetDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionCleared); dataSource.reset(); QVERIFY(targetDataDeviceSelectionClearedSpy.wait()); } void SelectionTest::invalidSerialForPrimarySelection() { // This test verifies that a set_selection request will be ignored if the specified serial is invalid. auto connection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); std::unique_ptr dataDevice(connection->primarySelectionManager->getDevice(connection->seat)); std::unique_ptr dataSource(connection->primarySelectionManager->createSource()); dataSource->offer(QStringLiteral("text/plain")); connect(dataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto surface = Test::createSurface(connection->compositor); auto shellSurface = Test::createXdgToplevelSurface(connection->xdgShell, surface.get()); Test::renderAndWaitForShown(connection->shm, surface.get(), QSize(100, 100), Qt::red); std::unique_ptr keyboard(connection->seat->createKeyboard()); QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); QVERIFY(keyboardEnteredSpy.wait()); const quint32 enteredSerial = keyboardEnteredSpy.last().at(0).value(); // Set the selection. QSignalSpy dataSourceCancelledSpy(dataSource.get(), &Test::WpPrimarySelectionSourceV1::cancelled); dataDevice->set_selection(dataSource->object(), enteredSerial - 100); QVERIFY(dataSourceCancelledSpy.wait()); } void SelectionTest::unsetSupersededPrimarySelection() { // This test verifies that the current selection won't be unset if the previous selection is unset with set_selection(null). // Setup the first client. auto firstConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); QVERIFY(Test::waitForWaylandKeyboard(firstConnection->seat)); std::unique_ptr firstKeyboard(firstConnection->seat->createKeyboard()); std::unique_ptr firstDataDevice(firstConnection->primarySelectionManager->getDevice(firstConnection->seat)); std::unique_ptr firstDataSource(firstConnection->primarySelectionManager->createSource()); firstDataSource->offer(plainText().name()); connect(firstDataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto firstSurface = Test::createSurface(firstConnection->compositor); auto firstShellSurface = Test::createXdgToplevelSurface(firstConnection->xdgShell, firstSurface.get()); auto firstWindow = Test::renderAndWaitForShown(firstConnection->shm, firstSurface.get(), QSize(100, 100), Qt::red); // Setup the second client. auto secondConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); QVERIFY(Test::waitForWaylandKeyboard(secondConnection->seat)); std::unique_ptr secondKeyboard(secondConnection->seat->createKeyboard()); std::unique_ptr secondDataDevice(secondConnection->primarySelectionManager->getDevice(secondConnection->seat)); std::unique_ptr secondDataSource(secondConnection->primarySelectionManager->createSource()); secondDataSource->offer(htmlText().name()); connect(secondDataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("bar"); write(fd, data.data(), data.size()); close(fd); }); auto secondSurface = Test::createSurface(secondConnection->compositor); auto secondShellSurface = Test::createXdgToplevelSurface(secondConnection->xdgShell, secondSurface.get()); auto secondWindow = Test::renderAndWaitForShown(secondConnection->shm, secondSurface.get(), QSize(100, 100), Qt::blue); // Activate the first window and set the selection. QSignalSpy firstKeyboardEnteredSpy(firstKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy firstDataDeviceSelectionOfferedSpy(firstDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); workspace()->activateWindow(firstWindow); QVERIFY(firstKeyboardEnteredSpy.wait()); const quint32 firstEnteredSerial = firstKeyboardEnteredSpy.last().at(0).value(); firstDataDevice->set_selection(firstDataSource->object(), firstEnteredSerial); QVERIFY(firstDataDeviceSelectionOfferedSpy.wait()); // Activate the second window and set the selection. QSignalSpy secondKeyboardEnteredSpy(secondKeyboard.get(), &KWayland::Client::Keyboard::entered); QSignalSpy secondDataDeviceSelectionOfferedSpy(secondDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); workspace()->activateWindow(secondWindow); QVERIFY(secondKeyboardEnteredSpy.wait()); const quint32 secondEnteredSerial = secondKeyboardEnteredSpy.last().at(0).value(); secondDataDevice->set_selection(secondDataSource->object(), secondEnteredSerial); QVERIFY(secondDataDeviceSelectionOfferedSpy.wait()); // Attempt to unset the first selection. QSignalSpy secondDataDeviceSelectionClearedSpy(secondDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionCleared); firstDataDevice->set_selection(nullptr, firstEnteredSerial); QVERIFY(!secondDataDeviceSelectionClearedSpy.wait(100)); // Attempt to unset the second selection. secondDataDevice->set_selection(nullptr, secondEnteredSerial); QVERIFY(secondDataDeviceSelectionClearedSpy.wait()); } void SelectionTest::receiveFromWithdrawnPrimarySelectionOffer() { // This test verifies that a client cannot receive data from a withdrawn primary selection offer. auto sourceConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); auto targetConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); // Setup the source side. std::unique_ptr sourceDataDevice(sourceConnection->primarySelectionManager->getDevice(sourceConnection->seat)); std::unique_ptr dataSource(sourceConnection->primarySelectionManager->createSource()); dataSource->offer(plainText().name()); connect(dataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto sourceSurface = Test::createSurface(sourceConnection->compositor); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceConnection->xdgShell, sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceConnection->shm, sourceSurface.get(), QSize(100, 100), Qt::red); // Setup the target side. std::unique_ptr targetDataDevice(targetConnection->primarySelectionManager->getDevice(targetConnection->seat)); auto targetSurface = Test::createSurface(targetConnection->compositor); auto targetShellSurface = Test::createXdgToplevelSurface(targetConnection->xdgShell, targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetConnection->shm, targetSurface.get(), QSize(100, 100), Qt::blue); // Focus the source window, and set the primary selection. std::unique_ptr sourceKeyboard(sourceConnection->seat->createKeyboard()); QSignalSpy sourceKeyboardEnteredSpy(sourceKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(sourceWindow); QVERIFY(sourceKeyboardEnteredSpy.wait()); const quint32 sourceEnteredSerial = sourceKeyboardEnteredSpy.last().at(0).value(); sourceDataDevice->set_selection(dataSource->object(), sourceEnteredSerial); // The target client should be offered the selection when its window gets focused. QSignalSpy targetDataDeviceSelectionOfferedSpy(targetDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); workspace()->activateWindow(targetWindow); QVERIFY(targetDataDeviceSelectionOfferedSpy.wait()); std::unique_ptr offer = targetDataDevice->takeOffer(); QCOMPARE(offer->mimeTypes(), QList{plainText()}); // Ask for data. const QFuture plainData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // Remove focus from the target window, it should not be able to receive data anymore. workspace()->activateWindow(nullptr); const QFuture emptyData = readMimeTypeData(offer, plainText()); QVERIFY(waitFuture(emptyData)); QCOMPARE(emptyData.result(), QByteArray()); } void SelectionTest::singlePrimarySelectionPerClient() { // This test verifies that only one selection event will be sent when switching between two // surfaces that belong to the same client. // Setup the first client. auto firstConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); QVERIFY(Test::waitForWaylandKeyboard(firstConnection->seat)); std::unique_ptr firstKeyboard(firstConnection->seat->createKeyboard()); std::unique_ptr firstDataDevice(firstConnection->primarySelectionManager->getDevice(firstConnection->seat)); std::unique_ptr firstDataSource(firstConnection->primarySelectionManager->createSource()); firstDataSource->offer(plainText().name()); connect(firstDataSource.get(), &Test::WpPrimarySelectionSourceV1::sendDataRequested, this, [](const QString &mimeType, int fd) { const auto data = QByteArrayLiteral("foo"); write(fd, data.data(), data.size()); close(fd); }); auto firstSurface = Test::createSurface(firstConnection->compositor); auto firstShellSurface = Test::createXdgToplevelSurface(firstConnection->xdgShell, firstSurface.get()); auto firstWindow = Test::renderAndWaitForShown(firstConnection->shm, firstSurface.get(), QSize(100, 100), Qt::red); // Setup the second client. auto secondConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::WpPrimarySelectionV1); QVERIFY(Test::waitForWaylandKeyboard(secondConnection->seat)); std::unique_ptr secondKeyboard(secondConnection->seat->createKeyboard()); std::unique_ptr secondDataDevice(secondConnection->primarySelectionManager->getDevice(secondConnection->seat)); auto secondSurface = Test::createSurface(secondConnection->compositor); auto secondShellSurface = Test::createXdgToplevelSurface(secondConnection->xdgShell, secondSurface.get()); auto secondWindow = Test::renderAndWaitForShown(secondConnection->shm, secondSurface.get(), QSize(100, 100), Qt::blue); auto thirdSurface = Test::createSurface(secondConnection->compositor); auto thirdShellSurface = Test::createXdgToplevelSurface(secondConnection->xdgShell, thirdSurface.get()); auto thirdWindow = Test::renderAndWaitForShown(secondConnection->shm, thirdSurface.get(), QSize(100, 100), Qt::green); // Activate the first window and set the selection. QSignalSpy firstKeyboardEnteredSpy(firstKeyboard.get(), &KWayland::Client::Keyboard::entered); workspace()->activateWindow(firstWindow); QVERIFY(firstKeyboardEnteredSpy.wait()); const quint32 firstEnteredSerial = firstKeyboardEnteredSpy.last().at(0).value(); firstDataDevice->set_selection(firstDataSource->object(), firstEnteredSerial); QSignalSpy firstDataDeviceSelectionOfferedSpy(firstDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); QSignalSpy firstDataDeviceSelectionClearedSpy(firstDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionCleared); QSignalSpy secondDataDeviceSelectionOfferedSpy(secondDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionOffered); QSignalSpy secondDataDeviceSelectionClearedSpy(secondDataDevice.get(), &Test::WpPrimarySelectionDeviceV1::selectionCleared); QVERIFY(firstDataDeviceSelectionOfferedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 2nd window. workspace()->activateWindow(secondWindow); QVERIFY(secondDataDeviceSelectionOfferedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 3rd window (the 2nd and the 3rd window belong to the same client). workspace()->activateWindow(thirdWindow); QVERIFY(!secondDataDeviceSelectionOfferedSpy.wait(100)); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 2nd window (the 2nd and the 3rd window belong to the same client). workspace()->activateWindow(secondWindow); QVERIFY(!secondDataDeviceSelectionOfferedSpy.wait(100)); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 1st window (different connection from the 2nd and 3rd window). workspace()->activateWindow(firstWindow); QVERIFY(firstDataDeviceSelectionOfferedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 2); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 0); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Clear the selection. firstDataDevice->set_selection(nullptr, firstEnteredSerial); QVERIFY(firstDataDeviceSelectionClearedSpy.wait()); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 2); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 0); // Activate the 2nd window (different connection from the 1st window). workspace()->activateWindow(secondWindow); QVERIFY(!secondDataDeviceSelectionOfferedSpy.wait(100)); QCOMPARE(firstDataDeviceSelectionOfferedSpy.count(), 2); QCOMPARE(firstDataDeviceSelectionClearedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionOfferedSpy.count(), 1); QCOMPARE(secondDataDeviceSelectionClearedSpy.count(), 1); // 1 because a null selection is sent } } // namespace KWin WAYLANDTEST_MAIN(KWin::SelectionTest) #include "selection_test.moc"