File indexing completed on 2025-01-12 12:29:23
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 */ 0007 0008 #include <QDir> 0009 #include <QMenu> 0010 #include <QMimeData> 0011 #include <QSignalSpy> 0012 #include <QStandardPaths> 0013 #include <QTemporaryDir> 0014 #include <QTest> 0015 0016 #include "jobuidelegatefactory.h" 0017 #include "kiotesthelper.h" 0018 #include "mockcoredelegateextensions.h" 0019 #include <KConfigGroup> 0020 #include <KDesktopFile> 0021 #include <KFileItemListProperties> 0022 #include <KIO/CopyJob> 0023 #include <KIO/DeleteJob> 0024 #include <KIO/DropJob> 0025 #include <KIO/StatJob> 0026 #include <KJobUiDelegate> 0027 0028 Q_DECLARE_METATYPE(Qt::KeyboardModifiers) 0029 Q_DECLARE_METATYPE(Qt::DropAction) 0030 Q_DECLARE_METATYPE(Qt::DropActions) 0031 Q_DECLARE_METATYPE(KFileItemListProperties) 0032 0033 #ifndef Q_OS_WIN 0034 void initLocale() 0035 { 0036 setenv("LC_ALL", "en_US.utf-8", 1); 0037 } 0038 Q_CONSTRUCTOR_FUNCTION(initLocale) 0039 #endif 0040 0041 class JobSpy : public QObject 0042 { 0043 Q_OBJECT 0044 public: 0045 explicit JobSpy(KIO::Job *job) 0046 : QObject(nullptr) 0047 , m_spy(job, &KJob::result) 0048 , m_error(0) 0049 { 0050 connect(job, &KJob::result, this, [this](KJob *job) { 0051 m_error = job->error(); 0052 }); 0053 } 0054 // like job->exec(), but with a timeout (to avoid being stuck with a popup grabbing mouse and keyboard...) 0055 bool waitForResult() 0056 { 0057 // implementation taken from QTRY_COMPARE, to move the QVERIFY to the caller 0058 if (m_spy.isEmpty()) { 0059 QTest::qWait(0); 0060 } 0061 for (int i = 0; i < 5000 && m_spy.isEmpty(); i += 50) { 0062 QTest::qWait(50); 0063 } 0064 return !m_spy.isEmpty(); 0065 } 0066 int error() const 0067 { 0068 return m_error; 0069 } 0070 0071 private: 0072 QSignalSpy m_spy; 0073 int m_error; 0074 }; 0075 0076 class DropJobTest : public QObject 0077 { 0078 Q_OBJECT 0079 0080 private Q_SLOTS: 0081 void initTestCase() 0082 { 0083 QStandardPaths::setTestModeEnabled(true); 0084 qputenv("KIOSLAVE_ENABLE_TESTMODE", "1"); // ensure the KIO workers call QStandardPaths::setTestModeEnabled too 0085 0086 KIO::setDefaultJobUiDelegateFactoryV2(nullptr); 0087 KIO::setDefaultJobUiDelegateExtension(nullptr); 0088 0089 m_trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/Trash"); 0090 QDir(m_trashDir).removeRecursively(); 0091 0092 QVERIFY(m_tempDir.isValid()); 0093 QVERIFY(m_nonWritableTempDir.isValid()); 0094 QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ExeOwner | QFile::ExeUser)); 0095 m_srcDir = m_tempDir.path(); 0096 0097 m_srcFile = m_srcDir + "/srcfile"; 0098 m_srcLink = m_srcDir + "/link"; 0099 0100 qRegisterMetaType<KIO::CopyJob *>(); 0101 } 0102 0103 void cleanupTestCase() 0104 { 0105 QVERIFY(QFile(m_nonWritableTempDir.path()) 0106 .setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::WriteOwner | QFile::WriteUser | QFile::ExeOwner | QFile::ExeUser)); 0107 } 0108 0109 // Before every test method, ensure the test file m_srcFile exists 0110 void init() 0111 { 0112 if (QFile::exists(m_srcFile)) { 0113 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0114 } else { 0115 QFile srcFile(m_srcFile); 0116 QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); 0117 srcFile.write("Hello world\n"); 0118 } 0119 #ifndef Q_OS_WIN 0120 if (!QFile::exists(m_srcLink)) { 0121 QVERIFY(QFile(m_srcFile).link(m_srcLink)); 0122 QVERIFY(QFileInfo(m_srcLink).isSymLink()); 0123 } 0124 #endif 0125 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0126 m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(m_srcFile)}); 0127 } 0128 0129 void shouldDropToDesktopFile() 0130 { 0131 // Given an executable application desktop file and a source file 0132 const QString desktopPath = m_srcDir + "/target.desktop"; 0133 KDesktopFile desktopFile(desktopPath); 0134 KConfigGroup desktopGroup = desktopFile.desktopGroup(); 0135 desktopGroup.writeEntry("Type", "Application"); 0136 desktopGroup.writeEntry("StartupNotify", "false"); 0137 #ifdef Q_OS_WIN 0138 desktopGroup.writeEntry("Exec", "copy.exe %f %d/dest"); 0139 #else 0140 desktopGroup.writeEntry("Exec", "cp %f %d/dest"); 0141 #endif 0142 desktopFile.sync(); 0143 QFile file(desktopPath); 0144 file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser); 0145 0146 // When dropping the source file onto the desktop file 0147 QUrl destUrl = QUrl::fromLocalFile(desktopPath); 0148 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier); 0149 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo); 0150 QSignalSpy spy(job, &KIO::DropJob::itemCreated); 0151 0152 // Then the application is run with the source file as argument 0153 // (in this example, it copies the source file to "dest") 0154 QVERIFY2(job->exec(), qPrintable(job->errorString())); 0155 QCOMPARE(spy.count(), 0); 0156 const QString dest = m_srcDir + "/dest"; 0157 QTRY_VERIFY(QFile::exists(dest)); 0158 0159 QVERIFY(QFile::remove(desktopPath)); 0160 QVERIFY(QFile::remove(dest)); 0161 } 0162 0163 void shouldDropToDirectory_data() 0164 { 0165 QTest::addColumn<Qt::KeyboardModifiers>("modifiers"); 0166 QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here 0167 QTest::addColumn<QString>("srcFile"); 0168 QTest::addColumn<QString>("dest"); // empty for a temp dir 0169 QTest::addColumn<int>("expectedError"); 0170 QTest::addColumn<bool>("shouldSourceStillExist"); 0171 0172 QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile << QString() << 0 << true; 0173 QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile << QString() << 0 << false; 0174 QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile << QString() << 0 << true; 0175 QTest::newRow("DropOnItself") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcDir << m_srcDir << int(KIO::ERR_DROP_ON_ITSELF) << true; 0176 QTest::newRow("DropDirOnFile") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcDir << m_srcFile << int(KIO::ERR_ACCESS_DENIED) 0177 << true; 0178 QTest::newRow("NonWritableDest") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile << m_nonWritableTempDir.path() 0179 << int(KIO::ERR_WRITE_ACCESS_DENIED) << true; 0180 } 0181 0182 void shouldDropToDirectory() 0183 { 0184 QFETCH(Qt::KeyboardModifiers, modifiers); 0185 QFETCH(Qt::DropAction, dropAction); 0186 QFETCH(QString, srcFile); 0187 QFETCH(QString, dest); 0188 QFETCH(int, expectedError); 0189 QFETCH(bool, shouldSourceStillExist); 0190 0191 // Given a directory and a source file 0192 QTemporaryDir tempDestDir; 0193 QVERIFY(tempDestDir.isValid()); 0194 if (dest.isEmpty()) { 0195 dest = tempDestDir.path(); 0196 } 0197 0198 // When dropping the source file onto the directory 0199 const QUrl destUrl = QUrl::fromLocalFile(dest); 0200 m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)}); 0201 QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers); 0202 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo | KIO::NoPrivilegeExecution); 0203 JobSpy jobSpy(job); 0204 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted); 0205 QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated); 0206 0207 // Then the file is copied 0208 QVERIFY(jobSpy.waitForResult()); 0209 QCOMPARE(jobSpy.error(), expectedError); 0210 if (expectedError == 0) { 0211 QCOMPARE(copyJobSpy.count(), 1); 0212 const QString destFile = dest + "/srcfile"; 0213 QCOMPARE(itemCreatedSpy.count(), 1); 0214 QCOMPARE(itemCreatedSpy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(destFile)); 0215 QVERIFY(QFile::exists(destFile)); 0216 QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist); 0217 if (dropAction == Qt::LinkAction) { 0218 QVERIFY(QFileInfo(destFile).isSymLink()); 0219 } 0220 } 0221 } 0222 0223 void shouldDropToTrash_data() 0224 { 0225 QTest::addColumn<Qt::KeyboardModifiers>("modifiers"); 0226 QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here 0227 QTest::addColumn<QString>("srcFile"); 0228 0229 QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile; 0230 QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile; 0231 QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile; 0232 QTest::newRow("NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile; 0233 #ifndef Q_OS_WIN 0234 QTest::newRow("Link_Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcLink; 0235 QTest::newRow("Link_Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcLink; 0236 QTest::newRow("Link_Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcLink; 0237 QTest::newRow("Link_NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcLink; 0238 #endif 0239 } 0240 0241 void shouldDropToTrash() 0242 { 0243 // Given a source file 0244 QFETCH(Qt::KeyboardModifiers, modifiers); 0245 QFETCH(Qt::DropAction, dropAction); 0246 QFETCH(QString, srcFile); 0247 const bool isLink = QFileInfo(srcFile).isSymLink(); 0248 0249 // When dropping it into the trash, with <modifiers> pressed 0250 m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)}); 0251 QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers); 0252 KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); 0253 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted); 0254 QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated); 0255 0256 // Then a confirmation dialog should appear 0257 auto *uiDelegate = new KJobUiDelegate; 0258 job->setUiDelegate(uiDelegate); 0259 auto *askUserHandler = new MockAskUserInterface(uiDelegate); 0260 askUserHandler->m_deleteResult = true; 0261 0262 // And the file should be moved to the trash, no matter what the modifiers are 0263 QVERIFY2(job->exec(), qPrintable(job->errorString())); 0264 QCOMPARE(askUserHandler->m_askUserDeleteCalled, 1); 0265 QCOMPARE(copyJobSpy.count(), 1); 0266 QCOMPARE(itemCreatedSpy.count(), 1); 0267 const QUrl trashUrl = itemCreatedSpy.at(0).at(0).value<QUrl>(); 0268 QCOMPARE(trashUrl.scheme(), QString("trash")); 0269 KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo); 0270 QVERIFY(statJob->exec()); 0271 if (isLink) { 0272 QVERIFY(statJob->statResult().isLink()); 0273 } 0274 0275 // clean up 0276 KIO::DeleteJob *delJob = KIO::del(trashUrl, KIO::HideProgressInfo); 0277 QVERIFY2(delJob->exec(), qPrintable(delJob->errorString())); 0278 } 0279 0280 void shouldDropFromTrash() 0281 { 0282 // Given a file in the trash 0283 const QFileInfo srcInfo(m_srcFile); 0284 const QFile::Permissions origPerms = srcInfo.permissions(); 0285 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0286 KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/"))); 0287 0288 QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone); 0289 QVERIFY(copyJob->exec()); 0290 const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>(); 0291 QVERIFY(trashUrl.isValid()); 0292 QVERIFY(!QFile::exists(m_srcFile)); 0293 0294 // trashinfo file was created 0295 const QString infoFile(m_trashDir + QStringLiteral("/info/") + srcInfo.fileName() + QStringLiteral(".trashinfo")); 0296 QVERIFY(QFileInfo::exists(infoFile)); 0297 0298 // When dropping the trashed file into a local dir, without modifiers 0299 m_mimeData.setUrls(QList<QUrl>{trashUrl}); 0300 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier); 0301 KIO::DropJob *job = KIO::drop(&dropEvent, QUrl::fromLocalFile(m_srcDir), KIO::HideProgressInfo); 0302 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted); 0303 QSignalSpy spy(job, &KIO::DropJob::itemCreated); 0304 0305 // Then the file should be moved, without a popup. No point in copying out of the trash, or linking to it. 0306 QVERIFY2(job->exec(), qPrintable(job->errorString())); 0307 QCOMPARE(copyJobSpy.count(), 1); 0308 QCOMPARE(spy.count(), 1); 0309 QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(m_srcFile)); 0310 QVERIFY(QFile::exists(m_srcFile)); 0311 QCOMPARE(int(QFileInfo(m_srcFile).permissions()), int(origPerms)); 0312 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0313 KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo); 0314 QVERIFY(!statJob->exec()); 0315 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0316 0317 // trashinfo file was removed 0318 QVERIFY(!QFileInfo::exists(infoFile)); 0319 0320 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0321 } 0322 0323 void shouldDropTrashRootWithoutMovingAllTrashedFiles() // #319660 0324 { 0325 // Given some stuff in the trash 0326 const QUrl trashUrl(QStringLiteral("trash:/")); 0327 KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), trashUrl); 0328 QVERIFY(copyJob->exec()); 0329 // and an empty destination directory 0330 QTemporaryDir tempDestDir; 0331 QVERIFY(tempDestDir.isValid()); 0332 const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path()); 0333 0334 // When dropping a link / icon of the trash... 0335 m_mimeData.setUrls(QList<QUrl>{trashUrl}); 0336 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier); 0337 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo); 0338 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted); 0339 QVERIFY2(job->exec(), qPrintable(job->errorString())); 0340 0341 // Then a full move shouldn't happen, just a link 0342 QCOMPARE(copyJobSpy.count(), 1); 0343 const QStringList items = QDir(tempDestDir.path()).entryList(); 0344 QVERIFY2(!items.contains("srcfile"), qPrintable(items.join(','))); 0345 QVERIFY2(items.contains("trash:" + QChar(0x2044) + ".desktop"), qPrintable(items.join(','))); 0346 } 0347 0348 void shouldDropFromTrashToTrash() // #378051 0349 { 0350 // Given a file in the trash 0351 QVERIFY(QFileInfo(m_srcFile).isWritable()); 0352 KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/"))); 0353 QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone); 0354 QVERIFY(copyJob->exec()); 0355 const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>(); 0356 QVERIFY(trashUrl.isValid()); 0357 QVERIFY(!QFile::exists(m_srcFile)); 0358 0359 // When dropping the trashed file in the trash 0360 m_mimeData.setUrls(QList<QUrl>{trashUrl}); 0361 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier); 0362 KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); 0363 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted); 0364 QSignalSpy spy(job, &KIO::DropJob::itemCreated); 0365 0366 // Then an error should be reported and no files action should occur 0367 QVERIFY(!job->exec()); 0368 QCOMPARE(job->error(), KIO::ERR_DROP_ON_ITSELF); 0369 } 0370 0371 void shouldDropToDirectoryWithPopup_data() 0372 { 0373 QTest::addColumn<QString>("dest"); // empty for a temp dir 0374 QTest::addColumn<Qt::DropActions>("offeredActions"); 0375 QTest::addColumn<int>("triggerActionNumber"); 0376 QTest::addColumn<int>("expectedError"); 0377 QTest::addColumn<Qt::DropAction>("expectedDropAction"); 0378 QTest::addColumn<bool>("shouldSourceStillExist"); 0379 0380 const Qt::DropActions threeActions = Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; 0381 const Qt::DropActions copyAndLink = Qt::CopyAction | Qt::LinkAction; 0382 QTest::newRow("Move") << QString() << threeActions << 0 << 0 << Qt::MoveAction << false; 0383 QTest::newRow("Copy") << QString() << threeActions << 1 << 0 << Qt::CopyAction << true; 0384 QTest::newRow("Link") << QString() << threeActions << 2 << 0 << Qt::LinkAction << true; 0385 QTest::newRow("SameDestCopy") << m_srcDir << copyAndLink << 0 << int(KIO::ERR_IDENTICAL_FILES) << Qt::CopyAction << true; 0386 QTest::newRow("SameDestLink") << m_srcDir << copyAndLink << 1 << int(KIO::ERR_FILE_ALREADY_EXIST) << Qt::LinkAction << true; 0387 } 0388 0389 void shouldDropToDirectoryWithPopup() 0390 { 0391 QFETCH(QString, dest); 0392 QFETCH(Qt::DropActions, offeredActions); 0393 QFETCH(int, triggerActionNumber); 0394 QFETCH(int, expectedError); 0395 QFETCH(Qt::DropAction, expectedDropAction); 0396 QFETCH(bool, shouldSourceStillExist); 0397 0398 // Given a directory and a source file 0399 QTemporaryDir tempDestDir; 0400 QVERIFY(tempDestDir.isValid()); 0401 if (dest.isEmpty()) { 0402 dest = tempDestDir.path(); 0403 } 0404 QVERIFY(!findPopup()); 0405 0406 // When dropping the source file onto the directory 0407 QUrl destUrl = QUrl::fromLocalFile(dest); 0408 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier); 0409 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo); 0410 JobSpy jobSpy(job); 0411 qRegisterMetaType<KFileItemListProperties>(); 0412 QSignalSpy spyShow(job, &KIO::DropJob::popupMenuAboutToShow); 0413 QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted); 0414 QVERIFY(spyShow.isValid()); 0415 0416 // Then a popup should appear, with the expected available actions 0417 QVERIFY(spyShow.wait()); 0418 QTRY_VERIFY(findPopup()); 0419 QMenu *popup = findPopup(); 0420 QCOMPARE(int(popupDropActions(popup)), int(offeredActions)); 0421 0422 // And when selecting action number <triggerActionNumber> 0423 QAction *action = popup->actions().at(triggerActionNumber); 0424 QVERIFY(action); 0425 QCOMPARE(int(action->data().value<Qt::DropAction>()), int(expectedDropAction)); 0426 const QRect actionGeom = popup->actionGeometry(action); 0427 QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center()); 0428 0429 // Then the job should finish, and the chosen action should happen. 0430 QVERIFY(jobSpy.waitForResult()); 0431 QCOMPARE(jobSpy.error(), expectedError); 0432 if (expectedError == 0) { 0433 QCOMPARE(copyJobSpy.count(), 1); 0434 const QString destFile = dest + "/srcfile"; 0435 QVERIFY(QFile::exists(destFile)); 0436 QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist); 0437 if (expectedDropAction == Qt::LinkAction) { 0438 QVERIFY(QFileInfo(destFile).isSymLink()); 0439 } 0440 } 0441 QTRY_VERIFY(!findPopup()); // flush deferred delete, so we don't get this popup again in findPopup 0442 } 0443 0444 void shouldAddApplicationActionsToPopup() 0445 { 0446 // Given a directory and a source file 0447 QTemporaryDir tempDestDir; 0448 QVERIFY(tempDestDir.isValid()); 0449 const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path()); 0450 0451 // When dropping the source file onto the directory 0452 QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier); 0453 KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo); 0454 QAction appAction1(QStringLiteral("action1"), this); 0455 QAction appAction2(QStringLiteral("action2"), this); 0456 QList<QAction *> appActions; 0457 appActions << &appAction1 << &appAction2; 0458 job->setApplicationActions(appActions); 0459 JobSpy jobSpy(job); 0460 0461 // Then a popup should appear, with the expected available actions 0462 QTRY_VERIFY(findPopup()); 0463 QMenu *popup = findPopup(); 0464 const QList<QAction *> actions = popup->actions(); 0465 QVERIFY(actions.contains(&appAction1)); 0466 QVERIFY(actions.contains(&appAction2)); 0467 QVERIFY(actions.at(actions.indexOf(&appAction1) - 1)->isSeparator()); 0468 QVERIFY(actions.at(actions.indexOf(&appAction2) + 1)->isSeparator()); 0469 0470 // And when selecting action appAction1 0471 const QRect actionGeom = popup->actionGeometry(&appAction1); 0472 QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center()); 0473 0474 // Then the menu should hide and the job terminate (without doing any copying) 0475 QVERIFY(jobSpy.waitForResult()); 0476 QCOMPARE(jobSpy.error(), 0); 0477 const QString destFile = tempDestDir.path() + "/srcfile"; 0478 QVERIFY(!QFile::exists(destFile)); 0479 } 0480 0481 private: 0482 static QMenu *findPopup() 0483 { 0484 const QList<QWidget *> widgetsList = qApp->topLevelWidgets(); 0485 for (QWidget *widget : widgetsList) { 0486 if (QMenu *menu = qobject_cast<QMenu *>(widget)) { 0487 return menu; 0488 } 0489 } 0490 return nullptr; 0491 } 0492 static Qt::DropActions popupDropActions(QMenu *menu) 0493 { 0494 Qt::DropActions actions; 0495 const QList<QAction *> actionsList = menu->actions(); 0496 for (const QAction *action : actionsList) { 0497 const QVariant userData = action->data(); 0498 if (userData.isValid()) { 0499 actions |= userData.value<Qt::DropAction>(); 0500 } 0501 } 0502 return actions; 0503 } 0504 QMimeData m_mimeData; // contains m_srcFile 0505 QTemporaryDir m_tempDir; 0506 QString m_srcDir; 0507 QString m_srcFile; 0508 QString m_srcLink; 0509 QTemporaryDir m_nonWritableTempDir; 0510 QString m_trashDir; 0511 }; 0512 0513 QTEST_MAIN(DropJobTest) 0514 0515 #include "dropjobtest.moc"