mudlet/test/functional_tests/EmbeddedMapperCreationTest.cpp
Vadim Peretokin 13ac7da839
fix: mapper scripts stopped being told the map had opened (#9709)
#### Brief overview of PR changes/additions
- `createMapper()` raises `mapOpenEvent` again when the profile's map is
already loaded, and repopulates/reselects the mapper's area dropdown on
that path
- The redundant second `map->restore()` that `2a3334a6a` ("Fix: don't
load a map if trying to create a mapper & map is already loaded
(#9415)") was written to skip is still skipped - only the event raise
and the combo box setup move back out of its guard, matching the shape
`Host::createMapper()` has always had
- New `EmbeddedMapperCreationTest` functional test covering both sides
of the branch; the busted suite structurally cannot, since an embedded
mapper and the dockable map widget are mutually exclusive for the life
of a profile

#### Motivation for adding to Mudlet
Every returning user has a saved map, so `createMapper()` and
`Geyser.Mapper{embedded = true}` hit exactly this path, and third-party
mapping scripts that finish setting themselves up on `mapOpenEvent`
silently stopped running.

#### Other info (issues closed, discussion etc)
Found in the 5.0 QA sweep (finding C4), verified on two pristine
profiles: 0 rooms raises the event once, 1 room raised it zero times.
The same guard also swallowed
`updateAreaComboBox()`/`resetAreaComboBoxToPlayerRoomArea()`, which left
the area dropdown reading "Default Area" while the player room was
elsewhere.


Assisted-by: Claude:claude-opus-5

**Test case:** on a profile with a saved map, run
`registerAnonymousEventHandler("mapOpenEvent", function()
echo("\nmapOpenEvent fired\n") end) createMapper(0, 0, 400, 400)` - the
echo appears, and the mapper's area dropdown shows the player's area.
2026-08-07 06:20:08 +02:00

203 lines
8 KiB
C++

/***************************************************************************
* 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. *
***************************************************************************/
/*
* Covers TMainConsole::createMapper() - the embedded mapper behind Lua
* createMapper() and Geyser.Mapper{embedded = true} - on both sides of its
* already-loaded-map branch.
*
* An embedded mapper and the dockable map widget are mutually exclusive for the
* life of a profile and neither can be destroyed, so the busted suite cannot go
* here and each test method needs a mudlet of its own.
*/
#include <QSignalSpy>
#include <QtTest/QtTest>
#include <chrono>
#include "Host.h"
#include "MudletInstanceCoordinator.h"
#include "TMainConsole.h"
#include "TMap.h"
#include "TRoomDB.h"
#include "TelnetServerStub.h"
#include "ctelnet.h"
#include "dlgConnectionProfiles.h"
#include "dlgMapper.h"
#include "mudlet.h"
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();
void initializeQRCResourcesForEmbeddedMapperTest();
class EmbeddedMapperCreationTest : public QObject
{
Q_OBJECT
private:
TelnetServerStub* mpServer = nullptr;
Host* mpHost = nullptr;
const QString mHostname = qsl("Embedded-Mapper-Test-Host");
QString mPort;
const QString mLocalhost = qsl("localhost");
const QString mFirstAreaName = qsl("AAArea");
const QString mPlayerAreaName = qsl("QAArea");
private slots:
void initTestCase() { initializeQRCResourcesForEmbeddedMapperTest(); }
void init()
{
mpServer = new TelnetServerStub(qApp);
mpServer->start(mLocalhost, 0);
mPort = QString::number(mpServer->serverPort());
mudlet::start();
mudlet::self()->setupConfig();
mudlet::self()->takeOwnershipOfInstanceCoordinator(std::make_unique<MudletInstanceCoordinator>("MudletInstanceCoordinator"));
mudlet::self()->init();
mudlet::self()->setStorePasswordsSecurely(false);
deleteProfileDirectory();
QTimer::singleShot(0ms, qApp, [this]() {
mudlet::self()->startAutoLogin({});
QTest::qWait(100ms);
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button, Qt::LeftButton);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), mHostname);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), mLocalhost);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
QTest::qWait(100ms);
QTest::keyClicks(QApplication::focusWidget(), mPort);
QTest::qWait(100ms);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
});
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
if (!spy.wait(5000)) {
QFAIL("Profile took too long to load.");
}
mpHost = mudlet::self()->getActiveHost();
if (!mpHost) {
QFAIL("No active host available for the test.");
}
QSignalSpy connectionSpy(&(mpHost->mTelnet), &cTelnet::signal_connected);
if (!connectionSpy.wait(2000)) {
QFAIL("Could not connect with the host.");
}
QVERIFY(mpHost->mpConsole);
watchMapOpenEvent();
}
void cleanup()
{
delete mpServer;
mpServer = nullptr;
mpHost = nullptr;
deleteProfileDirectory();
delete mudlet::self();
}
void test_createMapperWithALoadedMap()
{
TMap* pMap = mpHost->mpMap.data();
TRoomDB* pRoomDB = pMap->mpRoomDB.get();
// the player's area has to sort after the one the dlgMapper constructor's own fill leaves selected
QVERIFY(pRoomDB->addArea(mFirstAreaName) > 0);
const int playerAreaId = pRoomDB->addArea(mPlayerAreaName);
QVERIFY(playerAreaId > 0);
QVERIFY(pMap->addRoom(1));
QVERIFY(pMap->setRoomArea(1, playerAreaId, false));
pMap->mRoomIdHash[pMap->mProfileName] = 1;
pMap->setDefaultAreaShown(false);
QVERIFY2(!pRoomDB->isEmpty(), "the map has to be non-empty for this to be the returning-user path");
auto [created, message] = mpHost->mpConsole->createMapper(QString(), 0, 0, 300, 300);
QVERIFY2(created, qPrintable(message));
QVERIFY(mpHost->mpConsole->mpMapper);
QVERIFY2(mapOpenEventCountIs(1), "createMapper() did not raise mapOpenEvent exactly once for an already-loaded map");
auto* pComboBox = mpHost->mpConsole->mpMapper->comboBox_showArea;
QCOMPARE(pComboBox->count(), 2); // the hidden default area is still in the constructor's fill
QCOMPARE(pComboBox->currentText(), mPlayerAreaName);
// Geyser.Mapper re-runs createMapper() on every reposition
auto [recreated, recreateMessage] = mpHost->mpConsole->createMapper(QString(), 0, 0, 300, 300);
QVERIFY2(recreated, qPrintable(recreateMessage));
QVERIFY2(mapOpenEventCountIs(1), "a repeat createMapper() raised mapOpenEvent again");
}
void test_createMapperWithNoMapToLoad()
{
QVERIFY2(mpHost->mpMap->mpRoomDB->isEmpty(), "a freshly created profile was expected to have no rooms");
auto [created, message] = mpHost->mpConsole->createMapper(QString(), 0, 0, 300, 300);
QVERIFY2(created, qPrintable(message));
QVERIFY(mpHost->mpConsole->mpMapper);
QVERIFY2(mapOpenEventCountIs(1), "createMapper() did not raise mapOpenEvent exactly once for a first-run profile");
}
private:
void watchMapOpenEvent()
{
mpHost->getLuaInterpreter()->compileAndExecuteScript(qsl("mapOpenSeen = 0\n"
"registerAnonymousEventHandler('mapOpenEvent', function() mapOpenSeen = mapOpenSeen + 1 end)"));
}
bool mapOpenEventCountIs(const int expected) const { return mpHost->getLuaInterpreter()->compileAndExecuteScript(qsl("assert(mapOpenSeen == %1)").arg(expected)); }
void deleteProfileDirectory() const
{
QDir dir(mudlet::getMudletPath(enums::profileHomePath, mHostname));
if (dir.exists()) {
dir.removeRecursively();
}
}
};
void initializeQRCResourcesForEmbeddedMapperTest()
{
#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();
}
#include "EmbeddedMapperCreationTest.moc"
QTEST_MAIN(EmbeddedMapperCreationTest)