summaryrefslogtreecommitdiffstats
path: root/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp')
-rw-r--r--tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp476
1 files changed, 476 insertions, 0 deletions
diff --git a/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp b/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp
new file mode 100644
index 000000000..53a048a35
--- /dev/null
+++ b/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp
@@ -0,0 +1,476 @@
+// Copyright (C) 2018 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "mockcompositor.h"
+
+#include <qwayland-server-wp-primary-selection-unstable-v1.h>
+
+#include <QtGui/QRasterWindow>
+#include <QtGui/QClipboard>
+#include <QtCore/private/qcore_unix_p.h>
+
+#include <fcntl.h>
+
+using namespace MockCompositor;
+
+constexpr int primarySelectionVersion = 1; // protocol VERSION, not the name suffix (_v1)
+
+class PrimarySelectionDeviceV1;
+class PrimarySelectionDeviceManagerV1;
+
+class PrimarySelectionOfferV1 : public QObject, public QtWaylandServer::zwp_primary_selection_offer_v1
+{
+ Q_OBJECT
+public:
+ explicit PrimarySelectionOfferV1(PrimarySelectionDeviceV1 *device, wl_client *client, int version)
+ : zwp_primary_selection_offer_v1(client, 0, version)
+ , m_device(device)
+ {}
+ void send_offer() = delete;
+ void sendOffer(const QString &offer)
+ {
+ zwp_primary_selection_offer_v1::send_offer(offer);
+ m_mimeTypes << offer;
+ }
+
+ PrimarySelectionDeviceV1 *m_device = nullptr;
+ QStringList m_mimeTypes;
+
+signals:
+ void receive(QString mimeType, int fd);
+
+protected:
+ void zwp_primary_selection_offer_v1_destroy_resource(Resource *resource) override
+ {
+ Q_UNUSED(resource);
+ delete this;
+ }
+
+ void zwp_primary_selection_offer_v1_receive(Resource *resource, const QString &mime_type, int32_t fd) override
+ {
+ Q_UNUSED(resource);
+ QTRY_VERIFY(m_mimeTypes.contains(mime_type));
+ emit receive(mime_type, fd);
+ }
+
+ void zwp_primary_selection_offer_v1_destroy(Resource *resource) override;
+};
+
+class PrimarySelectionSourceV1 : public QObject, public QtWaylandServer::zwp_primary_selection_source_v1
+{
+ Q_OBJECT
+public:
+ explicit PrimarySelectionSourceV1(wl_client *client, int id, int version)
+ : zwp_primary_selection_source_v1(client, id, version)
+ {
+ }
+ QStringList m_offers;
+protected:
+ void zwp_primary_selection_source_v1_destroy_resource(Resource *resource) override
+ {
+ Q_UNUSED(resource);
+ delete this;
+ }
+ void zwp_primary_selection_source_v1_offer(Resource *resource, const QString &mime_type) override
+ {
+ Q_UNUSED(resource);
+ m_offers << mime_type;
+ }
+ void zwp_primary_selection_source_v1_destroy(Resource *resource) override
+ {
+ wl_resource_destroy(resource->handle);
+ }
+};
+
+class PrimarySelectionDeviceV1 : public QObject, public QtWaylandServer::zwp_primary_selection_device_v1
+{
+ Q_OBJECT
+public:
+ explicit PrimarySelectionDeviceV1(PrimarySelectionDeviceManagerV1 *manager, Seat *seat)
+ : m_manager(manager)
+ , m_seat(seat)
+ {}
+
+ void send_data_offer(::wl_resource *resource) = delete;
+
+ PrimarySelectionOfferV1 *sendDataOffer(::wl_client *client, const QStringList &mimeTypes = {});
+
+ PrimarySelectionOfferV1 *sendDataOffer(const QStringList &mimeTypes = {}) // creates a new offer for the focused surface and sends it
+ {
+ Q_ASSERT(m_seat->m_capabilities & Seat::capability_keyboard);
+ Q_ASSERT(m_seat->m_keyboard->m_enteredSurface);
+ auto *client = m_seat->m_keyboard->m_enteredSurface->resource()->client();
+ return sendDataOffer(client, mimeTypes);
+ }
+
+ void send_selection(::wl_resource *resource) = delete;
+ void sendSelection(PrimarySelectionOfferV1 *offer)
+ {
+ auto *client = offer->resource()->client();
+ for (auto *resource : resourceMap().values(client))
+ zwp_primary_selection_device_v1::send_selection(resource->handle, offer->resource()->handle);
+ m_sentSelectionOffers << offer;
+ }
+
+ PrimarySelectionDeviceManagerV1 *m_manager = nullptr;
+ Seat *m_seat = nullptr;
+ QList<PrimarySelectionOfferV1 *> m_sentSelectionOffers;
+ PrimarySelectionSourceV1 *m_selectionSource = nullptr;
+ uint m_serial = 0;
+
+protected:
+ void zwp_primary_selection_device_v1_set_selection(Resource *resource, ::wl_resource *source, uint32_t serial) override
+ {
+ Q_UNUSED(resource);
+ m_selectionSource = fromResource<PrimarySelectionSourceV1>(source);
+ m_serial = serial;
+ }
+ void zwp_primary_selection_device_v1_destroy(Resource *resource) override
+ {
+ wl_resource_destroy(resource->handle);
+ }
+};
+
+class PrimarySelectionDeviceManagerV1 : public Global, public QtWaylandServer::zwp_primary_selection_device_manager_v1
+{
+ Q_OBJECT
+public:
+ explicit PrimarySelectionDeviceManagerV1(CoreCompositor *compositor, int version = 1)
+ : QtWaylandServer::zwp_primary_selection_device_manager_v1(compositor->m_display, version)
+ , m_version(version)
+ {}
+ ~PrimarySelectionDeviceManagerV1() override
+ {
+ qDeleteAll(m_devices);
+ }
+ bool isClean() override
+ {
+ for (auto *device : std::as_const(m_devices)) {
+ // The client should not leak selection offers, i.e. if this fails, there is a missing
+ // zwp_primary_selection_offer_v1.destroy request
+ if (!device->m_sentSelectionOffers.empty())
+ return false;
+ }
+ return true;
+ }
+
+ PrimarySelectionDeviceV1 *deviceFor(Seat *seat)
+ {
+ Q_ASSERT(seat);
+ if (auto *device = m_devices.value(seat, nullptr))
+ return device;
+
+ auto *device = new PrimarySelectionDeviceV1(this, seat);
+ m_devices[seat] = device;
+ return device;
+ }
+
+ int m_version = 1; // TODO: Remove on libwayland upgrade
+ QMap<Seat *, PrimarySelectionDeviceV1 *> m_devices;
+ QList<PrimarySelectionSourceV1 *> m_sources;
+protected:
+ void zwp_primary_selection_device_manager_v1_destroy(Resource *resource) override
+ {
+ // The protocol doesn't say whether managed objects should be destroyed as well,
+ // so leave them alone, they'll be cleaned up in the destructor anyway
+ wl_resource_destroy(resource->handle);
+ }
+
+ void zwp_primary_selection_device_manager_v1_create_source(Resource *resource, uint32_t id) override
+ {
+ int version = m_version;
+ m_sources << new PrimarySelectionSourceV1(resource->client(), id, version);
+ }
+ void zwp_primary_selection_device_manager_v1_get_device(Resource *resource, uint32_t id, ::wl_resource *seatResource) override
+ {
+ auto *seat = fromResource<Seat>(seatResource);
+ QVERIFY(seat);
+ auto *device = deviceFor(seat);
+ device->add(resource->client(), id, resource->version());
+ }
+};
+
+PrimarySelectionOfferV1 *PrimarySelectionDeviceV1::sendDataOffer(wl_client *client, const QStringList &mimeTypes)
+{
+ Q_ASSERT(client);
+ auto *offer = new PrimarySelectionOfferV1(this, client, m_manager->m_version);
+ for (auto *resource : resourceMap().values(client))
+ zwp_primary_selection_device_v1::send_data_offer(resource->handle, offer->resource()->handle);
+ for (const auto &mimeType : mimeTypes)
+ offer->sendOffer(mimeType);
+ return offer;
+}
+
+void PrimarySelectionOfferV1::zwp_primary_selection_offer_v1_destroy(QtWaylandServer::zwp_primary_selection_offer_v1::Resource *resource)
+{
+ bool removed = m_device->m_sentSelectionOffers.removeOne(this);
+ QVERIFY(removed);
+ wl_resource_destroy(resource->handle);
+}
+
+class PrimarySelectionCompositor : public DefaultCompositor {
+public:
+ explicit PrimarySelectionCompositor()
+ {
+ exec([this] {
+ m_config.autoConfigure = true;
+ add<PrimarySelectionDeviceManagerV1>(primarySelectionVersion);
+ });
+ }
+ PrimarySelectionDeviceV1 *primarySelectionDevice(int i = 0) {
+ return get<PrimarySelectionDeviceManagerV1>()->deviceFor(get<Seat>(i));
+ }
+};
+
+class tst_primaryselectionv1 : public QObject, private PrimarySelectionCompositor
+{
+ Q_OBJECT
+private slots:
+ void cleanup() { QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage())); }
+ void initTestCase();
+ void bindsToManager();
+ void createsPrimaryDevice();
+ void createsPrimaryDeviceForNewSeats();
+ void pasteAscii();
+ void pasteUtf8();
+ void destroysPreviousSelection();
+ void destroysSelectionOnLeave();
+ void copy();
+};
+
+void tst_primaryselectionv1::initTestCase()
+{
+ QCOMPOSITOR_TRY_VERIFY(pointer());
+ QCOMPOSITOR_TRY_VERIFY(keyboard());
+}
+
+void tst_primaryselectionv1::bindsToManager()
+{
+ QCOMPOSITOR_TRY_COMPARE(get<PrimarySelectionDeviceManagerV1>()->resourceMap().size(), 1);
+ QCOMPOSITOR_TRY_COMPARE(get<PrimarySelectionDeviceManagerV1>()->resourceMap().first()->version(), primarySelectionVersion);
+}
+
+void tst_primaryselectionv1::createsPrimaryDevice()
+{
+ QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice());
+ QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()->resourceMap().contains(client()));
+ QCOMPOSITOR_TRY_COMPARE(primarySelectionDevice()->resourceMap().value(client())->version(), primarySelectionVersion);
+ QTRY_VERIFY(QGuiApplication::clipboard()->supportsSelection());
+}
+
+void tst_primaryselectionv1::createsPrimaryDeviceForNewSeats()
+{
+ exec([&] { add<Seat>(); });
+ QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice(1));
+}
+
+void tst_primaryselectionv1::pasteAscii()
+{
+ class Window : public QRasterWindow {
+ public:
+ void mousePressEvent(QMouseEvent *event) override
+ {
+ Q_UNUSED(event);
+ auto *mimeData = QGuiApplication::clipboard()->mimeData(QClipboard::Selection);
+ m_formats = mimeData->formats();
+ m_text = QGuiApplication::clipboard()->text(QClipboard::Selection);
+ }
+ QStringList m_formats;
+ QString m_text;
+ };
+
+ Window window;
+ window.resize(64, 64);
+ window.show();
+
+ QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
+ exec([&] {
+ auto *surface = xdgSurface()->m_surface;
+ keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
+
+ auto *device = primarySelectionDevice();
+ auto *offer = device->sendDataOffer({"text/plain"});
+ connect(offer, &PrimarySelectionOfferV1::receive, offer, [](QString mimeType, int fd) {
+ QFile file;
+ file.open(fd, QIODevice::WriteOnly, QFile::FileHandleFlag::AutoCloseHandle);
+ QCOMPARE(mimeType, "text/plain");
+ file.write(QByteArray("normal ascii"));
+ file.close();
+ }, Qt::DirectConnection);
+ device->sendSelection(offer);
+
+ pointer()->sendEnter(surface, {32, 32});
+ pointer()->sendFrame(client());
+ pointer()->sendButton(client(), BTN_MIDDLE, 1);
+ pointer()->sendFrame(client());
+ pointer()->sendButton(client(), BTN_MIDDLE, 0);
+ pointer()->sendFrame(client());
+ });
+ QTRY_COMPARE(window.m_formats, QStringList{"text/plain"});
+ QTRY_COMPARE(window.m_text, "normal ascii");
+}
+
+void tst_primaryselectionv1::pasteUtf8()
+{
+ class Window : public QRasterWindow {
+ public:
+ void mousePressEvent(QMouseEvent *event) override
+ {
+ Q_UNUSED(event);
+ auto *mimeData = QGuiApplication::clipboard()->mimeData(QClipboard::Selection);
+ m_formats = mimeData->formats();
+ m_text = QGuiApplication::clipboard()->text(QClipboard::Selection);
+ }
+ QStringList m_formats;
+ QString m_text;
+ };
+
+ Window window;
+ window.resize(64, 64);
+ window.show();
+
+ QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
+ exec([&] {
+ auto *surface = xdgSurface()->m_surface;
+ keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
+
+ auto *device = primarySelectionDevice();
+ auto *offer = device->sendDataOffer({"text/plain", "text/plain;charset=utf-8"});
+ connect(offer, &PrimarySelectionOfferV1::receive, offer, [](QString mimeType, int fd) {
+ QFile file;
+ file.open(fd, QIODevice::WriteOnly, QFile::FileHandleFlag::AutoCloseHandle);
+ QCOMPARE(mimeType, "text/plain;charset=utf-8");
+ file.write(QByteArray("face with tears of joy: 😂"));
+ file.close();
+ }, Qt::DirectConnection);
+ device->sendSelection(offer);
+
+ pointer()->sendEnter(surface, {32, 32});
+ pointer()->sendFrame(client());
+ pointer()->sendButton(client(), BTN_MIDDLE, 1);
+ pointer()->sendFrame(client());
+ pointer()->sendButton(client(), BTN_MIDDLE, 0);
+ pointer()->sendFrame(client());
+ });
+ QTRY_COMPARE(window.m_formats, QStringList({"text/plain", "text/plain;charset=utf-8"}));
+ QTRY_COMPARE(window.m_text, "face with tears of joy: 😂");
+}
+
+void tst_primaryselectionv1::destroysPreviousSelection()
+{
+ QRasterWindow window;
+ window.resize(64, 64);
+ window.show();
+ QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
+
+ // When the client receives a selection event, it is required to destroy the previous offer
+ exec([&] {
+ auto *surface = xdgSurface()->m_surface;
+ keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
+
+ auto *offer = primarySelectionDevice()->sendDataOffer({"text/plain"});
+ primarySelectionDevice()->sendSelection(offer);
+ });
+
+ exec([&] {
+ auto *offer = primarySelectionDevice()->sendDataOffer({"text/plain"});
+ primarySelectionDevice()->sendSelection(offer);
+ QCOMPARE(primarySelectionDevice()->m_sentSelectionOffers.size(), 2);
+ });
+
+ // Verify the first offer gets destroyed
+ QCOMPOSITOR_TRY_COMPARE(primarySelectionDevice()->m_sentSelectionOffers.size(), 1);
+}
+
+void tst_primaryselectionv1::destroysSelectionOnLeave()
+{
+ QRasterWindow window;
+ window.resize(64, 64);
+ window.show();
+ QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
+
+ exec([&] {
+ auto *surface = xdgSurface()->m_surface;
+ keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
+
+ auto *offer = primarySelectionDevice()->sendDataOffer({"text/plain"});
+ primarySelectionDevice()->sendSelection(offer);
+ });
+
+ QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Selection));
+ QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Selection)->hasText());
+
+ QSignalSpy selectionChangedSpy(QGuiApplication::clipboard(), &QClipboard::selectionChanged);
+
+ exec([&] {
+ auto *surface = xdgSurface()->m_surface;
+ keyboard()->sendLeave(surface);
+ });
+
+ QTRY_COMPARE(selectionChangedSpy.size(), 1);
+ QVERIFY(!QGuiApplication::clipboard()->mimeData(QClipboard::Selection)->hasText());
+}
+
+void tst_primaryselectionv1::copy()
+{
+ class Window : public QRasterWindow {
+ public:
+ void mousePressEvent(QMouseEvent *event) override
+ {
+ Q_UNUSED(event);
+ QGuiApplication::clipboard()->setText("face with tears of joy: 😂", QClipboard::Selection);
+ }
+ QStringList m_formats;
+ QString m_text;
+ };
+
+ Window window;
+ window.resize(64, 64);
+ window.show();
+
+ QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
+ QList<uint> mouseSerials;
+ exec([&] {
+ auto *surface = xdgSurface()->m_surface;
+ keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol
+ pointer()->sendEnter(surface, {32, 32});
+ pointer()->sendFrame(client());
+ mouseSerials << pointer()->sendButton(client(), BTN_MIDDLE, 1);
+ pointer()->sendFrame(client());
+ mouseSerials << pointer()->sendButton(client(), BTN_MIDDLE, 0);
+ pointer()->sendFrame(client());
+ });
+ QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()->m_selectionSource);
+ QCOMPOSITOR_TRY_VERIFY(mouseSerials.contains(primarySelectionDevice()->m_serial));
+ QVERIFY(QGuiApplication::clipboard()->ownsSelection());
+ QByteArray pastedBuf;
+ exec([&](){
+ auto *source = primarySelectionDevice()->m_selectionSource;
+ QCOMPARE(source->m_offers, QStringList({"text/plain", "text/plain;charset=utf-8"}));
+ int fd[2];
+ if (pipe(fd) == -1)
+ QSKIP("Failed to create pipe");
+ fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL, 0) | O_NONBLOCK);
+ source->send_send("text/plain;charset=utf-8", fd[1]);
+ auto *notifier = new QSocketNotifier(fd[0], QSocketNotifier::Read, this);
+ connect(notifier, &QSocketNotifier::activated, this, [&](int fd) {
+ exec([&]{
+ static char buf[1024];
+ int n = QT_READ(fd, buf, sizeof buf);
+ if (n <= 0) {
+ delete notifier;
+ close(fd);
+ } else {
+ pastedBuf.append(buf, n);
+ }
+ });
+ }, Qt::DirectConnection);
+ });
+
+ QCOMPOSITOR_TRY_VERIFY(pastedBuf.size()); // this assumes we got everything in one read
+ auto pasted = QString::fromUtf8(pastedBuf);
+ QCOMPARE(pasted, "face with tears of joy: 😂");
+}
+
+QCOMPOSITOR_TEST_MAIN(tst_primaryselectionv1)
+#include "tst_primaryselectionv1.moc"