1 /*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8 #include <QDir>
9 #include <QMenu>
10 #include <QMimeData>
11 #include <QSignalSpy>
12 #include <QStandardPaths>
13 #include <QTemporaryDir>
14 #include <QTest>
15
16 #include "jobuidelegatefactory.h"
17 #include "kiotesthelper.h"
18 #include "mockcoredelegateextensions.h"
19 #include <KConfigGroup>
20 #include <KDesktopFile>
21 #include <KFileItemListProperties>
22 #include <KIO/CopyJob>
23 #include <KIO/DeleteJob>
24 #include <KIO/DropJob>
25 #include <KIO/StatJob>
26 #include <KJobUiDelegate>
27
28 Q_DECLARE_METATYPE(Qt::KeyboardModifiers)
Q_DECLARE_METATYPE(Qt::DropAction)29 Q_DECLARE_METATYPE(Qt::DropAction)
30 Q_DECLARE_METATYPE(Qt::DropActions)
31 Q_DECLARE_METATYPE(KFileItemListProperties)
32
33 #ifndef Q_OS_WIN
34 void initLocale()
35 {
36 setenv("LC_ALL", "en_US.utf-8", 1);
37 }
38 Q_CONSTRUCTOR_FUNCTION(initLocale)
39 #endif
40
41 class JobSpy : public QObject
42 {
43 Q_OBJECT
44 public:
JobSpy(KIO::Job * job)45 JobSpy(KIO::Job *job)
46 : QObject(nullptr)
47 , m_spy(job, &KJob::result)
48 , m_error(0)
49 {
50 connect(job, &KJob::result, this, [this](KJob *job) {
51 m_error = job->error();
52 });
53 }
54 // like job->exec(), but with a timeout (to avoid being stuck with a popup grabbing mouse and keyboard...)
waitForResult()55 bool waitForResult()
56 {
57 // implementation taken from QTRY_COMPARE, to move the QVERIFY to the caller
58 if (m_spy.isEmpty()) {
59 QTest::qWait(0);
60 }
61 for (int i = 0; i < 5000 && m_spy.isEmpty(); i += 50) {
62 QTest::qWait(50);
63 }
64 return !m_spy.isEmpty();
65 }
error() const66 int error() const
67 {
68 return m_error;
69 }
70
71 private:
72 QSignalSpy m_spy;
73 int m_error;
74 };
75
76 class DropJobTest : public QObject
77 {
78 Q_OBJECT
79
80 private Q_SLOTS:
initTestCase()81 void initTestCase()
82 {
83 QStandardPaths::setTestModeEnabled(true);
84 qputenv("KIOSLAVE_ENABLE_TESTMODE", "1"); // ensure the ioslaves call QStandardPaths::setTestModeEnabled too
85
86 // To avoid a runtime dependency on klauncher
87 qputenv("KDE_FORK_SLAVES", "yes");
88
89 KIO::setDefaultJobUiDelegateFactory(nullptr);
90 KIO::setDefaultJobUiDelegateExtension(nullptr);
91
92 const QString trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/Trash");
93 QDir(trashDir).removeRecursively();
94
95 QVERIFY(m_tempDir.isValid());
96 QVERIFY(m_nonWritableTempDir.isValid());
97 QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ExeOwner | QFile::ExeUser));
98 m_srcDir = m_tempDir.path();
99
100 m_srcFile = m_srcDir + "/srcfile";
101 m_srcLink = m_srcDir + "/link";
102
103 qRegisterMetaType<KIO::CopyJob *>();
104 }
105
cleanupTestCase()106 void cleanupTestCase()
107 {
108 QVERIFY(QFile(m_nonWritableTempDir.path())
109 .setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::WriteOwner | QFile::WriteUser | QFile::ExeOwner | QFile::ExeUser));
110 }
111
112 // Before every test method, ensure the test file m_srcFile exists
init()113 void init()
114 {
115 if (QFile::exists(m_srcFile)) {
116 QVERIFY(QFileInfo(m_srcFile).isWritable());
117 } else {
118 QFile srcFile(m_srcFile);
119 QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
120 srcFile.write("Hello world\n");
121 }
122 #ifndef Q_OS_WIN
123 if (!QFile::exists(m_srcLink)) {
124 QVERIFY(QFile(m_srcFile).link(m_srcLink));
125 QVERIFY(QFileInfo(m_srcLink).isSymLink());
126 }
127 #endif
128 QVERIFY(QFileInfo(m_srcFile).isWritable());
129 m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(m_srcFile)});
130 }
131
shouldDropToDesktopFile()132 void shouldDropToDesktopFile()
133 {
134 // Given an executable application desktop file and a source file
135 const QString desktopPath = m_srcDir + "/target.desktop";
136 KDesktopFile desktopFile(desktopPath);
137 KConfigGroup desktopGroup = desktopFile.desktopGroup();
138 desktopGroup.writeEntry("Type", "Application");
139 desktopGroup.writeEntry("StartupNotify", "false");
140 #ifdef Q_OS_WIN
141 desktopGroup.writeEntry("Exec", "copy.exe %f %d/dest");
142 #else
143 desktopGroup.writeEntry("Exec", "cp %f %d/dest");
144 #endif
145 desktopFile.sync();
146 QFile file(desktopPath);
147 file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser);
148
149 // When dropping the source file onto the desktop file
150 QUrl destUrl = QUrl::fromLocalFile(desktopPath);
151 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
152 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
153 QSignalSpy spy(job, &KIO::DropJob::itemCreated);
154
155 // Then the application is run with the source file as argument
156 // (in this example, it copies the source file to "dest")
157 QVERIFY2(job->exec(), qPrintable(job->errorString()));
158 QCOMPARE(spy.count(), 0);
159 const QString dest = m_srcDir + "/dest";
160 QTRY_VERIFY(QFile::exists(dest));
161
162 QVERIFY(QFile::remove(desktopPath));
163 QVERIFY(QFile::remove(dest));
164 }
165
shouldDropToDirectory_data()166 void shouldDropToDirectory_data()
167 {
168 QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
169 QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
170 QTest::addColumn<QString>("srcFile");
171 QTest::addColumn<QString>("dest"); // empty for a temp dir
172 QTest::addColumn<int>("expectedError");
173 QTest::addColumn<bool>("shouldSourceStillExist");
174
175 QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile << QString() << 0 << true;
176 QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile << QString() << 0 << false;
177 QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile << QString() << 0 << true;
178 QTest::newRow("DropOnItself") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcDir << m_srcDir << int(KIO::ERR_DROP_ON_ITSELF) << true;
179 QTest::newRow("DropDirOnFile") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcDir << m_srcFile << int(KIO::ERR_ACCESS_DENIED)
180 << true;
181 QTest::newRow("NonWritableDest") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile << m_nonWritableTempDir.path()
182 << int(KIO::ERR_WRITE_ACCESS_DENIED) << true;
183 }
184
shouldDropToDirectory()185 void shouldDropToDirectory()
186 {
187 QFETCH(Qt::KeyboardModifiers, modifiers);
188 QFETCH(Qt::DropAction, dropAction);
189 QFETCH(QString, srcFile);
190 QFETCH(QString, dest);
191 QFETCH(int, expectedError);
192 QFETCH(bool, shouldSourceStillExist);
193
194 // Given a directory and a source file
195 QTemporaryDir tempDestDir;
196 QVERIFY(tempDestDir.isValid());
197 if (dest.isEmpty()) {
198 dest = tempDestDir.path();
199 }
200
201 // When dropping the source file onto the directory
202 const QUrl destUrl = QUrl::fromLocalFile(dest);
203 m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
204 QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
205 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo | KIO::NoPrivilegeExecution);
206 JobSpy jobSpy(job);
207 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
208 QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
209
210 // Then the file is copied
211 QVERIFY(jobSpy.waitForResult());
212 QCOMPARE(jobSpy.error(), expectedError);
213 if (expectedError == 0) {
214 QCOMPARE(copyJobSpy.count(), 1);
215 const QString destFile = dest + "/srcfile";
216 QCOMPARE(itemCreatedSpy.count(), 1);
217 QCOMPARE(itemCreatedSpy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(destFile));
218 QVERIFY(QFile::exists(destFile));
219 QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
220 if (dropAction == Qt::LinkAction) {
221 QVERIFY(QFileInfo(destFile).isSymLink());
222 }
223 }
224 }
225
shouldDropToTrash_data()226 void shouldDropToTrash_data()
227 {
228 QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
229 QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
230 QTest::addColumn<QString>("srcFile");
231
232 QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile;
233 QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile;
234 QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile;
235 QTest::newRow("NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile;
236 #ifndef Q_OS_WIN
237 QTest::newRow("Link_Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcLink;
238 QTest::newRow("Link_Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcLink;
239 QTest::newRow("Link_Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcLink;
240 QTest::newRow("Link_NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcLink;
241 #endif
242 }
243
shouldDropToTrash()244 void shouldDropToTrash()
245 {
246 // Given a source file
247 QFETCH(Qt::KeyboardModifiers, modifiers);
248 QFETCH(Qt::DropAction, dropAction);
249 QFETCH(QString, srcFile);
250 const bool isLink = QFileInfo(srcFile).isSymLink();
251
252 // When dropping it into the trash, with <modifiers> pressed
253 m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
254 QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
255 KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
256 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
257 QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
258
259 // Then a confirmation dialog should appear
260 auto *uiDelegate = new KJobUiDelegate;
261 job->setUiDelegate(uiDelegate);
262 auto *askUserHandler = new MockAskUserInterface(uiDelegate);
263 askUserHandler->m_deleteResult = true;
264
265 // And the file should be moved to the trash, no matter what the modifiers are
266 QVERIFY2(job->exec(), qPrintable(job->errorString()));
267 QCOMPARE(askUserHandler->m_askUserDeleteCalled, 1);
268 QCOMPARE(copyJobSpy.count(), 1);
269 QCOMPARE(itemCreatedSpy.count(), 1);
270 const QUrl trashUrl = itemCreatedSpy.at(0).at(0).value<QUrl>();
271 QCOMPARE(trashUrl.scheme(), QString("trash"));
272 KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
273 QVERIFY(statJob->exec());
274 if (isLink) {
275 QVERIFY(statJob->statResult().isLink());
276 }
277
278 // clean up
279 KIO::DeleteJob *delJob = KIO::del(trashUrl, KIO::HideProgressInfo);
280 QVERIFY2(delJob->exec(), qPrintable(delJob->errorString()));
281 }
282
shouldDropFromTrash()283 void shouldDropFromTrash()
284 {
285 // Given a file in the trash
286 const QFile::Permissions origPerms = QFileInfo(m_srcFile).permissions();
287 QVERIFY(QFileInfo(m_srcFile).isWritable());
288 KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
289 QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
290 QVERIFY(copyJob->exec());
291 const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
292 QVERIFY(trashUrl.isValid());
293 QVERIFY(!QFile::exists(m_srcFile));
294
295 // When dropping the trashed file into a local dir, without modifiers
296 m_mimeData.setUrls(QList<QUrl>{trashUrl});
297 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
298 KIO::DropJob *job = KIO::drop(&dropEvent, QUrl::fromLocalFile(m_srcDir), KIO::HideProgressInfo);
299 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
300 QSignalSpy spy(job, &KIO::DropJob::itemCreated);
301
302 // Then the file should be moved, without a popup. No point in copying out of the trash, or linking to it.
303 QVERIFY2(job->exec(), qPrintable(job->errorString()));
304 QCOMPARE(copyJobSpy.count(), 1);
305 QCOMPARE(spy.count(), 1);
306 QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(m_srcFile));
307 QVERIFY(QFile::exists(m_srcFile));
308 QCOMPARE(int(QFileInfo(m_srcFile).permissions()), int(origPerms));
309 QVERIFY(QFileInfo(m_srcFile).isWritable());
310 KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
311 QVERIFY(!statJob->exec());
312 QVERIFY(QFileInfo(m_srcFile).isWritable());
313 }
314
shouldDropTrashRootWithoutMovingAllTrashedFiles()315 void shouldDropTrashRootWithoutMovingAllTrashedFiles() // #319660
316 {
317 // Given some stuff in the trash
318 const QUrl trashUrl(QStringLiteral("trash:/"));
319 KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), trashUrl);
320 QVERIFY(copyJob->exec());
321 // and an empty destination directory
322 QTemporaryDir tempDestDir;
323 QVERIFY(tempDestDir.isValid());
324 const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
325
326 // When dropping a link / icon of the trash...
327 m_mimeData.setUrls(QList<QUrl>{trashUrl});
328 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
329 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
330 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
331 QVERIFY2(job->exec(), qPrintable(job->errorString()));
332
333 // Then a full move shouldn't happen, just a link
334 QCOMPARE(copyJobSpy.count(), 1);
335 const QStringList items = QDir(tempDestDir.path()).entryList();
336 QVERIFY2(!items.contains("srcfile"), qPrintable(items.join(',')));
337 QVERIFY2(items.contains("trash:" + QChar(0x2044) + ".desktop"), qPrintable(items.join(',')));
338 }
339
shouldDropFromTrashToTrash()340 void shouldDropFromTrashToTrash() // #378051
341 {
342 // Given a file in the trash
343 QVERIFY(QFileInfo(m_srcFile).isWritable());
344 KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
345 QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
346 QVERIFY(copyJob->exec());
347 const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
348 QVERIFY(trashUrl.isValid());
349 QVERIFY(!QFile::exists(m_srcFile));
350
351 // When dropping the trashed file in the trash
352 m_mimeData.setUrls(QList<QUrl>{trashUrl});
353 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
354 KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
355 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
356 QSignalSpy spy(job, &KIO::DropJob::itemCreated);
357
358 // Then an error should be reported and no files action should occur
359 QVERIFY(!job->exec());
360 QCOMPARE(job->error(), KIO::ERR_DROP_ON_ITSELF);
361 }
362
shouldDropToDirectoryWithPopup_data()363 void shouldDropToDirectoryWithPopup_data()
364 {
365 QTest::addColumn<QString>("dest"); // empty for a temp dir
366 QTest::addColumn<Qt::DropActions>("offeredActions");
367 QTest::addColumn<int>("triggerActionNumber");
368 QTest::addColumn<int>("expectedError");
369 QTest::addColumn<Qt::DropAction>("expectedDropAction");
370 QTest::addColumn<bool>("shouldSourceStillExist");
371
372 const Qt::DropActions threeActions = Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
373 const Qt::DropActions copyAndLink = Qt::CopyAction | Qt::LinkAction;
374 QTest::newRow("Move") << QString() << threeActions << 0 << 0 << Qt::MoveAction << false;
375 QTest::newRow("Copy") << QString() << threeActions << 1 << 0 << Qt::CopyAction << true;
376 QTest::newRow("Link") << QString() << threeActions << 2 << 0 << Qt::LinkAction << true;
377 QTest::newRow("SameDestCopy") << m_srcDir << copyAndLink << 0 << int(KIO::ERR_IDENTICAL_FILES) << Qt::CopyAction << true;
378 QTest::newRow("SameDestLink") << m_srcDir << copyAndLink << 1 << int(KIO::ERR_FILE_ALREADY_EXIST) << Qt::LinkAction << true;
379 }
380
shouldDropToDirectoryWithPopup()381 void shouldDropToDirectoryWithPopup()
382 {
383 QFETCH(QString, dest);
384 QFETCH(Qt::DropActions, offeredActions);
385 QFETCH(int, triggerActionNumber);
386 QFETCH(int, expectedError);
387 QFETCH(Qt::DropAction, expectedDropAction);
388 QFETCH(bool, shouldSourceStillExist);
389
390 // Given a directory and a source file
391 QTemporaryDir tempDestDir;
392 QVERIFY(tempDestDir.isValid());
393 if (dest.isEmpty()) {
394 dest = tempDestDir.path();
395 }
396 QVERIFY(!findPopup());
397
398 // When dropping the source file onto the directory
399 QUrl destUrl = QUrl::fromLocalFile(dest);
400 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
401 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
402 JobSpy jobSpy(job);
403 qRegisterMetaType<KFileItemListProperties>();
404 QSignalSpy spyShow(job, &KIO::DropJob::popupMenuAboutToShow);
405 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
406 QVERIFY(spyShow.isValid());
407
408 // Then a popup should appear, with the expected available actions
409 QVERIFY(spyShow.wait());
410 QTRY_VERIFY(findPopup());
411 QMenu *popup = findPopup();
412 QCOMPARE(int(popupDropActions(popup)), int(offeredActions));
413
414 // And when selecting action number <triggerActionNumber>
415 QAction *action = popup->actions().at(triggerActionNumber);
416 QVERIFY(action);
417 QCOMPARE(int(action->data().value<Qt::DropAction>()), int(expectedDropAction));
418 const QRect actionGeom = popup->actionGeometry(action);
419 QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
420
421 // Then the job should finish, and the chosen action should happen.
422 QVERIFY(jobSpy.waitForResult());
423 QCOMPARE(jobSpy.error(), expectedError);
424 if (expectedError == 0) {
425 QCOMPARE(copyJobSpy.count(), 1);
426 const QString destFile = dest + "/srcfile";
427 QVERIFY(QFile::exists(destFile));
428 QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
429 if (expectedDropAction == Qt::LinkAction) {
430 QVERIFY(QFileInfo(destFile).isSymLink());
431 }
432 }
433 QTRY_VERIFY(!findPopup()); // flush deferred delete, so we don't get this popup again in findPopup
434 }
435
shouldAddApplicationActionsToPopup()436 void shouldAddApplicationActionsToPopup()
437 {
438 // Given a directory and a source file
439 QTemporaryDir tempDestDir;
440 QVERIFY(tempDestDir.isValid());
441 const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
442
443 // When dropping the source file onto the directory
444 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
445 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
446 QAction appAction1(QStringLiteral("action1"), this);
447 QAction appAction2(QStringLiteral("action2"), this);
448 QList<QAction *> appActions;
449 appActions << &appAction1 << &appAction2;
450 job->setApplicationActions(appActions);
451 JobSpy jobSpy(job);
452
453 // Then a popup should appear, with the expected available actions
454 QTRY_VERIFY(findPopup());
455 QMenu *popup = findPopup();
456 const QList<QAction *> actions = popup->actions();
457 QVERIFY(actions.contains(&appAction1));
458 QVERIFY(actions.contains(&appAction2));
459 QVERIFY(actions.at(actions.indexOf(&appAction1) - 1)->isSeparator());
460 QVERIFY(actions.at(actions.indexOf(&appAction2) + 1)->isSeparator());
461
462 // And when selecting action appAction1
463 const QRect actionGeom = popup->actionGeometry(&appAction1);
464 QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
465
466 // Then the menu should hide and the job terminate (without doing any copying)
467 QVERIFY(jobSpy.waitForResult());
468 QCOMPARE(jobSpy.error(), 0);
469 const QString destFile = tempDestDir.path() + "/srcfile";
470 QVERIFY(!QFile::exists(destFile));
471 }
472
473 private:
findPopup()474 static QMenu *findPopup()
475 {
476 const QList<QWidget *> widgetsList = qApp->topLevelWidgets();
477 for (QWidget *widget : widgetsList) {
478 if (QMenu *menu = qobject_cast<QMenu *>(widget)) {
479 return menu;
480 }
481 }
482 return nullptr;
483 }
popupDropActions(QMenu * menu)484 static Qt::DropActions popupDropActions(QMenu *menu)
485 {
486 Qt::DropActions actions;
487 const QList<QAction *> actionsList = menu->actions();
488 for (const QAction *action : actionsList) {
489 const QVariant userData = action->data();
490 if (userData.isValid()) {
491 actions |= userData.value<Qt::DropAction>();
492 }
493 }
494 return actions;
495 }
496 QMimeData m_mimeData; // contains m_srcFile
497 QTemporaryDir m_tempDir;
498 QString m_srcDir;
499 QString m_srcFile;
500 QString m_srcLink;
501 QTemporaryDir m_nonWritableTempDir;
502 };
503
504 QTEST_MAIN(DropJobTest)
505
506 #include "dropjobtest.moc"
507