File indexing completed on 2024-04-28 03:53:22

0001 /*
0002     SPDX-FileCopyrightText: 2023 Felix Ernst <felixernst@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include <QMainWindow>
0008 #include <QMenu>
0009 #include <QMenuBar>
0010 #include <QObject>
0011 #include <QSignalSpy>
0012 #include <QStandardPaths>
0013 #include <QTest>
0014 #include <QTimer>
0015 #include <QToolBar>
0016 #include <QToolButton>
0017 
0018 #include "khamburgermenu.h"
0019 
0020 class KHamburgerMenuTest : public QObject
0021 {
0022     Q_OBJECT
0023 
0024 private Q_SLOTS:
0025     void initTestCase();
0026     void init();
0027 
0028     /**
0029      * Tests whether the hamburger button actually hides when it is redundant.
0030      * Also makes sure that the "Menu" action, that can be added to context menus in situations in which no other menus are available,
0031      * only appears in such situations.
0032      * A special focus is set on making sure that the aboutToShowMenu() signal is emitted correctly to avoid premature population of menus.
0033      */
0034     void visibilityTest();
0035 
0036     /**
0037      * Tests whether the populating of the hamburger menu happens correctly based on the provided information.
0038      * A special focus is set on making sure that the aboutToShowMenu() signal is emitted correctly to avoid premature population of menus.
0039      * We don't want those menus to be populated until the aboutToShowMenu() has been emitted, so the populating code is not triggered until needed.
0040      */
0041     void menuContentsTest();
0042 
0043     /**
0044      * For accessibility we want the shortcut of KHamburgerMenu to always open a menu.
0045      */
0046     void openMenuByShortcutTest();
0047 
0048 private:
0049     QScopedPointer<QMainWindow> m_mainWindow;
0050     QPointer<QMenuBar> m_menuBar;
0051     QPointer<QMenu> m_menuBarFileMenu;
0052     QPointer<QToolBar> m_toolBar;
0053 };
0054 
0055 void KHamburgerMenuTest::initTestCase()
0056 {
0057     QStandardPaths::setTestModeEnabled(true);
0058 }
0059 
0060 void KHamburgerMenuTest::init()
0061 {
0062     m_mainWindow.reset(new QMainWindow());
0063     m_toolBar = new QToolBar(m_mainWindow.get());
0064     m_mainWindow->addToolBar(m_toolBar);
0065 
0066     m_menuBar = m_mainWindow->menuBar();
0067     m_menuBarFileMenu = new QMenu(QStringLiteral("File"), m_menuBar);
0068     m_menuBarFileMenu->addAction(QStringLiteral("New"));
0069     m_menuBarFileMenu->addAction(QStringLiteral("Open"));
0070     m_menuBarFileMenu->addAction(QStringLiteral("Quit"));
0071     m_menuBar->addMenu(m_menuBarFileMenu);
0072     m_menuBar->addAction(QStringLiteral("Help"));
0073 
0074     m_mainWindow->show();
0075 }
0076 
0077 bool isHamburgerButtonVisibleOnToolBar(KHamburgerMenu *hamburgerMenuAction, const QToolBar *toolBar)
0078 {
0079     const QToolButton *hamburgerToolButton = qobject_cast<QToolButton *>(toolBar->widgetForAction(hamburgerMenuAction));
0080     if (!hamburgerToolButton) {
0081         return false;
0082     }
0083     if (hamburgerToolButton->width() < 1 || hamburgerToolButton->height() < 1) {
0084         return false;
0085     }
0086 
0087     bool actuallyVisible = hamburgerToolButton->isVisible();
0088     const QWidget *ancestorWidget = hamburgerToolButton->parentWidget();
0089     while (actuallyVisible && ancestorWidget) {
0090         actuallyVisible = ancestorWidget->isVisible();
0091         ancestorWidget = ancestorWidget->parentWidget();
0092     }
0093     return actuallyVisible;
0094 }
0095 
0096 void KHamburgerMenuTest::visibilityTest()
0097 {
0098     KHamburgerMenu *hamburgerMenuAction = new KHamburgerMenu(m_mainWindow.get());
0099     QSignalSpy aboutToShowMenuSignalsSpy(hamburgerMenuAction, &KHamburgerMenu::aboutToShowMenu);
0100     m_toolBar->addAction(hamburgerMenuAction);
0101     QTRY_VERIFY(isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, m_toolBar));
0102 
0103     /* 1. Test visibility interactions with menu bar */
0104 
0105     hamburgerMenuAction->setMenuBar(m_menuBar);
0106     QTRY_VERIFY2(!isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, m_toolBar), "KHamburgerMenu hides when there is a visible menu bar.");
0107 
0108     m_menuBar->hide();
0109     QTRY_VERIFY2(isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, m_toolBar), "KHamburgerMenu re-appears when the menu bar is hidden.");
0110 
0111     /* 2. Test visibility interactions of the context menu action */
0112 
0113     QMenu *contextMenu = new QMenu(m_mainWindow.get());
0114     hamburgerMenuAction->addToMenu(contextMenu);
0115     QCOMPARE(contextMenu->actions().count(), 1);
0116     contextMenu->addAction("test");
0117     QMenu *hamburgerMenuMenu = new QMenu(m_mainWindow.get());
0118     hamburgerMenuMenu->addAction("testActionInsideTheHamburgerMenu");
0119     hamburgerMenuAction->setMenu(hamburgerMenuMenu);
0120 
0121     m_menuBar->show();
0122     hamburgerMenuAction->addToMenu(contextMenu);
0123     contextMenu->popup(QPoint());
0124     QVERIFY2(!contextMenu->actions().last()->isVisible(), "The context menu action should be invisible because the menu bar is visible.");
0125 
0126     m_menuBar->hide();
0127     QTRY_VERIFY2(isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, m_toolBar), "KHamburgerMenu re-appears when the menu bar is hidden.");
0128     hamburgerMenuAction->addToMenu(contextMenu);
0129     contextMenu->popup(QPoint());
0130     QVERIFY2(!contextMenu->actions().last()->isVisible(), "The context menu action should be invisible because KHamburgerMenu is visible in the toolbar.");
0131 
0132     m_menuBar->hide();
0133     m_toolBar->hide();
0134     hamburgerMenuAction->addToMenu(contextMenu);
0135     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 0);
0136     contextMenu->popup(QPoint());
0137     QVERIFY2(contextMenu->actions().last()->isVisible(), "The context menu action should be visible because there is no other menu anywhere.");
0138     QVERIFY2(aboutToShowMenuSignalsSpy.count() > 0,
0139              "The hamburger menu within the context menu should already be populated at this point, "
0140              "so at least one aboutToShowMenu() signal should have been emitted.");
0141 
0142     const int count = aboutToShowMenuSignalsSpy.count();
0143     m_toolBar->show();
0144     hamburgerMenuAction->addToMenu(contextMenu);
0145     contextMenu->popup(QPoint());
0146     QVERIFY2(!contextMenu->actions().last()->isVisible(),
0147              "The context menu action should be hidden because the toolbar containing the hamburger menu is "
0148              "now visible.");
0149     QCOMPARE(aboutToShowMenuSignalsSpy.count(), count); // There is no reason why it would have changed.
0150 
0151     /* 3. Test visibility interactions when there are multiple buttons for one KHamburgerMenu */
0152 
0153     auto secondToolBar = new QToolBar(m_mainWindow.get());
0154     m_mainWindow->addToolBar(Qt::BottomToolBarArea, secondToolBar);
0155     secondToolBar->addAction(hamburgerMenuAction);
0156     QTRY_VERIFY(isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, secondToolBar));
0157 
0158     m_menuBar->show();
0159     QTRY_VERIFY(!isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, secondToolBar));
0160 
0161     m_menuBar->hide();
0162     QTRY_VERIFY(isHamburgerButtonVisibleOnToolBar(hamburgerMenuAction, secondToolBar));
0163 
0164     m_toolBar->hide();
0165     hamburgerMenuAction->addToMenu(contextMenu);
0166     contextMenu->popup(QPoint());
0167     QVERIFY2(!contextMenu->actions().last()->isVisible(),
0168              "The context menu action should be hidden because the secondToolBar containing the hamburger menu "
0169              "action is now visible.");
0170 
0171     secondToolBar->hide();
0172     hamburgerMenuAction->addToMenu(contextMenu);
0173     QCOMPARE(aboutToShowMenuSignalsSpy.count(), count); // There is no reason why it would have changed.
0174     contextMenu->popup(QPoint());
0175     QVERIFY2(contextMenu->actions().last()->isVisible(), "The context menu action should be visible because there is no other menu anywhere.");
0176     QVERIFY(aboutToShowMenuSignalsSpy.count() > count); // It changed for the previous popup() because there actually was a menu in there that needed updating.
0177 }
0178 
0179 void openAndCloseToolButton(QToolButton *button)
0180 {
0181     QTimer::singleShot(30, [button]() {
0182         button->menu()->close();
0183     }); // This seems to be the only way because code that isn't already set in motion through a timer doesn't get executed after the menu pops up.
0184 
0185     QTest::mouseClick(button, Qt::LeftButton);
0186 }
0187 
0188 void KHamburgerMenuTest::menuContentsTest()
0189 {
0190     KHamburgerMenu *hamburgerMenuAction = new KHamburgerMenu(m_mainWindow.get());
0191     QSignalSpy aboutToShowMenuSignalsSpy(hamburgerMenuAction, &KHamburgerMenu::aboutToShowMenu);
0192     m_toolBar->addAction(hamburgerMenuAction);
0193     QToolButton *hamburgerToolButton = qobject_cast<QToolButton *>(m_toolBar->widgetForAction(hamburgerMenuAction));
0194     QVERIFY(hamburgerToolButton);
0195     QVERIFY2(!hamburgerToolButton->menu() || hamburgerToolButton->menu()->isEmpty(),
0196              "The hamburger menu button is not supposed to have a menu until it is "
0197              "pressed the first time.");
0198 
0199     m_menuBar->hide();
0200     hamburgerMenuAction->setMenuBar(m_menuBar);
0201     openAndCloseToolButton(hamburgerToolButton);
0202     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 1);
0203     // The hamburger menu now contains the two menu bar actions "File" and "Help".
0204     QVERIFY(hamburgerToolButton->menu());
0205     QVERIFY(!hamburgerToolButton->menu()->isEmpty());
0206     QCOMPARE(hamburgerToolButton->menu()->actions().count(), 2);
0207 
0208     QMenu *hamburgerMenuMenu = new QMenu(m_mainWindow.get());
0209     hamburgerMenuMenu->addAction("testActionInsideTheHamburgerMenu");
0210     hamburgerMenuAction->setMenu(hamburgerMenuMenu);
0211     QCOMPARE(hamburgerToolButton->menu()->actions().count(), 2); // The menu actions are not updated until the menu is opened again.
0212 
0213     openAndCloseToolButton(hamburgerToolButton);
0214     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 2);
0215     QVERIFY(!hamburgerToolButton->menu()->isEmpty());
0216     QCOMPARE(hamburgerToolButton->menu()->actions().count(), 4); // The hamburger menu now contains
0217     // testActionInsideTheHamburgerMenu, the help menu, a separator and the menuBarAdvertisementsMenu.
0218 
0219     hamburgerMenuMenu->addAction("testActionInsideTheHamburgerMenu2");
0220     QCOMPARE(hamburgerToolButton->menu()->actions().count(), 4); // The menu actions are not updated until the menu is opened again.
0221 
0222     openAndCloseToolButton(hamburgerToolButton);
0223     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 3);
0224     QVERIFY(!hamburgerToolButton->menu()->isEmpty());
0225     QCOMPARE(hamburgerToolButton->menu()->actions().count(), 5); // The hamburger menu now contains
0226     // testActionInsideTheHamburgerMenu, testActionInsideTheHamburgerMenu2, the help menu, a separator and the menuBarAdvertisementsMenu.
0227 
0228     hamburgerToolButton->menu()->actions().last()->menu()->aboutToShow();
0229     QCOMPARE(hamburgerToolButton->menu()->actions().last()->menu()->actions().count(), 2); // The "More" menu also known as the "menuBarAdvertisementMenu"
0230     // contains the a section header and the "File" menu.
0231 
0232     hamburgerMenuAction->setShowMenuBarAction(new QAction(QStringLiteral("Show Menubar"), m_mainWindow.get()));
0233     m_menuBar->removeAction(m_menuBar->actions().first()); // Remove "File" menu.
0234     openAndCloseToolButton(hamburgerToolButton);
0235     hamburgerToolButton->menu()->actions().last()->menu()->aboutToShow();
0236     QCOMPARE(hamburgerToolButton->menu()->actions().last()->menu()->actions().count(), 2); // The "More" menu also known as the "menuBarAdvertisementMenu"
0237     // contains the "Show Menubar"-action and an invisible section header.
0238 
0239     m_menuBar->addAction(new QAction(QStringLiteral("Edit"), m_mainWindow.get()));
0240     openAndCloseToolButton(hamburgerToolButton);
0241     hamburgerToolButton->menu()->actions().last()->menu()->aboutToShow();
0242     QCOMPARE(hamburgerToolButton->menu()->actions().last()->menu()->actions().count(),
0243              3); // The "More" menu also known as the "menuBarAdvertisementMenu" contains the "Show Menubar"-action, a section header and an "Edit" action.
0244 }
0245 
0246 void KHamburgerMenuTest::openMenuByShortcutTest()
0247 {
0248     KHamburgerMenu *hamburgerMenuAction = new KHamburgerMenu(m_mainWindow.get());
0249     QSignalSpy aboutToShowMenuSignalsSpy(hamburgerMenuAction, &KHamburgerMenu::aboutToShowMenu);
0250     QMenu *hamburgerMenuMenu = new QMenu(m_mainWindow.get());
0251     QAction *testActionInsideHamburgerMenu = new QAction(QStringLiteral("testActionInsideTheHamburgerMenu"), m_mainWindow.get());
0252     hamburgerMenuMenu->addAction(testActionInsideHamburgerMenu);
0253     hamburgerMenuAction->setMenu(hamburgerMenuMenu);
0254 
0255     // Make sure a KHamburgerMenu button can be activated/opened by shortcut
0256     m_toolBar->addAction(hamburgerMenuAction);
0257     QToolButton *hamburgerToolButton = qobject_cast<QToolButton *>(m_toolBar->widgetForAction(hamburgerMenuAction));
0258     hamburgerMenuAction->trigger();
0259     QTRY_VERIFY(hamburgerToolButton->menu()->isVisible()); // Wait for the menu to be shown
0260     QVERIFY(hamburgerToolButton->menu()->actions().first() == testActionInsideHamburgerMenu);
0261     hamburgerToolButton->menu()->close();
0262     QTRY_VERIFY(!hamburgerToolButton->menu()->isVisible());
0263     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 1);
0264 
0265     // Make sure a KHamburgerMenu menu can be activated/opened by shortcut even if there is no visible button
0266     m_toolBar->hide();
0267     hamburgerMenuAction->trigger();
0268     QTRY_VERIFY(hamburgerToolButton->menu()->isVisible()); // We test for the toolButton's menu even though the menu that will be shown won't visually belong
0269                                                            // to the button (which is invisible) because the menu should still be the same.
0270     QVERIFY(hamburgerToolButton->menu()->actions().first() == testActionInsideHamburgerMenu);
0271     QVERIFY(!hamburgerToolButton->isDown());
0272     hamburgerToolButton->menu()->close();
0273     QTRY_VERIFY(!hamburgerToolButton->menu()->isVisible());
0274     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 2);
0275 
0276     // Make sure the first menu bar menu can be activated/opened by shortcut when it is visible
0277     hamburgerMenuAction->setMenuBar(m_menuBar);
0278     hamburgerMenuAction->trigger();
0279     QTRY_VERIFY(m_menuBarFileMenu->isVisible());
0280     QVERIFY(!hamburgerToolButton->menu()->isVisible());
0281     m_menuBarFileMenu->close();
0282     QTRY_VERIFY(!m_menuBarFileMenu->isVisible());
0283     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 2); // Unchanged because the menu was not opened.
0284 
0285     // Make sure a KHamburgerMenu button will again be activated/opened by shortcut after the shortcut has been used to open the first menu bar menu.
0286     m_menuBar->hide();
0287     m_toolBar->show();
0288     hamburgerMenuAction->trigger();
0289     QTRY_VERIFY(hamburgerToolButton->menu()->isVisible()); // Wait for the menu to be shown
0290     QVERIFY(hamburgerToolButton->menu()->actions().first() == testActionInsideHamburgerMenu);
0291     hamburgerToolButton->menu()->close();
0292     QTRY_VERIFY(!hamburgerToolButton->menu()->isVisible());
0293     QCOMPARE(aboutToShowMenuSignalsSpy.count(), 3);
0294 }
0295 
0296 QTEST_MAIN(KHamburgerMenuTest)
0297 
0298 #include "khamburgermenutest.moc"