1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | |
30 | #include <QtTest/QtTest> |
31 | #include <QtCore/QDebug> |
32 | #include <QtCore/QFileInfo> |
33 | #include <QtCore/QDir> |
34 | #include <QtGui/QGuiApplication> |
35 | #include <QtGui/QClipboard> |
36 | #include <QtGui/QImage> |
37 | #include <QtGui/QColor> |
38 | #include "../../../shared/platformclipboard.h" |
39 | |
40 | class tst_QClipboard : public QObject |
41 | { |
42 | Q_OBJECT |
43 | private slots: |
44 | void initTestCase(); |
45 | #if QT_CONFIG(clipboard) |
46 | void init(); |
47 | #if defined(Q_OS_WIN) || defined(Q_OS_MAC) || defined(Q_OS_QNX) |
48 | void copy_exit_paste(); |
49 | void copyImage(); |
50 | #endif |
51 | void capabilityFunctions(); |
52 | void modes(); |
53 | void testSignals(); |
54 | void setMimeData(); |
55 | void clearBeforeSetText(); |
56 | #endif |
57 | }; |
58 | |
59 | void tst_QClipboard::initTestCase() |
60 | { |
61 | #if !QT_CONFIG(clipboard) |
62 | QSKIP("This test requires clipboard support" ); |
63 | #endif |
64 | if (QGuiApplication::platformName().startsWith(s: QLatin1String("wayland" ), cs: Qt::CaseInsensitive)) |
65 | QSKIP("Wayland: Manipulating the clipboard requires real input events. Can't auto test." ); |
66 | } |
67 | |
68 | #if QT_CONFIG(clipboard) |
69 | void tst_QClipboard::init() |
70 | { |
71 | #if QT_CONFIG(process) |
72 | const QString testdataDir = QFileInfo(QFINDTESTDATA("copier" )).absolutePath(); |
73 | QVERIFY2(QDir::setCurrent(testdataDir), qPrintable("Could not chdir to " + testdataDir)); |
74 | #endif |
75 | } |
76 | |
77 | Q_DECLARE_METATYPE(QClipboard::Mode) |
78 | |
79 | /* |
80 | Tests that the capability functions are implemented on all |
81 | platforms. |
82 | */ |
83 | void tst_QClipboard::capabilityFunctions() |
84 | { |
85 | QClipboard * const clipboard = QGuiApplication::clipboard(); |
86 | |
87 | clipboard->supportsSelection(); |
88 | clipboard->supportsFindBuffer(); |
89 | clipboard->ownsSelection(); |
90 | clipboard->ownsClipboard(); |
91 | clipboard->ownsFindBuffer(); |
92 | } |
93 | |
94 | /* |
95 | Test that text inserted into the clipboard in different modes is |
96 | kept separate. |
97 | */ |
98 | void tst_QClipboard::modes() |
99 | { |
100 | QClipboard * const clipboard = QGuiApplication::clipboard(); |
101 | |
102 | if (!PlatformClipboard::isAvailable()) |
103 | QSKIP("Native clipboard not working in this setup" ); |
104 | |
105 | const QString defaultMode = "default mode text;" ; |
106 | clipboard->setText(defaultMode); |
107 | QCOMPARE(clipboard->text(), defaultMode); |
108 | |
109 | if (clipboard->supportsSelection()) { |
110 | const QString selectionMode = "selection mode text" ; |
111 | clipboard->setText(selectionMode, mode: QClipboard::Selection); |
112 | QCOMPARE(clipboard->text(QClipboard::Selection), selectionMode); |
113 | QCOMPARE(clipboard->text(), defaultMode); |
114 | } |
115 | |
116 | if (clipboard->supportsFindBuffer()) { |
117 | const QString searchMode = "find mode text" ; |
118 | clipboard->setText(searchMode, mode: QClipboard::FindBuffer); |
119 | QCOMPARE(clipboard->text(QClipboard::FindBuffer), searchMode); |
120 | QCOMPARE(clipboard->text(), defaultMode); |
121 | } |
122 | } |
123 | |
124 | // A predicate to be used with a QSignalSpy / QTRY_VERIFY to ensure all delayed |
125 | // notifications are eaten. It waits at least one cycle and returns true when |
126 | // no new signals arrive. |
127 | class EatSignalSpyNotificationsPredicate |
128 | { |
129 | public: |
130 | explicit EatSignalSpyNotificationsPredicate(QSignalSpy &spy) : m_spy(spy) { reset(); } |
131 | |
132 | operator bool() const |
133 | { |
134 | if (m_timer.elapsed() && !m_spy.count()) |
135 | return true; |
136 | m_spy.clear(); |
137 | return false; |
138 | } |
139 | |
140 | inline void reset() { m_timer.start(); } |
141 | |
142 | private: |
143 | QSignalSpy &m_spy; |
144 | QElapsedTimer m_timer; |
145 | }; |
146 | |
147 | /* |
148 | Test that the appropriate signals are emitted when the clipboard |
149 | contents is changed by calling the qt functions. |
150 | */ |
151 | void tst_QClipboard::testSignals() |
152 | { |
153 | qRegisterMetaType<QClipboard::Mode>(typeName: "QClipboard::Mode" ); |
154 | |
155 | if (!PlatformClipboard::isAvailable()) |
156 | QSKIP("Native clipboard not working in this setup" ); |
157 | |
158 | QClipboard * const clipboard = QGuiApplication::clipboard(); |
159 | |
160 | QSignalSpy changedSpy(clipboard, SIGNAL(changed(QClipboard::Mode))); |
161 | QSignalSpy dataChangedSpy(clipboard, SIGNAL(dataChanged())); |
162 | // Clipboard notifications are asynchronous with the new AddClipboardFormatListener |
163 | // in Windows Vista (5.4). Eat away all signals to ensure they don't interfere |
164 | // with the QTRY_COMPARE below. |
165 | EatSignalSpyNotificationsPredicate noLeftOverDataChanges(dataChangedSpy); |
166 | EatSignalSpyNotificationsPredicate noLeftOverChanges(changedSpy); |
167 | QTRY_VERIFY(noLeftOverChanges && noLeftOverDataChanges); |
168 | |
169 | QSignalSpy searchChangedSpy(clipboard, SIGNAL(findBufferChanged())); |
170 | QSignalSpy selectionChangedSpy(clipboard, SIGNAL(selectionChanged())); |
171 | |
172 | const QString text = "clipboard text;" ; |
173 | |
174 | // Test the default mode signal. |
175 | clipboard->setText(text); |
176 | QTRY_COMPARE(dataChangedSpy.count(), 1); |
177 | QCOMPARE(searchChangedSpy.count(), 0); |
178 | QCOMPARE(selectionChangedSpy.count(), 0); |
179 | QCOMPARE(changedSpy.count(), 1); |
180 | QCOMPARE(changedSpy.at(0).count(), 1); |
181 | QCOMPARE(qvariant_cast<QClipboard::Mode>(changedSpy.at(0).at(0)), QClipboard::Clipboard); |
182 | |
183 | changedSpy.clear(); |
184 | |
185 | // Test the selection mode signal. |
186 | if (clipboard->supportsSelection()) { |
187 | clipboard->setText(text, mode: QClipboard::Selection); |
188 | QCOMPARE(selectionChangedSpy.count(), 1); |
189 | QCOMPARE(changedSpy.count(), 1); |
190 | QCOMPARE(changedSpy.at(0).count(), 1); |
191 | QCOMPARE(qvariant_cast<QClipboard::Mode>(changedSpy.at(0).at(0)), QClipboard::Selection); |
192 | } else { |
193 | QCOMPARE(selectionChangedSpy.count(), 0); |
194 | } |
195 | QCOMPARE(dataChangedSpy.count(), 1); |
196 | QCOMPARE(searchChangedSpy.count(), 0); |
197 | |
198 | changedSpy.clear(); |
199 | |
200 | // Test the search mode signal. |
201 | if (clipboard->supportsFindBuffer()) { |
202 | clipboard->setText(text, mode: QClipboard::FindBuffer); |
203 | QCOMPARE(searchChangedSpy.count(), 1); |
204 | QCOMPARE(changedSpy.count(), 1); |
205 | QCOMPARE(changedSpy.at(0).count(), 1); |
206 | QCOMPARE(qvariant_cast<QClipboard::Mode>(changedSpy.at(0).at(0)), QClipboard::FindBuffer); |
207 | } else { |
208 | QCOMPARE(searchChangedSpy.count(), 0); |
209 | } |
210 | QCOMPARE(dataChangedSpy.count(), 1); |
211 | } |
212 | |
213 | #if defined(Q_OS_WIN) || defined(Q_OS_MAC) || defined(Q_OS_QNX) |
214 | static bool runHelper(const QString &program, const QStringList &arguments, QByteArray *errorMessage) |
215 | { |
216 | #if QT_CONFIG(process) |
217 | QProcess process; |
218 | process.setProcessChannelMode(QProcess::ForwardedChannels); |
219 | process.start(program, arguments); |
220 | if (!process.waitForStarted()) { |
221 | *errorMessage = "Unable to start '" + program.toLocal8Bit() + " ': " |
222 | + process.errorString().toLocal8Bit(); |
223 | return false; |
224 | } |
225 | |
226 | // Windows: Due to implementation changes, the event loop needs |
227 | // to be spun since we ourselves also need to answer the |
228 | // WM_DRAWCLIPBOARD message as we are in the chain of clipboard |
229 | // viewers. Check for running before waitForFinished() in case |
230 | // the process terminated while processEvents() was executed. |
231 | bool running = true; |
232 | for (int i = 0; i < 60 && running; ++i) { |
233 | QGuiApplication::processEvents(QEventLoop::ExcludeUserInputEvents); |
234 | if (process.state() != QProcess::Running || process.waitForFinished(500)) |
235 | running = false; |
236 | } |
237 | if (running) { |
238 | process.kill(); |
239 | *errorMessage = "Timeout running '" + program.toLocal8Bit() + '\''; |
240 | return false; |
241 | } |
242 | if (process.exitStatus() != QProcess::NormalExit) { |
243 | *errorMessage = "Process '" + program.toLocal8Bit() + "' crashed." ; |
244 | return false; |
245 | } |
246 | if (process.exitCode()) { |
247 | *errorMessage = "Process '" + program.toLocal8Bit() + "' returns " |
248 | + QByteArray::number(process.exitCode()); |
249 | return false; |
250 | } |
251 | return true; |
252 | #else // QT_CONFIG(process) |
253 | Q_UNUSED(program) |
254 | Q_UNUSED(arguments) |
255 | Q_UNUSED(errorMessage) |
256 | return false; |
257 | #endif // QT_CONFIG(process) |
258 | } |
259 | |
260 | // Test that pasted text remains on the clipboard after a Qt application exits. |
261 | // This test does not make sense on X11 and embedded, copied data disappears from the clipboard when the application exits |
262 | void tst_QClipboard::copy_exit_paste() |
263 | { |
264 | #if QT_CONFIG(process) |
265 | // ### It's still possible to test copy/paste - just keep the apps running |
266 | if (!PlatformClipboard::isAvailable()) |
267 | QSKIP("Native clipboard not working in this setup" ); |
268 | const QString stringArgument(QStringLiteral("Test string." )); |
269 | QByteArray errorMessage; |
270 | QVERIFY2(runHelper(QStringLiteral("copier/copier" ), QStringList(stringArgument), &errorMessage), |
271 | errorMessage.constData()); |
272 | #ifdef Q_OS_MAC |
273 | // The Pasteboard needs a moment to breathe (at least on older Macs). |
274 | QTest::qWait(100); |
275 | #endif // Q_OS_MAC |
276 | QVERIFY2(runHelper(QStringLiteral("paster/paster" ), |
277 | QStringList() << QStringLiteral("--text" ) << stringArgument, |
278 | &errorMessage), |
279 | errorMessage.constData()); |
280 | #endif // QT_CONFIG(process) |
281 | } |
282 | |
283 | void tst_QClipboard::copyImage() |
284 | { |
285 | #if QT_CONFIG(process) |
286 | if (!PlatformClipboard::isAvailable()) |
287 | QSKIP("Native clipboard not working in this setup" ); |
288 | QImage image(100, 100, QImage::Format_ARGB32); |
289 | image.fill(QColor(Qt::transparent)); |
290 | image.setPixel(QPoint(1, 0), QColor(Qt::blue).rgba()); |
291 | QGuiApplication::clipboard()->setImage(image); |
292 | #ifdef Q_OS_MACOS |
293 | // The Pasteboard needs a moment to breathe (at least on older Macs). |
294 | QTest::qWait(100); |
295 | #endif // Q_OS_MACOS |
296 | // paster will perform hard-coded checks on the copied image. |
297 | QByteArray errorMessage; |
298 | QVERIFY2(runHelper(QStringLiteral("paster/paster" ), |
299 | QStringList(QStringLiteral("--image" )), &errorMessage), |
300 | errorMessage.constData()); |
301 | #endif // QT_CONFIG(process) |
302 | } |
303 | |
304 | #endif // Q_OS_WIN || Q_OS_MAC || Q_OS_QNX |
305 | |
306 | void tst_QClipboard::setMimeData() |
307 | { |
308 | if (!PlatformClipboard::isAvailable()) |
309 | QSKIP("Native clipboard not working in this setup" ); |
310 | QMimeData *mimeData = new QMimeData; |
311 | const QString TestName(QLatin1String("tst_QClipboard::setMimeData() mimeData" )); |
312 | mimeData->setObjectName(TestName); |
313 | |
314 | QGuiApplication::clipboard()->setMimeData(data: mimeData); |
315 | QCOMPARE(QGuiApplication::clipboard()->mimeData(), (const QMimeData *)mimeData); |
316 | QCOMPARE(QGuiApplication::clipboard()->mimeData()->objectName(), TestName); |
317 | |
318 | // set it to the same data again, it shouldn't delete mimeData (and crash as a result) |
319 | QGuiApplication::clipboard()->setMimeData(data: mimeData); |
320 | QCOMPARE(QGuiApplication::clipboard()->mimeData(), (const QMimeData *)mimeData); |
321 | QCOMPARE(QGuiApplication::clipboard()->mimeData()->objectName(), TestName); |
322 | QGuiApplication::clipboard()->clear(); |
323 | const QMimeData *appMimeData = QGuiApplication::clipboard()->mimeData(); |
324 | QVERIFY(appMimeData != mimeData || appMimeData->objectName() != TestName); |
325 | |
326 | // check for crash when using the same mimedata object on several clipboards |
327 | QMimeData *data = new QMimeData; |
328 | data->setText("foo" ); |
329 | |
330 | QGuiApplication::clipboard()->setMimeData(data, mode: QClipboard::Clipboard); |
331 | if (QGuiApplication::clipboard()->supportsSelection()) |
332 | QGuiApplication::clipboard()->setMimeData(data, mode: QClipboard::Selection); |
333 | if (QGuiApplication::clipboard()->supportsFindBuffer()) |
334 | QGuiApplication::clipboard()->setMimeData(data, mode: QClipboard::FindBuffer); |
335 | |
336 | QSignalSpy spySelection(QGuiApplication::clipboard(), SIGNAL(selectionChanged())); |
337 | QSignalSpy spyData(QGuiApplication::clipboard(), SIGNAL(dataChanged())); |
338 | // Clipboard notifications are asynchronous with the new AddClipboardFormatListener |
339 | // in Windows Vista (5.4). Eat away all signals to ensure they don't interfere |
340 | // with the QTRY_COMPARE below. |
341 | EatSignalSpyNotificationsPredicate noLeftOverDataChanges(spyData); |
342 | QTRY_VERIFY(noLeftOverDataChanges); |
343 | QSignalSpy spyFindBuffer(QGuiApplication::clipboard(), SIGNAL(findBufferChanged())); |
344 | |
345 | QGuiApplication::clipboard()->clear(mode: QClipboard::Clipboard); |
346 | QGuiApplication::clipboard()->clear(mode: QClipboard::Selection); // used to crash on X11 |
347 | QGuiApplication::clipboard()->clear(mode: QClipboard::FindBuffer); |
348 | |
349 | if (QGuiApplication::clipboard()->supportsSelection()) |
350 | QCOMPARE(spySelection.count(), 1); |
351 | else |
352 | QCOMPARE(spySelection.count(), 0); |
353 | |
354 | if (QGuiApplication::clipboard()->supportsFindBuffer()) |
355 | QCOMPARE(spyFindBuffer.count(), 1); |
356 | else |
357 | QCOMPARE(spyFindBuffer.count(), 0); |
358 | |
359 | QTRY_COMPARE(spyData.count(), 1); |
360 | |
361 | // an other crash test |
362 | data = new QMimeData; |
363 | data->setText("foo" ); |
364 | |
365 | QGuiApplication::clipboard()->setMimeData(data, mode: QClipboard::Clipboard); |
366 | if (QGuiApplication::clipboard()->supportsSelection()) |
367 | QGuiApplication::clipboard()->setMimeData(data, mode: QClipboard::Selection); |
368 | if (QGuiApplication::clipboard()->supportsFindBuffer()) |
369 | QGuiApplication::clipboard()->setMimeData(data, mode: QClipboard::FindBuffer); |
370 | |
371 | QMimeData *newData = new QMimeData; |
372 | newData->setText("bar" ); |
373 | |
374 | spySelection.clear(); |
375 | noLeftOverDataChanges.reset(); |
376 | QTRY_VERIFY(noLeftOverDataChanges); |
377 | spyFindBuffer.clear(); |
378 | |
379 | QGuiApplication::clipboard()->setMimeData(data: newData, mode: QClipboard::Clipboard); |
380 | if (QGuiApplication::clipboard()->supportsSelection()) |
381 | QGuiApplication::clipboard()->setMimeData(data: newData, mode: QClipboard::Selection); // used to crash on X11 |
382 | if (QGuiApplication::clipboard()->supportsFindBuffer()) |
383 | QGuiApplication::clipboard()->setMimeData(data: newData, mode: QClipboard::FindBuffer); |
384 | |
385 | if (QGuiApplication::clipboard()->supportsSelection()) |
386 | QCOMPARE(spySelection.count(), 1); |
387 | else |
388 | QCOMPARE(spySelection.count(), 0); |
389 | |
390 | if (QGuiApplication::clipboard()->supportsFindBuffer()) |
391 | QCOMPARE(spyFindBuffer.count(), 1); |
392 | else |
393 | QCOMPARE(spyFindBuffer.count(), 0); |
394 | |
395 | QTRY_COMPARE(spyData.count(), 1); |
396 | } |
397 | |
398 | void tst_QClipboard::clearBeforeSetText() |
399 | { |
400 | QGuiApplication::processEvents(); |
401 | |
402 | if (!PlatformClipboard::isAvailable()) |
403 | QSKIP("Native clipboard not working in this setup" ); |
404 | |
405 | const QString text = "tst_QClipboard::clearBeforeSetText()" ; |
406 | |
407 | // setText() should work after processEvents() |
408 | QGuiApplication::clipboard()->setText(text); |
409 | QCOMPARE(QGuiApplication::clipboard()->text(), text); |
410 | QGuiApplication::processEvents(); |
411 | QCOMPARE(QGuiApplication::clipboard()->text(), text); |
412 | |
413 | // same with clear() |
414 | QGuiApplication::clipboard()->clear(); |
415 | QVERIFY(QGuiApplication::clipboard()->text().isEmpty()); |
416 | QGuiApplication::processEvents(); |
417 | QVERIFY(QGuiApplication::clipboard()->text().isEmpty()); |
418 | |
419 | // setText() again |
420 | QGuiApplication::clipboard()->setText(text); |
421 | QCOMPARE(QGuiApplication::clipboard()->text(), text); |
422 | QGuiApplication::processEvents(); |
423 | QCOMPARE(QGuiApplication::clipboard()->text(), text); |
424 | |
425 | // clear() immediately followed by setText() should still return the text |
426 | QGuiApplication::clipboard()->clear(); |
427 | QVERIFY(QGuiApplication::clipboard()->text().isEmpty()); |
428 | QGuiApplication::clipboard()->setText(text); |
429 | QCOMPARE(QGuiApplication::clipboard()->text(), text); |
430 | QGuiApplication::processEvents(); |
431 | QCOMPARE(QGuiApplication::clipboard()->text(), text); |
432 | } |
433 | |
434 | #endif // QT_CONFIG(clipboard) |
435 | |
436 | QTEST_MAIN(tst_QClipboard) |
437 | |
438 | #include "tst_qclipboard.moc" |
439 | |