Fix three crashes in the game selection screen (#9702)
#### Brief overview of PR changes/additions
- **Right-clicking the games list with nothing selected killed Mudlet.**
`dlgConnectionProfiles::slot_profileContextMenu()` dereferenced
`currentItem()` unguarded. That line is byte-identical in 4.22.0, so the
null deref itself is long-standing and latent - what is new is that it
became reachable: "improve: split the games list into My games and All
games tabs" (#9452) leaves a user with no saved profiles an empty but
still right-clickable "My games" tab, a state 4.22.0's always-populated
list never had. About 40 seconds into a fresh install.
- **Copying a profile while the list was rebuilt was a use-after-free.**
The copy runs on a thread pool and its completion handler kept the
`QListWidgetItem*` it had made; clicking the other games tab meanwhile
calls `fillout_form()`, which destroys every item. The handler now finds
the copy by name, and the `QFutureWatcher` is parented so it cannot
outlive the dialog.
- **Quitting before the connection dialog had been shown dereferenced
null.** The queued `0ms` lambda in `mudlet::slot_showConnectionDialog()`
used `mpConnectionDialog`, which `mudlet::closeEvent()` closes (it is
`WA_DeleteOnClose`) and clears.
#### Motivation for adding to Mudlet
All three came out of the 5.0 QA sweep and are confirmed with
AddressSanitizer. The first is the serious one - it is the default state
of a brand-new install, so a new user can lose Mudlet before they have
connected to anything.
Scope note on the third: it is **not** a 5.0 regression. It has been
there since "Fix: Improve tab indicators and detached window UX" (#7965)
and is unchanged in 4.22.0; #9493 only turned the literal `0` into
`0ms`. Nor could I reach it by clicking: I drove *Games -> Close
profile* followed by quitting at six delays from 0 to 2000 ms and the
dialog was always painted first. It reproduces deterministically
in-process, and QA reproduced it 2/2 driving the close from Lua. Worth
guarding - the pointer is documented to go null - but latent rather than
routinely hit.
#### Other info (issues closed, discussion etc)
Test case: `ctest -R ConnectionDialogCrashTest` - with the fix reverted,
four of its tests reproduce the original ASan reports exactly (two SEGVs
in `slot_profileContextMenu`, a heap-use-after-free in
`slot_itemClicked`, the SEGV in `QWidget::show()` from the lambda); two
more are controls that pass either way, one of them pinning that the
menu still opens for a selected profile so the guard cannot degenerate
into an unconditional early return. Full suite 79/79.
Assisted-by: Claude:claude-opus-5
2026-08-07 10:14:30 +02:00
/***************************************************************************
* Copyright ( C ) 2026 by Vadim Peretokin - vadim . peretokin @ mudlet . org *
* *
* This program is free software ; you can redistribute it and / or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation ; either version 2 of the License , or *
* ( at your option ) any later version . *
* *
* This program is distributed in the hope that it will be useful , *
* but WITHOUT ANY WARRANTY ; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the *
* GNU General Public License for more details . *
* *
* You should have received a copy of the GNU General Public License *
* along with this program ; if not , write to the *
* Free Software Foundation , Inc . , *
* 59 Temple Place - Suite 330 , Boston , MA 02111 - 1307 , USA . *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*
* Crashes of the connection dialog , driven through the real dialog against an
* isolated config directory . Reaching the end of a test is most of what it
* asserts ; the rest pins the behaviour that replaced the crash .
*
* Run with : ctest - R ConnectionDialogCrashTest - V
*/
# include "MudletInstanceCoordinator.h"
# include "dlgConnectionProfiles.h"
# include "mudlet.h"
# include <QtTest/QtTest>
# include <QAbstractScrollArea>
# include <QContextMenuEvent>
# include <QMenu>
# include <QPushButton>
# include <QTabBar>
# include <chrono>
using namespace std : : chrono_literals ;
extern void qInitResources_mudlet ( ) ;
extern void qInitResources_qm ( ) ;
extern void qInitResources_additional_splash_screens ( ) ;
extern void qInitResources_mudlet_fonts_common ( ) ;
extern void qInitResources_mudlet_fonts_posix ( ) ;
static void initializeQRCResources ( )
{
# ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
qInitResources_additional_splash_screens ( ) ;
# endif
# ifdef INCLUDE_FONTS
qInitResources_mudlet_fonts_common ( ) ;
# if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
qInitResources_mudlet_fonts_posix ( ) ;
# endif
# endif
qInitResources_mudlet ( ) ;
qInitResources_qm ( ) ;
}
class ConnectionDialogCrashTest : public QObject
{
Q_OBJECT
private :
QTemporaryDir mXdgDir ;
QByteArray mSavedXdg ;
// not created in initTestCase(): the first test needs a profiles/ with
// nothing in it at all
const QString mProfileName = qsl ( " ConnDialogCrash-Test " ) ;
// what copyProfileWidget() names the copy of a name not ending in a digit
const QString mCopyName = qsl ( " ConnDialogCrash-Test1 " ) ;
const QString mQuietProfileName = qsl ( " ConnDialogCrash-Quiet " ) ;
const QString mQuietCopyName = qsl ( " ConnDialogCrash-Quiet1 " ) ;
static constexpr int scmMyGamesTab = 0 ;
static constexpr int scmAllGamesTab = 1 ;
static constexpr int scmTestMarkerRole = Qt : : UserRole + 99 ;
const QString mProfileUrl = qsl ( " mudlet.org " ) ;
const QString mProfilePort = qsl ( " 23 " ) ;
// setupConfig() consults portable.txt before the XDG logic, so its presence
// would put this test on the user's real config dir (see ConfigDirOverrideTest)
bool portableMarkerPresent ( ) const
{
return QFileInfo : : exists ( qsl ( " %1/portable.txt " ) . arg ( QCoreApplication : : applicationDirPath ( ) ) ) | | QFileInfo : : exists ( qsl ( " %1/.config/mudlet/portable.txt " ) . arg ( QDir : : homePath ( ) ) ) ;
}
// by name: QTabWidget gives the dialog a second QTabBar
QTabBar * gamesTabBar ( dlgConnectionProfiles * dialog ) const { return dialog - > findChild < QTabBar * > ( qsl ( " gamesTabBar " ) ) ; }
// members, not locals of the calling test: with no menu the timer is still
// armed when that method returns
QStringList mMenuActionTexts ;
bool mSawMenu = false ;
QString mUnexpectedPopup ;
// menu.exec() runs its own event loop, so the menu can only be inspected and
// dismissed from inside it; callers stop the timer when no menu appears
QTimer * armMenuCloser ( )
{
mMenuActionTexts . clear ( ) ;
mSawMenu = false ;
mUnexpectedPopup . clear ( ) ;
auto * closer = new QTimer ( this ) ;
closer - > setInterval ( 20 ) ;
connect ( closer , & QTimer : : timeout , this , [ this , closer ] ( ) {
auto * popup = QApplication : : activePopupWidget ( ) ;
if ( ! popup ) {
return ;
}
auto * menu = qobject_cast < QMenu * > ( popup ) ;
if ( ! menu ) {
// menu.exec() waits on whatever holds the popup, so close it
// rather than time the run out
mUnexpectedPopup = QString : : fromLatin1 ( popup - > metaObject ( ) - > className ( ) ) ;
popup - > close ( ) ;
closer - > stop ( ) ;
return ;
}
mSawMenu = true ;
const auto actions = menu - > actions ( ) ;
for ( const auto * action : actions ) {
mMenuActionTexts < < action - > text ( ) ;
}
menu - > close ( ) ;
closer - > stop ( ) ;
} ) ;
closer - > start ( ) ;
return closer ;
}
void disarmMenuCloser ( QTimer * closer )
{
closer - > stop ( ) ;
closer - > deleteLater ( ) ;
}
QString menuOutcome ( ) const { return mUnexpectedPopup . isEmpty ( ) ? QString ( ) : qsl ( " (a %1 took the popup instead) " ) . arg ( mUnexpectedPopup ) ; }
// Must go to the viewport: QAbstractScrollArea ignores a mouse-reason
// context menu sent to itself, and its viewportEvent() is what raises
// customContextMenuRequested.
void rightClickBelowTheLastItem ( QAbstractScrollArea * view ) const
{
auto * viewport = view - > viewport ( ) ;
const QPoint pos ( viewport - > width ( ) / 2 , viewport - > height ( ) - 4 ) ;
QContextMenuEvent event ( QContextMenuEvent : : Mouse , pos , viewport - > mapToGlobal ( pos ) ) ;
QApplication : : sendEvent ( viewport , & event ) ;
}
// reports instead of QVERIFYing: a QVERIFY here would only leave the helper
bool makeProfileFolder ( const QString & name ) const
{
return QDir ( ) . mkpath ( mudlet : : getMudletPath ( enums : : profileHomePath , name ) ) & & mudlet : : self ( ) - > writeProfileData ( name , qsl ( " url " ) , mProfileUrl ) . first
& & mudlet : : self ( ) - > writeProfileData ( name , qsl ( " port " ) , mProfilePort ) . first ;
}
private slots :
void initTestCase ( )
{
if ( portableMarkerPresent ( ) ) {
QSKIP ( " portable.txt present - cannot redirect the config dir for this test " ) ;
}
initializeQRCResources ( ) ;
mSavedXdg = qgetenv ( " XDG_CONFIG_HOME " ) ;
QVERIFY ( mXdgDir . isValid ( ) ) ;
2026-08-11 01:50:14 +02:00
QVERIFY ( QDir ( ) . mkpath ( qsl ( " %1/mudlet/profiles " ) . arg ( mXdgDir . path ( ) ) ) ) ; // profiles/ = XDG opt-in
Fix three crashes in the game selection screen (#9702)
#### Brief overview of PR changes/additions
- **Right-clicking the games list with nothing selected killed Mudlet.**
`dlgConnectionProfiles::slot_profileContextMenu()` dereferenced
`currentItem()` unguarded. That line is byte-identical in 4.22.0, so the
null deref itself is long-standing and latent - what is new is that it
became reachable: "improve: split the games list into My games and All
games tabs" (#9452) leaves a user with no saved profiles an empty but
still right-clickable "My games" tab, a state 4.22.0's always-populated
list never had. About 40 seconds into a fresh install.
- **Copying a profile while the list was rebuilt was a use-after-free.**
The copy runs on a thread pool and its completion handler kept the
`QListWidgetItem*` it had made; clicking the other games tab meanwhile
calls `fillout_form()`, which destroys every item. The handler now finds
the copy by name, and the `QFutureWatcher` is parented so it cannot
outlive the dialog.
- **Quitting before the connection dialog had been shown dereferenced
null.** The queued `0ms` lambda in `mudlet::slot_showConnectionDialog()`
used `mpConnectionDialog`, which `mudlet::closeEvent()` closes (it is
`WA_DeleteOnClose`) and clears.
#### Motivation for adding to Mudlet
All three came out of the 5.0 QA sweep and are confirmed with
AddressSanitizer. The first is the serious one - it is the default state
of a brand-new install, so a new user can lose Mudlet before they have
connected to anything.
Scope note on the third: it is **not** a 5.0 regression. It has been
there since "Fix: Improve tab indicators and detached window UX" (#7965)
and is unchanged in 4.22.0; #9493 only turned the literal `0` into
`0ms`. Nor could I reach it by clicking: I drove *Games -> Close
profile* followed by quitting at six delays from 0 to 2000 ms and the
dialog was always painted first. It reproduces deterministically
in-process, and QA reproduced it 2/2 driving the close from Lua. Worth
guarding - the pointer is documented to go null - but latent rather than
routinely hit.
#### Other info (issues closed, discussion etc)
Test case: `ctest -R ConnectionDialogCrashTest` - with the fix reverted,
four of its tests reproduce the original ASan reports exactly (two SEGVs
in `slot_profileContextMenu`, a heap-use-after-free in
`slot_itemClicked`, the SEGV in `QWidget::show()` from the lambda); two
more are controls that pass either way, one of them pinning that the
menu still opens for a selected profile so the guard cannot degenerate
into an unconditional early return. Full suite 79/79.
Assisted-by: Claude:claude-opus-5
2026-08-07 10:14:30 +02:00
qputenv ( " XDG_CONFIG_HOME " , mXdgDir . path ( ) . toUtf8 ( ) ) ;
mudlet : : start ( ) ;
mudlet : : self ( ) - > setupConfig ( ) ;
QVERIFY ( mudlet : : getMudletPath ( enums : : profilesPath ) . startsWith ( mXdgDir . path ( ) ) ) ;
mudlet : : self ( ) - > takeOwnershipOfInstanceCoordinator ( std : : make_unique < MudletInstanceCoordinator > ( " MudletInstanceCoordinator " ) ) ;
mudlet : : self ( ) - > init ( ) ;
mudlet : : self ( ) - > setStorePasswordsSecurely ( false ) ;
mudlet : : self ( ) - > startAutoLogin ( { } ) ;
// the dialog is only shown from a queued lambda, so the pointer turning
// up is not enough
QVERIFY ( QTest : : qWaitFor (
[ ] ( ) {
return mudlet : : self ( ) - > mpConnectionDialog & & mudlet : : self ( ) - > mpConnectionDialog - > isVisible ( ) ;
} ,
5000 ) ) ;
}
void cleanupTestCase ( )
{
mSavedXdg . isNull ( ) ? qunsetenv ( " XDG_CONFIG_HOME " ) : qputenv ( " XDG_CONFIG_HOME " , mSavedXdg ) ;
delete mudlet : : self ( ) ;
}
void test_rightClickWithNoProfileSelected ( )
{
auto * dialog = mudlet : : self ( ) - > mpConnectionDialog . data ( ) ;
QVERIFY2 ( dialog , " No connection dialog to test against " ) ;
auto * skipButton = dialog - > findChild < QPushButton * > ( qsl ( " skipToGamesButton " ) ) ;
QVERIFY2 ( skipButton , " The first-launch invitation has no skip button any more " ) ;
QVERIFY2 ( skipButton - > isVisible ( ) , " This is not a first-launch dialog - the skip button is not shown " ) ;
skipButton - > click ( ) ;
QTest : : qWait ( 100 ms ) ;
auto * tabBar = gamesTabBar ( dialog ) ;
QVERIFY2 ( tabBar , " The games list has no tab bar " ) ;
tabBar - > setCurrentIndex ( scmMyGamesTab ) ;
QTest : : qWait ( 100 ms ) ;
// not an empty list: a debug build still lists the self-test entry, but
// none of it is on disk so fillout_form() makes nothing current
QVERIFY2 ( ! dialog - > listWidget_profiles - > currentItem ( ) ,
qPrintable (
qsl ( " The 'My games' tab of a fresh install selected something (%1 items listed) - this test no longer covers the reported crash " ) . arg ( dialog - > listWidget_profiles - > count ( ) ) ) ) ;
auto * closer = armMenuCloser ( ) ;
rightClickBelowTheLastItem ( dialog - > listWidget_profiles ) ;
disarmMenuCloser ( closer ) ;
QVERIFY2 ( ! mSawMenu , " A context menu was offered with no profile for it to act on " ) ;
QVERIFY2 ( mUnexpectedPopup . isEmpty ( ) , qPrintable ( menuOutcome ( ) ) ) ;
QVERIFY2 ( ! QApplication : : activePopupWidget ( ) , " A popup was left on screen " ) ;
}
void test_rightClickOnAnEmptyList ( )
{
auto * dialog = mudlet : : self ( ) - > mpConnectionDialog . data ( ) ;
QVERIFY2 ( dialog , " No connection dialog to test against " ) ;
dialog - > listWidget_profiles - > clear ( ) ;
QCOMPARE ( dialog - > listWidget_profiles - > count ( ) , 0 ) ;
auto * closer = armMenuCloser ( ) ;
rightClickBelowTheLastItem ( dialog - > listWidget_profiles ) ;
disarmMenuCloser ( closer ) ;
QVERIFY2 ( ! mSawMenu , " A context menu was offered for an empty games list " ) ;
QVERIFY2 ( mUnexpectedPopup . isEmpty ( ) , qPrintable ( menuOutcome ( ) ) ) ;
dialog - > fillout_form ( ) ;
QTest : : qWait ( 100 ms ) ;
}
// so that "return early when nothing is current" cannot become "return early"
void test_contextMenuStillOpensForASelectedProfile ( )
{
auto * dialog = mudlet : : self ( ) - > mpConnectionDialog . data ( ) ;
QVERIFY2 ( dialog , " No connection dialog to test against " ) ;
auto * tabBar = gamesTabBar ( dialog ) ;
QVERIFY ( tabBar ) ;
tabBar - > setCurrentIndex ( scmAllGamesTab ) ;
QTest : : qWait ( 100 ms ) ;
QVERIFY2 ( dialog - > listWidget_profiles - > count ( ) > 0 , " The 'All games' tab lists nothing " ) ;
dialog - > listWidget_profiles - > setCurrentRow ( 0 ) ;
QVERIFY2 ( dialog - > listWidget_profiles - > currentItem ( ) , " Could not select a profile to open the menu for " ) ;
auto * closer = armMenuCloser ( ) ;
rightClickBelowTheLastItem ( dialog - > listWidget_profiles ) ;
disarmMenuCloser ( closer ) ;
QVERIFY2 ( mSawMenu , qPrintable ( qsl ( " No context menu appeared for a selected profile%1 " ) . arg ( menuOutcome ( ) ) ) ) ;
// "Set custom icon" and "Set custom color" for a profile without one
QCOMPARE ( mMenuActionTexts . size ( ) , 2 ) ;
QVERIFY2 ( ! mMenuActionTexts . first ( ) . isEmpty ( ) , " The menu offered a nameless action " ) ;
}
void test_copiedProfileSurvivesTheListBeingRebuilt ( )
{
auto * dialog = mudlet : : self ( ) - > mpConnectionDialog . data ( ) ;
QVERIFY2 ( dialog , " No connection dialog to test against " ) ;
QVERIFY ( makeProfileFolder ( mProfileName ) ) ;
QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mCopyName ) ) . removeRecursively ( ) ;
auto * tabBar = gamesTabBar ( dialog ) ;
QVERIFY ( tabBar ) ;
tabBar - > setCurrentIndex ( scmMyGamesTab ) ;
dialog - > fillout_form ( ) ;
// clear slot_itemClicked()'s 100ms same-profile debounce, which would
// otherwise leave the form blank after fillout_form() cleared it
QTest : : qWait ( 300 ms ) ;
const auto items = dialog - > findData ( * dialog - > listWidget_profiles , mProfileName , dlgConnectionProfiles : : csmNameRole ) ;
QVERIFY2 ( ! items . isEmpty ( ) , " The test profile is not listed in the dialog " ) ;
dialog - > listWidget_profiles - > setCurrentItem ( items . first ( ) ) ;
dialog - > slot_itemClicked ( items . first ( ) ) ;
QCOMPARE ( dialog - > profile_name_entry - > text ( ) , mProfileName ) ;
auto * copyAction = dialog - > findChild < QAction * > ( qsl ( " copyProfile " ) ) ;
QVERIFY2 ( copyAction , " The dialog has no Copy action any more " ) ;
dialog - > slot_copyProfile ( ) ;
QVERIFY2 ( ! copyAction - > isEnabled ( ) , " The copy did not take the asynchronous path " ) ;
// the copy reports back through the event loop, which has not run since,
// so this destroys the copy's item before the handler sees it
tabBar - > setCurrentIndex ( scmAllGamesTab ) ;
QVERIFY2 ( QTest : : qWaitFor (
[ copyAction ] ( ) {
return copyAction - > isEnabled ( ) ;
} ,
15000 ) ,
" The copy never completed " ) ;
QVERIFY2 ( QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mCopyName ) ) . exists ( ) , " The copy has no folder on disk " ) ;
QCOMPARE ( dialog - > readProfileData ( mCopyName , qsl ( " url " ) ) , mProfileUrl ) ;
QCOMPARE ( dialog - > readProfileData ( mCopyName , qsl ( " port " ) ) , mProfilePort ) ;
QVERIFY2 ( ! dialog - > findData ( * dialog - > listWidget_profiles , mCopyName , dlgConnectionProfiles : : csmNameRole ) . isEmpty ( ) , " The copy is not listed in the games list " ) ;
// No assertions on the form fields: a copy completes inside
// slot_itemClicked()'s 100ms debounce, which swallows the fill and
// leaves Server address and Port blank - a separate bug.
auto * pCurrentItem = dialog - > listWidget_profiles - > currentItem ( ) ;
QVERIFY2 ( pCurrentItem , " Nothing is selected after the copy finished " ) ;
QCOMPARE ( pCurrentItem - > data ( dlgConnectionProfiles : : csmNameRole ) . toString ( ) , mCopyName ) ;
QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mCopyName ) ) . removeRecursively ( ) ;
QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mProfileName ) ) . removeRecursively ( ) ;
dialog - > fillout_form ( ) ;
QTest : : qWait ( 100 ms ) ;
}
// the branch where the copy's item is still there and still current
void test_copiedProfileIsSelectedWhenTheListIsLeftAlone ( )
{
auto * dialog = mudlet : : self ( ) - > mpConnectionDialog . data ( ) ;
QVERIFY2 ( dialog , " No connection dialog to test against " ) ;
QVERIFY ( makeProfileFolder ( mQuietProfileName ) ) ;
QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mQuietCopyName ) ) . removeRecursively ( ) ;
auto * tabBar = gamesTabBar ( dialog ) ;
QVERIFY ( tabBar ) ;
tabBar - > setCurrentIndex ( scmMyGamesTab ) ;
dialog - > fillout_form ( ) ;
QTest : : qWait ( 300 ms ) ;
const auto items = dialog - > findData ( * dialog - > listWidget_profiles , mQuietProfileName , dlgConnectionProfiles : : csmNameRole ) ;
QVERIFY2 ( ! items . isEmpty ( ) , " The test profile is not listed in the dialog " ) ;
dialog - > listWidget_profiles - > setCurrentItem ( items . first ( ) ) ;
dialog - > slot_itemClicked ( items . first ( ) ) ;
QCOMPARE ( dialog - > profile_name_entry - > text ( ) , mQuietProfileName ) ;
auto * copyAction = dialog - > findChild < QAction * > ( qsl ( " copyProfile " ) ) ;
QVERIFY ( copyAction ) ;
dialog - > slot_copyProfile ( ) ;
QVERIFY2 ( ! copyAction - > isEnabled ( ) , " The copy did not take the asynchronous path " ) ;
const auto created = dialog - > findData ( * dialog - > listWidget_profiles , mQuietCopyName , dlgConnectionProfiles : : csmNameRole ) ;
QVERIFY2 ( ! created . isEmpty ( ) , " The copy got no entry in the list " ) ;
// a mark outlives a rebuild check; a pointer would have been freed by one
created . first ( ) - > setData ( scmTestMarkerRole , true ) ;
QVERIFY2 ( QTest : : qWaitFor (
[ copyAction ] ( ) {
return copyAction - > isEnabled ( ) ;
} ,
15000 ) ,
" The copy never completed " ) ;
QVERIFY2 ( QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mQuietCopyName ) ) . exists ( ) , " The copy has no folder on disk " ) ;
QCOMPARE ( dialog - > readProfileData ( mQuietCopyName , qsl ( " url " ) ) , mProfileUrl ) ;
QCOMPARE ( dialog - > readProfileData ( mQuietCopyName , qsl ( " port " ) ) , mProfilePort ) ;
auto * pCurrentItem = dialog - > listWidget_profiles - > currentItem ( ) ;
QVERIFY2 ( pCurrentItem , " Nothing is selected after the copy finished " ) ;
QCOMPARE ( pCurrentItem - > data ( dlgConnectionProfiles : : csmNameRole ) . toString ( ) , mQuietCopyName ) ;
QVERIFY2 ( pCurrentItem - > data ( scmTestMarkerRole ) . toBool ( ) , " The list was rebuilt after all - this test no longer covers the undisturbed branch " ) ;
QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mQuietCopyName ) ) . removeRecursively ( ) ;
QDir ( mudlet : : getMudletPath ( enums : : profileHomePath , mQuietProfileName ) ) . removeRecursively ( ) ;
dialog - > fillout_form ( ) ;
QTest : : qWait ( 100 ms ) ;
}
// Must stay last: leaves the main window hidden and no connection dialog,
// both of which the other tests need.
void test_connectionDialogClosedBeforeItIsShown ( )
{
auto * mudletApp = mudlet : : self ( ) ;
if ( mudletApp - > mpConnectionDialog ) {
mudletApp - > mpConnectionDialog - > close ( ) ;
mudletApp - > mpConnectionDialog = nullptr ;
QTest : : qWait ( 200 ms ) ;
}
QVERIFY2 ( ! mudletApp - > mpConnectionDialog , " Could not get rid of the connection dialog this test starts from " ) ;
mudletApp - > slot_showConnectionDialog ( ) ;
QVERIFY2 ( mudletApp - > mpConnectionDialog , " No connection dialog was created " ) ;
// what closeEvent() does, with the event loop not having run since
// slot_showConnectionDialog() queued its lambda
QVERIFY2 ( mudletApp - > isVisible ( ) , " The main window has to start out visible for the hide() below to mean anything " ) ;
mudletApp - > mpConnectionDialog - > close ( ) ;
mudletApp - > mpConnectionDialog = nullptr ;
mudletApp - > hide ( ) ;
QVERIFY2 ( ! mudletApp - > isVisible ( ) , " The main window did not hide " ) ;
QTest : : qWait ( 300 ms ) ; // the queued lambda gets its turn in here
QVERIFY2 ( ! mudletApp - > mpConnectionDialog , " The queued lambda brought the connection dialog back " ) ;
QVERIFY2 ( ! mudletApp - > isVisible ( ) , " The queued lambda re-showed the main window Mudlet was shutting down " ) ;
}
} ;
QTEST_MAIN ( ConnectionDialogCrashTest )
# include "ConnectionDialogCrashTest.moc"