// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace QQuickVisualTestUtils; using namespace QQuickDialogTestUtils; using namespace QQuickControlsTestUtils; class tst_QQuickFolderDialogImpl : public QQmlDataTest { Q_OBJECT public: tst_QQuickFolderDialogImpl(); static void initMain() { // We need to set this attribute. QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); // We also don't want to run this for every style, as each one will have // different ways of implementing the dialogs. // For now we only test one style. QQuickStyle::setStyle("Basic"); } private slots: void initTestCase() override; void cleanupTestCase(); void defaults(); void chooseFolderViaStandardButtons(); void bindCurrentFolder_data(); void bindCurrentFolder(); void changeFolderViaDoubleClick(); void changeFolderViaTextEdit(); void changeFolderViaEnter(); void cancelDialogWhileTextEditHasFocus(); void goUp(); void goUpWhileTextEditHasFocus(); void goIntoLargeFolder(); void clickOnBreadcrumb(); void keyAndShortcutHandling(); void tabFocusNavigation(); void acceptRejectLabel(); void bindTitle(); void itemsDisabledWhenNecessary(); void done_data(); void done(); void checkModality_data(); void checkModality(); private: QTemporaryDir tempDir; QString tempDirCanonicalPath; QScopedPointer tempFile1; QString tempFile1CanonicalPath; QScopedPointer tempFile2; QString tempFile2CanonicalPath; QDir tempSubDir1; QString tempSubDir1CanonicalPath; QDir tempSubSubDir; QScopedPointer tempSubFile1; QScopedPointer tempSubFile2; QDir tempSubDir2; QString tempSubDir2CanonicalPath; QDir oldCurrentDir; }; tst_QQuickFolderDialogImpl::tst_QQuickFolderDialogImpl() : QQmlDataTest(QT_QMLTEST_DATADIR) { } void tst_QQuickFolderDialogImpl::initTestCase() { QQmlDataTest::initTestCase(); QVERIFY(tempDir.isValid()); tempDirCanonicalPath = QFileInfo(tempDir.path()).canonicalFilePath(); // QTEST_QUICKCONTROLS_MAIN constructs the test case object once, // and then calls qRun() for each style, and qRun() calls initTestCase(). // So, we need to check if we've already made the temporary directory. // Note that this is only necessary if the test is run with more than one style. if (!QDir(tempDirCanonicalPath).isEmpty()) return; /* Create a couple of files within the temporary directory. The structure is: [temp directory] ├── sub-dir-1 │ ├── sub-sub-dir │ ├── sub-file1.txt │ └── sub-file2.txt ├── sub-dir-2 ├── file1.txt └── file2.txt */ tempSubDir1 = QDir(tempDirCanonicalPath); QVERIFY2(tempSubDir1.mkdir("sub-dir-1"), qPrintable(QString::fromLatin1( "Failed to make sub-directory \"sub-dir-1\" in %1. Permissions are: %2") .arg(tempDirCanonicalPath, QDebug::toString(QFileInfo(tempDirCanonicalPath).permissions())))); QVERIFY(tempSubDir1.cd("sub-dir-1")); tempSubDir1CanonicalPath = tempSubDir1.canonicalPath(); QVERIFY(!tempSubDir1CanonicalPath.isEmpty()); tempSubSubDir = QDir(tempSubDir1CanonicalPath); QVERIFY2(tempSubSubDir.mkdir("sub-sub-dir"), qPrintable(QString::fromLatin1( "Failed to make sub-directory \"sub-sub-dir\" in %1. Permissions are: %2") .arg(tempSubDir1CanonicalPath, QDebug::toString(QFileInfo(tempSubDir1CanonicalPath).permissions())))); QVERIFY(tempSubSubDir.cd("sub-sub-dir")); tempSubFile1.reset(new QFile(tempSubDir1CanonicalPath + "/sub-file1.txt")); QVERIFY(tempSubFile1->open(QIODevice::ReadWrite)); tempSubFile2.reset(new QFile(tempSubDir1CanonicalPath + "/sub-file2.txt")); QVERIFY(tempSubFile2->open(QIODevice::ReadWrite)); tempSubDir2 = QDir(tempDirCanonicalPath); QVERIFY2(tempSubDir2.mkdir("sub-dir-2"), qPrintable(QString::fromLatin1( "Failed to make sub-directory \"sub-dir-2\" in %1. Permissions are: %2") .arg(tempDirCanonicalPath, QDebug::toString(QFileInfo(tempDirCanonicalPath).permissions())))); QVERIFY(tempSubDir2.cd("sub-dir-2")); tempSubDir2CanonicalPath = tempSubDir2.canonicalPath(); QVERIFY(!tempSubDir2CanonicalPath.isEmpty()); tempFile1.reset(new QFile(tempDirCanonicalPath + "/file1.txt")); QVERIFY(tempFile1->open(QIODevice::ReadWrite)); tempFile2.reset(new QFile(tempDirCanonicalPath + "/file2.txt")); QVERIFY(tempFile2->open(QIODevice::ReadWrite)); // Ensure that each test starts off in the temporary directory. oldCurrentDir = QDir::current(); QDir::setCurrent(tempDirCanonicalPath); } void tst_QQuickFolderDialogImpl::cleanupTestCase() { // Just in case... QDir::setCurrent(oldCurrentDir.path()); } void tst_QQuickFolderDialogImpl::defaults() { QTest::failOnWarning(QRegularExpression(".*")); QQuickApplicationHelper helper(this, "folderDialog.qml"); QVERIFY2(helper.ready, helper.failureMessage()); QQuickWindow *window = helper.window; window->show(); window->requestActivate(); QVERIFY(QTest::qWaitForWindowExposed(window)); QQuickFolderDialog *dialog = window->property("dialog").value(); QVERIFY(dialog); COMPARE_URL(dialog->currentFolder(), QUrl::fromLocalFile(QDir().absolutePath())); // The first file in the directory should be selected, but not until the dialog is actually open, // as QQuickFileDialogImpl hasn't been created yet. COMPARE_URL(dialog->selectedFolder(), QUrl()); QCOMPARE(dialog->title(), QString()); dialog->open(); QQuickFolderDialogImpl *quickDialog = window->findChild(); QVERIFY(quickDialog); QTRY_VERIFY(quickDialog->isOpened()); QVERIFY(quickDialog); COMPARE_URL(quickDialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); COMPARE_URL(quickDialog->currentFolder(), QUrl::fromLocalFile(QDir().absolutePath())); COMPARE_URL(dialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); COMPARE_URL(dialog->currentFolder(), QUrl::fromLocalFile(QDir().absolutePath())); QCOMPARE(quickDialog->title(), QString()); } typedef DialogTestHelper FolderDialogTestHelper; class FolderDialogSignalHelper { public: FolderDialogSignalHelper(const FolderDialogTestHelper &dialogTestHelper) : dialogSelectedFolderChangedSpy(dialogTestHelper.dialog, SIGNAL(selectedFolderChanged())), quickDialogSelectedFolderChangedSpy(dialogTestHelper.quickDialog, SIGNAL(selectedFolderChanged(QUrl))), dialogCurrentFolderChangedSpy(dialogTestHelper.dialog, SIGNAL(currentFolderChanged())), quickDialogCurrentFolderChangedSpy(dialogTestHelper.quickDialog, SIGNAL(currentFolderChanged(QUrl))) { if (!dialogSelectedFolderChangedSpy.isValid()) errorMessage = "selectedFolderChanged signal of dialog is not valid"; if (!quickDialogSelectedFolderChangedSpy.isValid()) errorMessage = "selectedFolderChanged signal of quickDialog is not valid"; if (!dialogCurrentFolderChangedSpy.isValid()) errorMessage = "currentFolderChanged signal of dialog is not valid"; if (!quickDialogCurrentFolderChangedSpy.isValid()) errorMessage = "currentFolderChanged signal of quickDialog is not valid"; } QSignalSpy dialogSelectedFolderChangedSpy; QSignalSpy quickDialogSelectedFolderChangedSpy; QSignalSpy dialogCurrentFolderChangedSpy; QSignalSpy quickDialogCurrentFolderChangedSpy; QByteArray errorMessage; }; void tst_QQuickFolderDialogImpl::chooseFolderViaStandardButtons() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Select the delegate by clicking once. const FolderDialogSignalHelper signalHelper(dialogHelper); QVERIFY2(signalHelper.errorMessage.isEmpty(), signalHelper.errorMessage); auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QQuickFileDialogDelegate *delegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 1, delegate)); COMPARE_URL(delegate->file(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); QVERIFY(clickButton(delegate)); // currentFolder shouldn't change just from a single click. COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); COMPARE_URL(dialogHelper.quickDialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); // Only selectedFile-related signals should be emitted. QCOMPARE(signalHelper.dialogSelectedFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.quickDialogSelectedFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.dialogCurrentFolderChangedSpy.size(), 0); QCOMPARE(signalHelper.quickDialogCurrentFolderChangedSpy.size(), 0); // Click the "Open" button. QVERIFY(dialogHelper.quickDialog->footer()); auto dialogButtonBox = qobject_cast(dialogHelper.quickDialog->footer()); QVERIFY(dialogButtonBox); QQuickAbstractButton* openButton = findDialogButton(dialogButtonBox, "Open"); QVERIFY(openButton); QVERIFY(clickButton(openButton)); COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); COMPARE_URL(dialogHelper.quickDialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); QCOMPARE(signalHelper.dialogSelectedFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.quickDialogSelectedFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.dialogCurrentFolderChangedSpy.size(), 0); QCOMPARE(signalHelper.quickDialogCurrentFolderChangedSpy.size(), 0); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); QVERIFY(!dialogHelper.dialog->isVisible()); } void tst_QQuickFolderDialogImpl::bindCurrentFolder_data() { QTest::addColumn("initialFolder"); QTest::addColumn("expectedFolder"); QTest::addColumn("expectedVisibleFiles"); const auto currentDirUrl = QUrl::fromLocalFile(QDir::current().canonicalPath()); const auto tempSubDirUrl = QUrl::fromLocalFile(tempSubDir1CanonicalPath); const auto tempSubFile1Url = QUrl::fromLocalFile(tempSubFile1->fileName()); const QStringList currentDirFiles = { tempSubDir1CanonicalPath, tempSubDir2CanonicalPath }; const QStringList tempSubDirFiles = { tempSubSubDir.path() }; // Setting the folder to "sub-dir-1" should result in "sub-sub-dir" being visible. QTest::addRow("sub-dir-1") << tempSubDirUrl << tempSubDirUrl << tempSubDirFiles; // Setting a file as the folder shouldn't work, and the dialog shouldn't change its folder. QTest::addRow("sub-dir/sub-file1.txt") << tempSubFile1Url << currentDirUrl << currentDirFiles; } void tst_QQuickFolderDialogImpl::bindCurrentFolder() { QFETCH(QUrl, initialFolder); QFETCH(QUrl, expectedFolder); QFETCH(QStringList, expectedVisibleFiles); // Open the dialog. FolderDialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, {{ "initialFolder", initialFolder }}); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); COMPARE_URL(dialogHelper.dialog->currentFolder(), expectedFolder); auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QString failureMessage; // Even waiting for ListView polish and that the FolderListModel's status is ready aren't enough // on Windows, apparently, as sometimes there just aren't any delegates by the time we do the check. // So, we use QTRY_VERIFY2 each time we call this function just to be safe. QTRY_VERIFY2(verifyFileDialogDelegates(folderDialogListView, expectedVisibleFiles, failureMessage), qPrintable(failureMessage)); // Check that the breadcrumb bar is correct by constructing the expected files from the expectedFolder. auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); QVERIFY2(verifyBreadcrumbDelegates(breadcrumbBar, expectedFolder, failureMessage), qPrintable(failureMessage)); } void tst_QQuickFolderDialogImpl::changeFolderViaDoubleClick() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Double-click on the "sub-dir-2" delegate. const FolderDialogSignalHelper signalHelper(dialogHelper); QVERIFY2(signalHelper.errorMessage.isEmpty(), signalHelper.errorMessage); auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QQuickFileDialogDelegate *delegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 1, delegate)); COMPARE_URL(delegate->file(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)) QVERIFY(doubleClickButton(delegate)); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)) COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl()); // selectedFolder is set to the folder when clicked and then set to an empty URL after // the double click. QCOMPARE(signalHelper.dialogSelectedFolderChangedSpy.size(), 2); QCOMPARE(signalHelper.quickDialogSelectedFolderChangedSpy.size(), 2); QCOMPARE(signalHelper.dialogCurrentFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.quickDialogCurrentFolderChangedSpy.size(), 1); // Since we only changed the current folder, the dialog should still be open. QVERIFY(dialogHelper.dialog->isVisible()); dialogHelper.dialog->close(); QVERIFY(!dialogHelper.dialog->isVisible()); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); } void tst_QQuickFolderDialogImpl::changeFolderViaTextEdit() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); #if QT_CONFIG(shortcut) // Get the text edit visible with Ctrl+L. const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); QTest::keySequence(dialogHelper.popupWindow(), editPathKeySequence); auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); QVERIFY(breadcrumbBar->textField()->isVisible()); QTRY_VERIFY(breadcrumbBar->textField()->hasActiveFocus()); QCOMPARE(breadcrumbBar->textField()->text(), dialogHelper.dialog->currentFolder().toLocalFile()); QCOMPARE(breadcrumbBar->textField()->selectedText(), breadcrumbBar->textField()->text()); // Enter the path to the folder in the text edit. enterText(dialogHelper.popupWindow(), tempSubDir2CanonicalPath); QCOMPARE(breadcrumbBar->textField()->text(), tempSubDir2CanonicalPath); #endif // Hit enter to accept. QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Return); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); // We changed into a directory with no folders, so the selected folder should be invalid. COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl()); COMPARE_URL(dialogHelper.quickDialog->selectedFolder(), QUrl()); QTRY_VERIFY(dialogHelper.dialog->isVisible()); // Close the dialog. dialogHelper.dialog->close(); QVERIFY(!dialogHelper.dialog->isVisible()); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); } void tst_QQuickFolderDialogImpl::changeFolderViaEnter() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); // The first delegate in the view should be selected and have focus. COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QQuickFileDialogDelegate *subDir1Delegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 0, subDir1Delegate)); COMPARE_URL(subDir1Delegate->file(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); QTRY_VERIFY_ACTIVE_FOCUS(subDir1Delegate); // Select the delegate by pressing enter. const FolderDialogSignalHelper signalHelper(dialogHelper); QVERIFY2(signalHelper.errorMessage.isEmpty(), signalHelper.errorMessage); QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Return); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl::fromLocalFile(tempSubSubDir.path())); QCOMPARE(signalHelper.dialogSelectedFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.quickDialogSelectedFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.dialogCurrentFolderChangedSpy.size(), 1); QCOMPARE(signalHelper.quickDialogCurrentFolderChangedSpy.size(), 1); // Since we only changed the current folder, the dialog should still be open. QVERIFY(dialogHelper.dialog->isVisible()); dialogHelper.dialog->close(); QVERIFY(!dialogHelper.dialog->isVisible()); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); } void tst_QQuickFolderDialogImpl::cancelDialogWhileTextEditHasFocus() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); #if QT_CONFIG(shortcut) // Get the text edit visible with Ctrl+L. const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); QTest::keySequence(dialogHelper.popupWindow(), editPathKeySequence); #endif auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); QVERIFY(breadcrumbBar->textField()->isVisible()); QCOMPARE(breadcrumbBar->textField()->text(), dialogHelper.dialog->currentFolder().toLocalFile()); QCOMPARE(breadcrumbBar->textField()->selectedText(), breadcrumbBar->textField()->text()); // Close it via the cancel button. auto dialogButtonBox = qobject_cast(dialogHelper.quickDialog->footer()); QVERIFY(dialogButtonBox); const QString cancelText = QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Cancel); auto cancelButton = findDialogButton(dialogButtonBox, cancelText); QVERIFY(cancelButton); QVERIFY(clickButton(cancelButton)); // Open it again. The text field should not be visible, but the breadcrumb bar itself should be. dialogHelper.dialog->open(); QVERIFY(dialogHelper.dialog->isVisible()); QTRY_VERIFY(dialogHelper.quickDialog->isOpened()); QVERIFY(breadcrumbBar->isVisible()); // The ListView that contains the breadcrumb delegates should be visible. QVERIFY(breadcrumbBar->contentItem()->isVisible()); QVERIFY(!breadcrumbBar->textField()->isVisible()); } void tst_QQuickFolderDialogImpl::goUp() { // Open the dialog, starting off in "sub-dir-1". FolderDialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, {{ "initialFolder", QUrl::fromLocalFile(tempSubDir1CanonicalPath) }}); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Go up a directory via the button next to the breadcrumb bar. auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); auto barListView = qobject_cast(breadcrumbBar->contentItem()); QVERIFY(barListView); if (QQuickTest::qIsPolishScheduled(barListView)) QVERIFY(QQuickTest::qWaitForPolish(barListView)); QVERIFY(clickButton(breadcrumbBar->upButton())); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); // The previous directory that we were in should now be selected (matches e.g. Windows and Ubuntu). COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QQuickFileDialogDelegate *subDirDelegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 0, subDirDelegate)); QCOMPARE(subDirDelegate->isHighlighted(), true); #if QT_CONFIG(shortcut) // Go up a directory via the keyboard shortcut. const auto goUpKeySequence = QKeySequence(Qt::ALT | Qt::Key_Up); QTest::keySequence(dialogHelper.window(), goUpKeySequence); QDir tempParentDir(tempDirCanonicalPath); QVERIFY(tempParentDir.cdUp()); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempParentDir.path())); COMPARE_URL(dialogHelper.dialog->selectedFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); #endif } void tst_QQuickFolderDialogImpl::goUpWhileTextEditHasFocus() { // Open the dialog, starting off in "sub-dir-1". FolderDialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, {{ "initialFolder", QUrl::fromLocalFile(tempSubDir1CanonicalPath) }}); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); // Go up a directory via the button next to the breadcrumb bar. auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); auto barListView = qobject_cast(breadcrumbBar->contentItem()); QVERIFY(barListView); if (QQuickTest::qIsPolishScheduled(barListView)) QVERIFY(QQuickTest::qWaitForPolish(barListView)); QVERIFY(clickButton(breadcrumbBar->upButton())); // The path should have changed to the parent directory. COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); // The text edit should be hidden when it loses focus. QVERIFY(!breadcrumbBar->textField()->hasActiveFocus()); QVERIFY(!breadcrumbBar->textField()->isVisible()); // The focus should be given to the first delegate. QTRY_VERIFY(dialogHelper.popupWindow()->activeFocusItem()); auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QQuickFileDialogDelegate *firstDelegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 0, firstDelegate)); QCOMPARE(dialogHelper.popupWindow()->activeFocusItem(), firstDelegate); } void tst_QQuickFolderDialogImpl::goIntoLargeFolder() { // Create a throwaway directory with a lot of folders within it... QTemporaryDir anotherTempDir; QVERIFY(anotherTempDir.isValid()); for (int i = 0; i < 30; ++i) { QDir newDir(anotherTempDir.path()); QVERIFY(newDir.exists()); // Pad with zeroes so that the directories are ordered as we expect. QVERIFY(newDir.mkdir(QString::fromLatin1("dir%1").arg(i, 2, 10, QLatin1Char('0')))); } // ... and within one of those folders, more folders. QDir dir20(anotherTempDir.path() + "/dir20"); QVERIFY(dir20.exists()); for (int i = 0; i < 30; ++i) { QDir newDir(dir20.path()); QVERIFY(newDir.exists()); QVERIFY(newDir.mkdir(QString::fromLatin1("subdir%1").arg(i, 2, 10, QLatin1Char('0')))); } // Open the dialog. Start off in the throwaway directory. FolderDialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, {{ "initialFolder", QUrl::fromLocalFile(anotherTempDir.path()) }}); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // If the screen is so tall that the contentItem is not vertically larger than the view, // then the test makes no sense. auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); if (QQuickTest::qIsPolishScheduled(folderDialogListView)) QVERIFY(QQuickTest::qWaitForPolish(folderDialogListView)); // Just to be safe, make sure it's at least twice as big. if (folderDialogListView->contentItem()->height() < folderDialogListView->height() * 2) { QSKIP(qPrintable(QString::fromLatin1("Expected height of folderDialogListView's contentItem (%1)" \ " to be at least twice as big as the ListView's height (%2)") .arg(folderDialogListView->contentItem()->height()).arg(folderDialogListView->height()))); } // Scroll down to dir20. The view should be somewhere past the middle. QVERIFY(QMetaObject::invokeMethod(folderDialogListView, "positionViewAtIndex", Qt::DirectConnection, Q_ARG(int, 20), Q_ARG(int, QQuickItemView::PositionMode::Center))); QVERIFY(folderDialogListView->contentY() > 0); // Go into it. The view should start at the top of the directory, not at the same contentY // that it had in the previous directory. QQuickFileDialogDelegate *dir20Delegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 20, dir20Delegate)); COMPARE_URL(dir20Delegate->file(), QUrl::fromLocalFile(dir20.path())); QVERIFY(doubleClickButton(dir20Delegate)); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(dir20.path())); COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(dir20.path())); QCOMPARE(folderDialogListView->contentY(), 0); } void tst_QQuickFolderDialogImpl::clickOnBreadcrumb() { // Open the dialog, starting off in "sub-dir-1". FolderDialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, {{ "initialFolder", QUrl::fromLocalFile(tempSubDir1CanonicalPath) }}); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Find the breadcrumb bar delegate for the temp directory (parent of "sub-dir-1"). auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); auto breadcrumbBarListView = qobject_cast(breadcrumbBar->contentItem()); QVERIFY(breadcrumbBarListView); QQuickAbstractButton *breadcrumbDelegate = nullptr; // The following breadcrumb bar has 5 delegates: // [ / > tmp > deadbeef ] // We can assume that, regardless of which system we're running on, we'll have // at least 3: one for the temp directory, and one for the > separator, and one for "sub-dir-1". QVERIFY(breadcrumbBarListView->count() >= 3); // - 1 to make it zero-based, and another 2 to get to the second-last directory, which is the parent of "sub-dir-1". QTRY_VERIFY(findViewDelegateItem(breadcrumbBarListView, breadcrumbBarListView->count() - 3, breadcrumbDelegate)); QVERIFY(clickButton(breadcrumbDelegate)); QCOMPARE(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); QCOMPARE(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); QCOMPARE(dialogHelper.quickDialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); QCOMPARE(dialogHelper.dialog->selectedFolder(), QUrl::fromLocalFile(tempSubDir1CanonicalPath)); } void tst_QQuickFolderDialogImpl::keyAndShortcutHandling() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); #if QT_CONFIG(shortcut) // Get the text edit visible with Ctrl+L. const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); QTest::keySequence(dialogHelper.popupWindow(), editPathKeySequence); #endif auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); QTRY_VERIFY_ACTIVE_FOCUS(breadcrumbBar->textField()); QVERIFY(breadcrumbBar->textField()->isVisible()); QCOMPARE(breadcrumbBar->textField()->text(), dialogHelper.dialog->currentFolder().toLocalFile()); QCOMPARE(breadcrumbBar->textField()->selectedText(), breadcrumbBar->textField()->text()); #if QT_CONFIG(shortcut) // Ctrl+L shouldn't hide it. QTest::keySequence(dialogHelper.popupWindow(), editPathKeySequence); QVERIFY(breadcrumbBar->textField()->isVisible()); #endif // Cancel it with the escape key. QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Escape); QVERIFY(!breadcrumbBar->textField()->isVisible()); QVERIFY(dialogHelper.dialog->isVisible()); #if QT_CONFIG(shortcut) // Make it visible. QTest::keySequence(dialogHelper.popupWindow(), editPathKeySequence); QVERIFY(breadcrumbBar->textField()->isVisible()); #endif // Cancel it with the escape key again. QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Escape); QVERIFY(!breadcrumbBar->textField()->isVisible()); QVERIFY(dialogHelper.dialog->isVisible()); // Pressing escape now should close the dialog. QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Escape); QVERIFY(!dialogHelper.dialog->isVisible()); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); } void tst_QQuickFolderDialogImpl::tabFocusNavigation() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); QList expectedFocusItems; // The initial focus should be on the first delegate. auto folderDialogListView = dialogHelper.quickDialog->findChild("folderDialogListView"); QVERIFY(folderDialogListView); QQuickFileDialogDelegate *firstDelegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, 0, firstDelegate)); expectedFocusItems << firstDelegate; // Next, the left-most dialog button. auto dialogButtonBox = qobject_cast(dialogHelper.quickDialog->footer()); QVERIFY(dialogButtonBox); QCOMPARE(dialogButtonBox->count(), 2); auto leftMostButton = qobject_cast(dialogButtonBox->itemAt(0)); QVERIFY(leftMostButton); expectedFocusItems << leftMostButton; // Then, the right-most dialog button. auto rightMostButton = qobject_cast(dialogButtonBox->itemAt(1)); QVERIFY(rightMostButton); expectedFocusItems << rightMostButton; // Then, the up button. auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); expectedFocusItems << breadcrumbBar->upButton(); // Finally, add each bread crumb delegate. for (int i = 0; i < folderDialogListView->count(); ++i) { QQuickFileDialogDelegate *delegate = nullptr; QTRY_VERIFY(findViewDelegateItem(folderDialogListView, i, delegate)); expectedFocusItems << delegate; } // Tab through each item, checking the focus after each. for (auto expectedFocusItem : std::as_const(expectedFocusItems)) { // Check the focus item first so that we account for the first item. // Print detailed failure message as workaround for QTBUG-92102. QVERIFY2(dialogHelper.popupWindow()->activeFocusItem() == expectedFocusItem, qPrintable(QString::fromLatin1( "\n Actual: %1\n Expected: %2").arg(QDebug::toString(dialogHelper.popupWindow()->activeFocusItem())) .arg(QDebug::toString(expectedFocusItem)))); if (expectedFocusItem != expectedFocusItems.last()) QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Tab); } // Ensure the order is reversed when shift-tabbing. std::reverse(expectedFocusItems.begin(), expectedFocusItems.end()); // We know the first (last) item has focus already, so skip it. expectedFocusItems.removeFirst(); for (auto expectedFocusItem : std::as_const(expectedFocusItems)) { QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Tab, Qt::ShiftModifier); QCOMPARE(dialogHelper.popupWindow()->activeFocusItem(), expectedFocusItem); } } void tst_QQuickFolderDialogImpl::acceptRejectLabel() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "acceptRejectLabel.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Check that the accept and reject buttons' labels have changed. auto dialogButtonBox = qobject_cast(dialogHelper.quickDialog->footer()); QVERIFY(dialogButtonBox); QVERIFY(findDialogButton(dialogButtonBox, "AcceptTest")); QVERIFY(findDialogButton(dialogButtonBox, "RejectTest")); // Close the dialog. dialogHelper.dialog->close(); QVERIFY(!dialogHelper.dialog->isVisible()); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); // Reset back to the default text. dialogHelper.dialog->resetAcceptLabel(); dialogHelper.dialog->resetRejectLabel(); // Re-open the dialog. dialogHelper.dialog->open(); QVERIFY(dialogHelper.dialog->isVisible()); QTRY_VERIFY(dialogHelper.quickDialog->isOpened()); // Check that the defaults are back. const QString openText = QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Open); QVERIFY2(findDialogButton(dialogButtonBox, openText), qPrintable(QString::fromLatin1( "Failed to find dialog button with text \"%1\"").arg(openText))); const QString cancelText = QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Cancel); QVERIFY2(findDialogButton(dialogButtonBox, cancelText), qPrintable(QString::fromLatin1( "Failed to find dialog button with text \"%1\"").arg(cancelText))); } void tst_QQuickFolderDialogImpl::bindTitle() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "bindTitle.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Open the dialog and check that the correct title is displayed. const QString expectedTitle = QLatin1String("Test Title"); QCOMPARE(dialogHelper.dialog->title(), expectedTitle); QCOMPARE(dialogHelper.quickDialog->title(), expectedTitle); auto header = dialogHelper.quickDialog->header(); QVERIFY(header); auto dialogTitleBarLabel = dialogHelper.quickDialog->header()->findChild("dialogTitleBarLabel"); QVERIFY(dialogTitleBarLabel); QCOMPARE(dialogTitleBarLabel->text(), expectedTitle); } void tst_QQuickFolderDialogImpl::itemsDisabledWhenNecessary() { // Open the dialog. FolderDialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, {{ "initialFolder", QUrl::fromLocalFile(tempSubDir2CanonicalPath) }}); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(tempSubDir2CanonicalPath)); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); // We opened it in a folder that has no files, so the Open button should be disabled. QVERIFY(dialogHelper.quickDialog->footer()); auto dialogButtonBox = qobject_cast(dialogHelper.quickDialog->footer()); QVERIFY(dialogButtonBox); QQuickAbstractButton* openButton = findDialogButton(dialogButtonBox, "Open"); QVERIFY(openButton); QCOMPARE(openButton->isEnabled(), false); // Now go up. The Open button should now be enabled. auto breadcrumbBar = dialogHelper.quickDialog->findChild(); QVERIFY(breadcrumbBar); QVERIFY(clickButton(breadcrumbBar->upButton())); QCOMPARE(openButton->isEnabled(), true); COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(tempDirCanonicalPath)); #if QT_CONFIG(shortcut) // Get the text edit visible with Ctrl+L. The Open button should now be disabled. const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); QTest::keySequence(dialogHelper.popupWindow(), editPathKeySequence); QVERIFY(breadcrumbBar->textField()->isVisible()); QCOMPARE(openButton->isEnabled(), false); #endif // Hide it with the escape key. The Open button should now be enabled. QTest::keyClick(dialogHelper.popupWindow(), Qt::Key_Escape); QVERIFY(!breadcrumbBar->textField()->isVisible()); QCOMPARE(openButton->isEnabled(), true); } void tst_QQuickFolderDialogImpl::done_data() { QTest::addColumn("result"); QTest::newRow("Accepted") << QQuickAbstractDialog::Accepted; QTest::newRow("Rejected") << QQuickAbstractDialog::Rejected; } void tst_QQuickFolderDialogImpl::done() { QFETCH(QQuickAbstractDialog::StandardCode, result); // Open the dialog. FolderDialogTestHelper dialogHelper(this, "folderDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); // Call the "done" slot manually. switch (result) { case QQuickAbstractDialog::Accepted: QVERIFY(QMetaObject::invokeMethod(dialogHelper.window(), "doneAccepted")); break; case QQuickAbstractDialog::Rejected: QVERIFY(QMetaObject::invokeMethod(dialogHelper.window(), "doneRejected")); break; } QVERIFY(!dialogHelper.dialog->isVisible()); QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); QCOMPARE(dialogHelper.dialog->result(), result); } void tst_QQuickFolderDialogImpl::checkModality_data() { QTest::addColumn("modality"); QTest::addColumn("options"); QTest::addColumn("expectedRootWindowChildCount"); QTest::addColumn("expectedChildWindowClickCount"); QTest::newRow("nonModal") << Qt::NonModal << QFileDialogOptions::DontUseNativeDialog << 1 << 1; QTest::newRow("windowModal") << Qt::WindowModal << QFileDialogOptions::DontUseNativeDialog << 0 << 1; // Verify application modal case only for the platform which supports multiple windows if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows)) QTest::newRow("applicationModal") << Qt::ApplicationModal << QFileDialogOptions::DontUseNativeDialog << 0 << 0; } void tst_QQuickFolderDialogImpl::checkModality() { QFETCH(Qt::WindowModality, modality); QFETCH(QFileDialogOptions::FileDialogOption, options); QFETCH(int, expectedRootWindowChildCount); QFETCH(int, expectedChildWindowClickCount); FolderDialogTestHelper dialogHelper(this, "checkModality.qml"); dialogHelper.dialog->setModality(modality); QCOMPARE(dialogHelper.dialog->modality(), modality); dialogHelper.dialog->setOptions(options); QCOMPARE(dialogHelper.dialog->options(), options); OPEN_QUICK_DIALOG(); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); auto *childWindow = dialogHelper.window()->property("childWindow").value(); QVERIFY(childWindow); // Setting the child window transient parent as root window won't allow it to receive mouse // events, causing WindowModal case not to be tested. Thus, resetting the transient parent. if (modality == Qt::WindowModal) childWindow->setTransientParent(nullptr); const auto *rootMouseArea = dialogHelper.window()->property("rootMArea").value(); QVERIFY(rootMouseArea); QSignalSpy rmaMouseSpy(rootMouseArea, &QQuickMouseArea::clicked); QTest::mouseClick(dialogHelper.window(), Qt::LeftButton, Qt::NoModifier, QPoint(5, 5)); QCOMPARE(rmaMouseSpy.size(), expectedRootWindowChildCount); const auto *childMouseArea = dialogHelper.window()->property("childMArea").value(); QVERIFY(childMouseArea); QSignalSpy cmaMouseSpy(childMouseArea, &QQuickMouseArea::clicked); QTest::mouseClick(childWindow, Qt::LeftButton, Qt::NoModifier, QPoint(5, 5)); QCOMPARE(cmaMouseSpy.size(), expectedChildWindowClickCount); } QTEST_MAIN(tst_QQuickFolderDialogImpl) #include "tst_qquickfolderdialogimpl.moc"