mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions While editing an alias, Mudlet autosaves each field as you finish it (the per-property "autosave" path, separate from the explicit Save button). That path was missing two safety checks the Save button already performs: - **Invalid patterns were stored silently.** Typing a pattern that fails to compile left no error icon and no message, so a broken alias was saved with no feedback. Autosave now flags it with the error icon and shows the same faulty-regex message as Save. - **Infinite-loop aliases were accepted.** An alias whose command matches its own pattern calls itself forever. Save rejects this; autosave did not. The loop guard now runs on both the pattern and the command field. Shared logic between the two paths was factored into small helpers (`aliasSubstitutionLoops`, `computeAliasIcon`, `setAliasNormalIcon`, `showAliasError`, `showAliasLoopWarning`, `applyAliasState`) so the autosave and explicit-save paths stay in sync. A freshly added alias keeps its "unsaved" cue until an explicit Save, so autosave no longer changes its activation state. Added four functional tests to `dlgTriggerEditorUndoRedoTest` covering: invalid-regex flagging and recovery, loop rejection from both the command and the pattern field, and error clearing after a fix. #### Motivation for adding to Mudlet Users typing an invalid or self-looping alias got no feedback and ended up with a broken, silently-stored alias. This brings the as-you-type autosave to parity with the Save button. #### Other info (issues closed, discussion etc) Relates to #8469. Assisted-by: Claude:claude-opus-4-8
2401 lines
82 KiB
C++
2401 lines
82 KiB
C++
/***************************************************************************
|
|
* Copyright (C) 2025 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. *
|
|
***************************************************************************/
|
|
|
|
#include <QtTest/QtTest>
|
|
#include <chrono>
|
|
|
|
#include "EditorUndoStack.h"
|
|
#include "Host.h"
|
|
#include "MudletInstanceCoordinator.h"
|
|
#include "TAction.h"
|
|
#include "TAlias.h"
|
|
#include "TKey.h"
|
|
#include "TTimer.h"
|
|
#include "TTreeWidget.h"
|
|
#include "TTrigger.h"
|
|
#include "TelnetServerStub.h"
|
|
#include "ctelnet.h"
|
|
#include "dlgActionMainArea.h"
|
|
#include "dlgAliasMainArea.h"
|
|
#include "dlgConnectionProfiles.h"
|
|
#include "dlgTimersMainArea.h"
|
|
#include "dlgTriggerEditor.h"
|
|
#include "dlgTriggerPatternEdit.h"
|
|
#include "dlgTriggersMainArea.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();
|
|
|
|
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 dlgTriggerEditorUndoRedoTest : public QObject {
|
|
Q_OBJECT
|
|
|
|
private:
|
|
TelnetServerStub *mpServer = nullptr;
|
|
dlgTriggerEditor *mpEditor = nullptr;
|
|
Host *mpHost = nullptr;
|
|
const QString mProfileName = qsl("UndoRedo-Test-Profile");
|
|
QString mPort; // assigned the stub's actual ephemeral port in initTestCase()
|
|
const QString mLocalhost = qsl("localhost");
|
|
|
|
struct ItemTypeInfo {
|
|
QString name;
|
|
EditorViewType viewType;
|
|
std::function<void()> showView;
|
|
std::function<void()> addItem;
|
|
std::function<void()> addFolder;
|
|
std::function<QTreeWidgetItem *()> getBaseItem;
|
|
std::function<TTreeWidget *()> getTreeWidget;
|
|
QString newItemText;
|
|
QString newFolderText;
|
|
|
|
QTreeWidgetItem *baseItem() const { return getBaseItem(); }
|
|
TTreeWidget *treeWidget() const { return getTreeWidget(); }
|
|
};
|
|
|
|
std::vector<ItemTypeInfo> mItemTypes;
|
|
|
|
void cleanupAll(const ItemTypeInfo &itemType) {
|
|
itemType.treeWidget()->clearSelection();
|
|
itemType.treeWidget()->setCurrentItem(nullptr);
|
|
QCoreApplication::processEvents();
|
|
|
|
while (itemType.baseItem()->childCount() > 0) {
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem()->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
}
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
void deleteProfileDirectory(const QString &profileName) {
|
|
const QString path =
|
|
mudlet::getMudletPath(enums::profileHomePath, profileName);
|
|
QDir dir(path);
|
|
if (dir.exists()) {
|
|
dir.removeRecursively();
|
|
}
|
|
}
|
|
|
|
void startProfile(const QString &profileName, const QString &address,
|
|
const QString &port) {
|
|
QTimer::singleShot(0ms, qApp, [profileName, address, port]() {
|
|
mudlet::self()->startAutoLogin({});
|
|
QTest::qWait(100ms);
|
|
|
|
// Verify connection dialog is available before UI interactions
|
|
Q_ASSERT_X(mudlet::self()->mpConnectionDialog, "startProfile",
|
|
"Connection dialog not initialized");
|
|
Q_ASSERT_X(mudlet::self()->mpConnectionDialog->new_profile_button,
|
|
"startProfile", "New profile button not found");
|
|
|
|
QTest::mouseClick(mudlet::self()->mpConnectionDialog->new_profile_button,
|
|
Qt::LeftButton);
|
|
QTest::qWait(100ms);
|
|
|
|
Q_ASSERT_X(QApplication::focusWidget(), "startProfile",
|
|
"No widget has focus after clicking new profile button");
|
|
|
|
QTest::keyClicks(QApplication::focusWidget(), profileName);
|
|
QTest::qWait(100ms);
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
|
QTest::qWait(100ms);
|
|
QTest::keyClicks(QApplication::focusWidget(), address);
|
|
QTest::qWait(100ms);
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
|
QTest::qWait(100ms);
|
|
QTest::keyClicks(QApplication::focusWidget(), port);
|
|
QTest::qWait(100ms);
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return);
|
|
});
|
|
|
|
QSignalSpy spy(mudlet::self(), &mudlet::signal_profileLoaded);
|
|
if (!spy.wait(2000)) {
|
|
QFAIL("Profile took too long to load.");
|
|
}
|
|
|
|
mpHost = mudlet::self()->getActiveHost();
|
|
if (!mpHost) {
|
|
QFAIL("No active host available for the test.");
|
|
}
|
|
|
|
QSignalSpy spy2(&(mpHost->mTelnet), &cTelnet::signal_connected);
|
|
if (!spy2.wait(1000)) {
|
|
QFAIL("Could not connect with the host.");
|
|
}
|
|
}
|
|
|
|
private slots:
|
|
void initTestCase() {
|
|
initializeQRCResources();
|
|
|
|
mpServer = new TelnetServerStub(qApp);
|
|
mpServer->start(mLocalhost, 0); // ephemeral OS-assigned port avoids collisions across concurrent test runs
|
|
QVERIFY2(mpServer->isListening(),
|
|
qPrintable(qsl("TelnetServerStub failed to start: %1")
|
|
.arg(mpServer->errorString())));
|
|
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(mProfileName);
|
|
startProfile(mProfileName, mLocalhost, mPort);
|
|
|
|
// Open the editor dialog (it's created lazily)
|
|
mudlet::self()->slot_showScriptDialog();
|
|
QTest::qWait(100ms);
|
|
|
|
mpEditor = mpHost->mpEditorDialog;
|
|
QVERIFY2(mpEditor != nullptr, "Editor dialog should be created");
|
|
QVERIFY2(mpEditor->mpUndoStack != nullptr, "Undo stack should exist");
|
|
|
|
mItemTypes = {{qsl("Trigger"), EditorViewType::cmTriggerView,
|
|
[this]() { mpEditor->slot_showTriggers(); },
|
|
[this]() { mpEditor->addTrigger(false); },
|
|
[this]() { mpEditor->addTrigger(true); },
|
|
[this]() { return mpEditor->mpTriggerBaseItem; },
|
|
[this]() { return mpEditor->treeWidget_triggers; },
|
|
qsl("New trigger"), qsl("New trigger group")},
|
|
{qsl("Timer"), EditorViewType::cmTimerView,
|
|
[this]() { mpEditor->slot_showTimers(); },
|
|
[this]() { mpEditor->addTimer(false); },
|
|
[this]() { mpEditor->addTimer(true); },
|
|
[this]() { return mpEditor->mpTimerBaseItem; },
|
|
[this]() { return mpEditor->treeWidget_timers; },
|
|
qsl("New timer"), qsl("New timer group")},
|
|
{qsl("Alias"), EditorViewType::cmAliasView,
|
|
[this]() { mpEditor->slot_showAliases(); },
|
|
[this]() { mpEditor->addAlias(false); },
|
|
[this]() { mpEditor->addAlias(true); },
|
|
[this]() { return mpEditor->mpAliasBaseItem; },
|
|
[this]() { return mpEditor->treeWidget_aliases; },
|
|
qsl("New alias"), qsl("New alias group")},
|
|
{qsl("Script"), EditorViewType::cmScriptView,
|
|
[this]() { mpEditor->slot_showScripts(); },
|
|
[this]() { mpEditor->addScript(false); },
|
|
[this]() { mpEditor->addScript(true); },
|
|
[this]() { return mpEditor->mpScriptsBaseItem; },
|
|
[this]() { return mpEditor->treeWidget_scripts; },
|
|
qsl("New script"), qsl("New script group")},
|
|
{qsl("Key"), EditorViewType::cmKeysView,
|
|
[this]() { mpEditor->slot_showKeys(); },
|
|
[this]() { mpEditor->addKey(false); },
|
|
[this]() { mpEditor->addKey(true); },
|
|
[this]() { return mpEditor->mpKeyBaseItem; },
|
|
[this]() { return mpEditor->treeWidget_keys; },
|
|
qsl("New key"), qsl("New key group")},
|
|
{qsl("Action"), EditorViewType::cmActionView,
|
|
[this]() { mpEditor->slot_showActions(); },
|
|
[this]() { mpEditor->addAction(false); },
|
|
[this]() { mpEditor->addAction(true); },
|
|
[this]() { return mpEditor->mpActionBaseItem; },
|
|
[this]() { return mpEditor->treeWidget_actions; },
|
|
qsl("New button"), qsl("New button group")}};
|
|
}
|
|
|
|
void cleanupTestCase() {
|
|
mItemTypes.clear();
|
|
mpEditor = nullptr;
|
|
mpHost = nullptr;
|
|
delete mpServer;
|
|
mpServer = nullptr;
|
|
deleteProfileDirectory(mProfileName);
|
|
delete mudlet::self();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 1: Core Operations - Single Items
|
|
// ========================================================================
|
|
void testCoreOperations_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testCoreOperations() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Add item → undo → redo
|
|
{
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
itemType.addItem();
|
|
QVERIFY2(itemType.baseItem()->childCount() > initialCount,
|
|
qPrintable(itemTypeName + ": Item should be added"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QVERIFY2(
|
|
itemType.baseItem()->childCount() > initialCount,
|
|
qPrintable(itemTypeName + ": Item should be restored after redo"));
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
// Test: Add folder → undo → redo
|
|
{
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
itemType.addFolder();
|
|
QVERIFY2(itemType.baseItem()->childCount() > initialCount,
|
|
qPrintable(itemTypeName + ": Folder should be added"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QVERIFY2(
|
|
itemType.baseItem()->childCount() > initialCount,
|
|
qPrintable(itemTypeName + ": Folder should be restored after redo"));
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
// Test: Delete item → undo → redo
|
|
{
|
|
itemType.addItem();
|
|
QTreeWidgetItem *item = itemType.baseItem()->child(0);
|
|
QVERIFY(item != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(item);
|
|
int countBeforeDelete = itemType.baseItem()->childCount();
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
QVERIFY2(itemType.baseItem()->childCount() < countBeforeDelete,
|
|
qPrintable(itemTypeName + ": Item should be deleted"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(itemType.baseItem()->childCount(), countBeforeDelete);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QVERIFY2(itemType.baseItem()->childCount() < countBeforeDelete,
|
|
qPrintable(itemTypeName + ": Delete should be redone"));
|
|
}
|
|
|
|
// Test: Delete empty folder → undo → redo
|
|
{
|
|
itemType.addFolder();
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY(folder != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
int countBeforeDelete = itemType.baseItem()->childCount();
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
QVERIFY2(itemType.baseItem()->childCount() < countBeforeDelete,
|
|
qPrintable(itemTypeName + ": Empty folder should be deleted"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(itemType.baseItem()->childCount(), countBeforeDelete);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QVERIFY2(
|
|
itemType.baseItem()->childCount() < countBeforeDelete,
|
|
qPrintable(itemTypeName + ": Empty folder delete should be redone"));
|
|
}
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 2: Parent-Only Selection
|
|
// ========================================================================
|
|
void testParentOnlySelection_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testParentOnlySelection() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Delete parent with children → undo
|
|
{
|
|
itemType.addFolder();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
QThread::sleep(10ms);
|
|
}
|
|
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY2(folder != nullptr,
|
|
qPrintable(itemTypeName + ": Folder should be created"));
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
QCOMPARE(folder->childCount(), 2);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
int totalCountBefore = itemType.baseItem()->childCount();
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
QVERIFY2(itemType.baseItem()->childCount() < totalCountBefore,
|
|
qPrintable(itemTypeName +
|
|
": Parent with children should be deleted"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restoredFolder = nullptr;
|
|
for (int i = 0; i < itemType.baseItem()->childCount(); i++) {
|
|
QTreeWidgetItem *item = itemType.baseItem()->child(i);
|
|
if (item->childCount() == 2) {
|
|
restoredFolder = item;
|
|
break;
|
|
}
|
|
}
|
|
|
|
QVERIFY2(restoredFolder != nullptr && restoredFolder->childCount() == 2,
|
|
qPrintable(itemTypeName +
|
|
": Parent and children should be restored"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Toggle parent off with active children → undo
|
|
{
|
|
itemType.addFolder();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
QThread::sleep(10ms);
|
|
}
|
|
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY2(folder != nullptr,
|
|
qPrintable(itemTypeName +
|
|
": Folder should be created for toggle test"));
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
QCOMPARE(folder->childCount(), 1);
|
|
|
|
QTreeWidgetItem *child = folder->child(0);
|
|
|
|
// Activate parent and child
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
mpEditor->slot_toggleItemOrGroupActiveFlag();
|
|
itemType.treeWidget()->setCurrentItem(child);
|
|
mpEditor->slot_toggleItemOrGroupActiveFlag();
|
|
|
|
// Now toggle parent off
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
mpEditor->slot_toggleItemOrGroupActiveFlag();
|
|
|
|
// Undo toggle
|
|
mpEditor->mpUndoStack->undo();
|
|
QVERIFY2(true, qPrintable(itemTypeName + ": Toggle undo should work"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Multi-level hierarchy delete (grandparent -> parent -> child)
|
|
{
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
|
|
// Create grandparent folder at root
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem());
|
|
itemType.addFolder();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
QThread::sleep(10ms);
|
|
}
|
|
|
|
QTreeWidgetItem *grandparent = itemType.baseItem()->child(0);
|
|
QVERIFY2(
|
|
grandparent != nullptr,
|
|
qPrintable(itemTypeName + ": Grandparent folder should be created"));
|
|
|
|
// Add parent folder under grandparent
|
|
itemType.treeWidget()->setCurrentItem(grandparent);
|
|
itemType.addFolder();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
QTreeWidgetItem *parent = grandparent->child(0);
|
|
QVERIFY2(parent != nullptr,
|
|
qPrintable(itemTypeName + ": Parent folder should be created"));
|
|
|
|
// Add child under parent
|
|
itemType.treeWidget()->setCurrentItem(parent);
|
|
itemType.addItem();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
QCOMPARE(parent->childCount(), 1);
|
|
|
|
// Delete grandparent (should delete 2 levels of children)
|
|
itemType.treeWidget()->setCurrentItem(grandparent);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
// Check if entire hierarchy restored
|
|
QTreeWidgetItem *restoredGP = itemType.baseItem()->child(0);
|
|
QVERIFY2(restoredGP != nullptr && restoredGP->childCount() == 1,
|
|
qPrintable(itemTypeName +
|
|
": Grandparent with parent should be restored"));
|
|
|
|
QTreeWidgetItem *restoredP = restoredGP->child(0);
|
|
QVERIFY2(
|
|
restoredP != nullptr && restoredP->childCount() == 1,
|
|
qPrintable(itemTypeName + ": Parent with child should be restored"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 3: Parent + All Children Selected (Multi-selection)
|
|
// ========================================================================
|
|
void testMultiSelection_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testMultiSelection() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Delete parent+children selected → verify single operation
|
|
{
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem());
|
|
itemType.addFolder();
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY(folder != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
|
|
QTreeWidgetItem *child1 = folder->child(0);
|
|
QTreeWidgetItem *child2 = folder->child(1);
|
|
|
|
QVERIFY2(folder->childCount() == 2 && child1 && child2 &&
|
|
child1 != child2,
|
|
qPrintable(itemTypeName + ": Should have 2 distinct children"));
|
|
|
|
QList<QTreeWidgetItem *> items;
|
|
items << folder << child1 << child2;
|
|
itemType.treeWidget()->clearSelection();
|
|
for (auto *item : std::as_const(items)) {
|
|
item->setSelected(true);
|
|
}
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
|
|
int stackCountBefore = mpEditor->mpUndoStack->count();
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
int stackCountAfter = mpEditor->mpUndoStack->count();
|
|
|
|
QVERIFY2(
|
|
stackCountAfter <= stackCountBefore + 1,
|
|
qPrintable(
|
|
itemTypeName +
|
|
": Delete parent+children should create single/batched command"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restored = itemType.baseItem()->child(0);
|
|
QVERIFY2(restored != nullptr && restored->childCount() == 2,
|
|
qPrintable(itemTypeName + ": Single undo should restore all"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Toggle parent+children selected → verify single operation
|
|
{
|
|
itemType.addFolder();
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY(folder != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
|
|
QCOMPARE(folder->childCount(), 1);
|
|
|
|
// Select all items (parent and child)
|
|
QList<QTreeWidgetItem *> items;
|
|
items << folder << folder->child(0);
|
|
itemType.treeWidget()->clearSelection();
|
|
for (auto *item : std::as_const(items)) {
|
|
item->setSelected(true);
|
|
}
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
|
|
int stackCountBefore = mpEditor->mpUndoStack->count();
|
|
mpEditor->slot_toggleItemOrGroupActiveFlag();
|
|
int stackCountAfter = mpEditor->mpUndoStack->count();
|
|
|
|
// Should be batched (single command or small increment)
|
|
QVERIFY2(
|
|
stackCountAfter <= stackCountBefore + 1,
|
|
qPrintable(
|
|
itemTypeName +
|
|
": Toggle parent+children should create single/batched command"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QVERIFY2(true, qPrintable(itemTypeName +
|
|
": Single undo should restore toggle state"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 4: ID Remapping
|
|
// ========================================================================
|
|
void testIdRemapping_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testIdRemapping() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Delete → undo (verify new ID assigned)
|
|
{
|
|
itemType.addItem();
|
|
QTreeWidgetItem *item = itemType.baseItem()->child(0);
|
|
QVERIFY(item != nullptr);
|
|
|
|
int originalID = item->data(0, Qt::UserRole).toInt();
|
|
|
|
itemType.treeWidget()->setCurrentItem(item);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restoredItem = itemType.baseItem()->child(0);
|
|
QVERIFY2(restoredItem != nullptr,
|
|
qPrintable(itemTypeName + ": Item should be restored"));
|
|
|
|
// ID may or may not be remapped depending on implementation
|
|
int newID = restoredItem->data(0, Qt::UserRole).toInt();
|
|
QVERIFY2(newID > 0, qPrintable(itemTypeName +
|
|
": Restored item should have valid ID"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Delete → undo → redo → undo chain
|
|
{
|
|
itemType.addItem();
|
|
QTreeWidgetItem *item = itemType.baseItem()->child(0);
|
|
QVERIFY(item != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(item);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->redo();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QVERIFY2(itemType.baseItem()->childCount() > 0,
|
|
qPrintable(itemTypeName + ": Undo/redo chain should work"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Delete parent with children → verify all IDs remapped
|
|
{
|
|
itemType.addFolder();
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY(folder != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
|
|
QCOMPARE(folder->childCount(), 2);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restoredFolder = itemType.baseItem()->child(0);
|
|
QVERIFY2(restoredFolder != nullptr && restoredFolder->childCount() == 2,
|
|
qPrintable(itemTypeName +
|
|
": Parent and children IDs should be remapped"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Commands in stack updated with new IDs
|
|
{
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem());
|
|
itemType.addItem();
|
|
QTreeWidgetItem *item1 = itemType.baseItem()->child(0);
|
|
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem());
|
|
itemType.addItem();
|
|
|
|
QVERIFY(itemType.baseItem()->childCount() >= 2);
|
|
QVERIFY(item1 != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(item1);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
for (int i = 0; i < 10 && itemType.baseItem()->childCount() > 1 &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
QVERIFY2(itemType.baseItem()->childCount() <= 1,
|
|
qPrintable(itemTypeName +
|
|
": Stack should handle ID remapping correctly"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Multiple undo/redo cycles with nested hierarchy and moves
|
|
// (regression test for child ID remapping across cycles)
|
|
{
|
|
itemType.addFolder();
|
|
QTreeWidgetItem *parent = itemType.baseItem()->child(0);
|
|
QVERIFY(parent != nullptr);
|
|
|
|
// Add first child folder
|
|
itemType.treeWidget()->setCurrentItem(parent);
|
|
itemType.addFolder();
|
|
QCoreApplication::processEvents();
|
|
|
|
// Add second child folder that will have a grandchild
|
|
itemType.treeWidget()->setCurrentItem(parent);
|
|
itemType.addFolder();
|
|
QCoreApplication::processEvents();
|
|
|
|
QCOMPARE(parent->childCount(), 2);
|
|
|
|
QTreeWidgetItem *childWithGrandchild = parent->child(0);
|
|
|
|
// Add grandchild item under the child folder
|
|
itemType.treeWidget()->setCurrentItem(childWithGrandchild);
|
|
itemType.addItem();
|
|
QCoreApplication::processEvents();
|
|
|
|
QCOMPARE(childWithGrandchild->childCount(), 1);
|
|
|
|
// Move the grandchild to be a direct child of parent (simulate drag-drop)
|
|
QTreeWidgetItem *grandchild = childWithGrandchild->child(0);
|
|
int grandchildID = grandchild->data(0, Qt::UserRole).toInt();
|
|
int oldParentID = childWithGrandchild->data(0, Qt::UserRole).toInt();
|
|
int newParentID = parent->data(0, Qt::UserRole).toInt();
|
|
int oldPosition = 0;
|
|
|
|
childWithGrandchild->takeChild(0);
|
|
parent->addChild(grandchild);
|
|
int newPosition = parent->indexOfChild(grandchild);
|
|
|
|
mpEditor->slot_itemMoved(grandchildID, oldParentID, newParentID,
|
|
oldPosition, newPosition);
|
|
|
|
QVERIFY2(parent->childCount() == 3 &&
|
|
childWithGrandchild->childCount() == 0,
|
|
qPrintable(itemTypeName + ": Move operation should succeed"));
|
|
|
|
// Test 3 full undo/redo cycles
|
|
for (int cycle = 0; cycle < 3; cycle++) {
|
|
// Undo all operations
|
|
for (int i = 0; i < 10 && mpEditor->mpUndoStack->canUndo() &&
|
|
itemType.baseItem()->childCount() > 0;
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
QVERIFY2(itemType.baseItem()->childCount() == 0,
|
|
qPrintable(itemTypeName +
|
|
": All items should be undone in cycle " +
|
|
QString::number(cycle)));
|
|
|
|
// Redo all operations
|
|
for (int i = 0; i < 10 && mpEditor->mpUndoStack->canRedo(); i++) {
|
|
mpEditor->mpUndoStack->redo();
|
|
}
|
|
|
|
QTreeWidgetItem *restoredParent = itemType.baseItem()->child(0);
|
|
QVERIFY2(
|
|
restoredParent != nullptr && restoredParent->childCount() == 3,
|
|
qPrintable(itemTypeName +
|
|
": Parent should have 3 children after redo in cycle " +
|
|
QString::number(cycle)));
|
|
|
|
// Verify moved grandchild is still direct child of parent
|
|
QTreeWidgetItem *childFolder = restoredParent->child(0);
|
|
QVERIFY2(
|
|
childFolder != nullptr && childFolder->childCount() == 0,
|
|
qPrintable(itemTypeName +
|
|
": Child folder should be empty (grandchild was moved) "
|
|
"in cycle " +
|
|
QString::number(cycle)));
|
|
}
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 5: Undo/Redo Chains
|
|
// ========================================================================
|
|
void testUndoRedoChains_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testUndoRedoChains() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: 5 operations → undo all → redo all
|
|
{
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
itemType.addItem();
|
|
}
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount + 5);
|
|
|
|
for (int i = 0;
|
|
i < 50 && itemType.baseItem()->childCount() > initialCount &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount);
|
|
|
|
for (int i = 0; i < 50 && mpEditor->mpUndoStack->canRedo() &&
|
|
itemType.baseItem()->childCount() < initialCount + 5;
|
|
i++) {
|
|
mpEditor->mpUndoStack->redo();
|
|
}
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount + 5);
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Undo 3 times → redo 2 → new op clears redo
|
|
{
|
|
itemType.addItem();
|
|
itemType.addItem();
|
|
itemType.addItem();
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->redo();
|
|
mpEditor->mpUndoStack->redo();
|
|
|
|
bool canRedoBefore = mpEditor->mpUndoStack->canRedo();
|
|
itemType.addItem();
|
|
bool canRedoAfter = mpEditor->mpUndoStack->canRedo();
|
|
|
|
QVERIFY2(
|
|
canRedoBefore && !canRedoAfter,
|
|
qPrintable(itemTypeName + ": New operation should clear redo stack"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Verify canUndo()/canRedo() states
|
|
{
|
|
itemType.addItem();
|
|
bool canUndoAfterAdd = mpEditor->mpUndoStack->canUndo();
|
|
mpEditor->mpUndoStack->undo();
|
|
bool canRedoAfterUndo = mpEditor->mpUndoStack->canRedo();
|
|
|
|
QVERIFY2(canUndoAfterAdd && canRedoAfterUndo,
|
|
qPrintable(itemTypeName +
|
|
": canUndo/canRedo states should be correct"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Complex operation chain (Add → delete → add → undo → redo → undo)
|
|
{
|
|
int stackIndexStart = mpEditor->mpUndoStack->index();
|
|
|
|
itemType.addItem();
|
|
QTreeWidgetItem *item1 = itemType.baseItem()->child(0);
|
|
QVERIFY(item1 != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(item1);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
itemType.addItem();
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->redo();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QVERIFY2(true,
|
|
qPrintable(itemTypeName + ": Complex operation chain works"));
|
|
|
|
while (mpEditor->mpUndoStack->index() > stackIndexStart) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 6: Edge Cases
|
|
// ========================================================================
|
|
void testEdgeCases_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testEdgeCases() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Undo when stack is empty
|
|
{
|
|
bool canUndoBefore = mpEditor->mpUndoStack->canUndo();
|
|
mpEditor->mpUndoStack->undo();
|
|
bool canUndoAfter = mpEditor->mpUndoStack->canUndo();
|
|
|
|
QVERIFY2(
|
|
!canUndoBefore && !canUndoAfter,
|
|
qPrintable(itemTypeName +
|
|
": Undo on empty stack should be handled gracefully"));
|
|
}
|
|
|
|
// Test: Redo when nothing to redo
|
|
{
|
|
bool canRedoBefore = mpEditor->mpUndoStack->canRedo();
|
|
mpEditor->mpUndoStack->redo();
|
|
bool canRedoAfter = mpEditor->mpUndoStack->canRedo();
|
|
|
|
QVERIFY2(!canRedoBefore && !canRedoAfter,
|
|
qPrintable(
|
|
itemTypeName +
|
|
": Redo with nothing to redo should be handled gracefully"));
|
|
}
|
|
|
|
// Test: Clear stack mid-operation
|
|
{
|
|
itemType.addItem();
|
|
itemType.addItem();
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
QVERIFY2(
|
|
!mpEditor->mpUndoStack->canUndo() &&
|
|
!mpEditor->mpUndoStack->canRedo(),
|
|
qPrintable(itemTypeName + ": Clear stack should work correctly"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Operations work after stack clear
|
|
{
|
|
mpEditor->mpUndoStack->clear();
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
|
|
itemType.addItem();
|
|
|
|
bool addWorked = itemType.baseItem()->childCount() > initialCount;
|
|
bool canUndoAfterAdd = mpEditor->mpUndoStack->canUndo();
|
|
|
|
QVERIFY2(addWorked && canUndoAfterAdd,
|
|
qPrintable(itemTypeName +
|
|
": Operations should work after stack clear"));
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Deep nesting (10 levels)
|
|
{
|
|
QTreeWidgetItem *currentParent = itemType.baseItem();
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
itemType.treeWidget()->setCurrentItem(currentParent);
|
|
itemType.addFolder();
|
|
if (currentParent == itemType.baseItem()) {
|
|
currentParent = itemType.baseItem()->child(0);
|
|
} else {
|
|
currentParent = currentParent->child(0);
|
|
}
|
|
}
|
|
|
|
QTreeWidgetItem *deepest = itemType.baseItem();
|
|
int depth = 0;
|
|
while (deepest && deepest->childCount() > 0) {
|
|
deepest = deepest->child(0);
|
|
depth++;
|
|
}
|
|
QCOMPARE(depth, 10);
|
|
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem()->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restored = itemType.baseItem();
|
|
int restoredDepth = 0;
|
|
while (restored && restored->childCount() > 0) {
|
|
restored = restored->child(0);
|
|
restoredDepth++;
|
|
}
|
|
QCOMPARE(restoredDepth, 10);
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 7: Integration Tests
|
|
// ========================================================================
|
|
void testIntegration() {
|
|
for (auto &itemType : mItemTypes) {
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Mixed operations across item types
|
|
{
|
|
const auto &triggers = mItemTypes[0];
|
|
const auto &timers = mItemTypes[1];
|
|
|
|
triggers.showView();
|
|
triggers.addItem();
|
|
int triggerCountAfterAdd = triggers.baseItem()->childCount();
|
|
|
|
timers.showView();
|
|
timers.addItem();
|
|
|
|
for (int i = 0; i < 20 &&
|
|
(triggers.baseItem()->childCount() > 0 ||
|
|
timers.baseItem()->childCount() > 0) &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
QVERIFY2(triggers.baseItem()->childCount() == 0 &&
|
|
timers.baseItem()->childCount() == 0,
|
|
"Mixed operations should undo correctly");
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// Test: Cross-type undo/redo ordering
|
|
{
|
|
const auto &triggers = mItemTypes[0];
|
|
const auto &aliases = mItemTypes[2];
|
|
|
|
triggers.showView();
|
|
triggers.addItem();
|
|
|
|
aliases.showView();
|
|
aliases.addItem();
|
|
|
|
for (int i = 0; i < 10 && aliases.baseItem()->childCount() > 0 &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
QVERIFY2(aliases.baseItem()->childCount() == 0 &&
|
|
triggers.baseItem()->childCount() == 1,
|
|
"Cross-type undo ordering should be correct");
|
|
|
|
for (int i = 0; i < 10 && aliases.baseItem()->childCount() == 0 &&
|
|
mpEditor->mpUndoStack->canRedo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->redo();
|
|
}
|
|
|
|
QVERIFY2(aliases.baseItem()->childCount() == 1,
|
|
"Cross-type redo ordering should be correct");
|
|
|
|
for (int i = 0; i < 20 && mpEditor->mpUndoStack->canUndo(); i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// Test: Undo works after view switch
|
|
{
|
|
const auto &triggers = mItemTypes[0];
|
|
const auto &scripts = mItemTypes[3];
|
|
|
|
triggers.showView();
|
|
triggers.addItem();
|
|
|
|
scripts.showView();
|
|
|
|
for (int i = 0; i < 10 && triggers.baseItem()->childCount() > 0 &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
QVERIFY2(triggers.baseItem()->childCount() == 0,
|
|
"Undo should work after view switch");
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// Test: Stack isolation verification
|
|
{
|
|
const auto &triggers = mItemTypes[0];
|
|
const auto &timers = mItemTypes[1];
|
|
|
|
triggers.showView();
|
|
int initialStackCount = mpEditor->mpUndoStack->count();
|
|
triggers.addItem();
|
|
int afterAddCount = mpEditor->mpUndoStack->count();
|
|
|
|
bool isolated = true;
|
|
for (const auto &otherType : mItemTypes) {
|
|
if (otherType.viewType != triggers.viewType &&
|
|
otherType.baseItem()->childCount() > 0) {
|
|
isolated = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
QVERIFY2(isolated && afterAddCount > initialStackCount,
|
|
"Stack should be properly isolated between types");
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// Test: Sequential delete operations across types
|
|
const auto &scripts = mItemTypes[3];
|
|
const auto &aliases = mItemTypes[2];
|
|
const auto &triggers = mItemTypes[0];
|
|
|
|
scripts.showView();
|
|
scripts.addItem();
|
|
aliases.showView();
|
|
aliases.addItem();
|
|
triggers.showView();
|
|
triggers.addItem();
|
|
|
|
QCOMPARE(scripts.baseItem()->childCount(), 1);
|
|
QCOMPARE(aliases.baseItem()->childCount(), 1);
|
|
QCOMPARE(triggers.baseItem()->childCount(), 1);
|
|
|
|
scripts.showView();
|
|
scripts.treeWidget()->setCurrentItem(scripts.baseItem()->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
aliases.showView();
|
|
aliases.treeWidget()->setCurrentItem(aliases.baseItem()->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
triggers.showView();
|
|
triggers.treeWidget()->setCurrentItem(triggers.baseItem()->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
QCOMPARE(scripts.baseItem()->childCount(), 0);
|
|
QCOMPARE(aliases.baseItem()->childCount(), 0);
|
|
QCOMPARE(triggers.baseItem()->childCount(), 0);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QVERIFY2(triggers.baseItem()->childCount() == 1 &&
|
|
aliases.baseItem()->childCount() == 0 &&
|
|
scripts.baseItem()->childCount() == 0,
|
|
"First undo should restore only triggers");
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QVERIFY2(triggers.baseItem()->childCount() == 1 &&
|
|
aliases.baseItem()->childCount() == 1 &&
|
|
scripts.baseItem()->childCount() == 0,
|
|
"Second undo should restore only aliases");
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QVERIFY2(triggers.baseItem()->childCount() == 1 &&
|
|
aliases.baseItem()->childCount() == 1 &&
|
|
scripts.baseItem()->childCount() == 1,
|
|
"Third undo should restore only scripts");
|
|
|
|
for (auto &type : mItemTypes) {
|
|
type.showView();
|
|
cleanupAll(type);
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 8: Large Batch Operations
|
|
// ========================================================================
|
|
void testLargeBatchOperations_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testLargeBatchOperations() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Large batch operations (50 items)
|
|
int originalLimit = mpEditor->mpUndoStack->undoLimit();
|
|
mpEditor->mpUndoStack->setUndoLimit(200);
|
|
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
|
|
for (int i = 0; i < 50; i++) {
|
|
itemType.addItem();
|
|
}
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount + 50);
|
|
|
|
for (int i = 0;
|
|
i < 500 && itemType.baseItem()->childCount() > initialCount &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount);
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
mpEditor->mpUndoStack->setUndoLimit(originalLimit);
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 9: State Consistency Tests
|
|
// ========================================================================
|
|
void testStateConsistency_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testStateConsistency() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: All items have valid IDs
|
|
{
|
|
itemType.addItem();
|
|
itemType.addItem();
|
|
|
|
bool allValid = true;
|
|
for (int i = 0; i < itemType.baseItem()->childCount(); i++) {
|
|
QTreeWidgetItem *item = itemType.baseItem()->child(i);
|
|
int id = item->data(0, Qt::UserRole).toInt();
|
|
if (id <= 0) {
|
|
allValid = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
QVERIFY2(allValid && itemType.baseItem()->childCount() == 2,
|
|
qPrintable(itemTypeName + ": All items should have valid IDs"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Parent-child relationships intact after undo/redo
|
|
{
|
|
itemType.addFolder();
|
|
QTreeWidgetItem *folder = itemType.baseItem()->child(0);
|
|
QVERIFY(folder != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
itemType.addItem();
|
|
|
|
int childCountBefore = folder->childCount();
|
|
|
|
itemType.treeWidget()->setCurrentItem(folder);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restored = itemType.baseItem()->child(0);
|
|
QVERIFY2(restored != nullptr &&
|
|
restored->childCount() == childCountBefore,
|
|
qPrintable(itemTypeName +
|
|
": Parent-child relationships should be intact"));
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Deep nested hierarchy preserved after undo
|
|
// (grandparent -> parent -> 5 children, verifies all children stay
|
|
// nested under parent and don't get restored at root level)
|
|
{
|
|
int initialCount = itemType.baseItem()->childCount();
|
|
|
|
itemType.treeWidget()->setCurrentItem(itemType.baseItem());
|
|
itemType.addFolder();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
QThread::sleep(10ms);
|
|
}
|
|
|
|
QTreeWidgetItem *grandparent = itemType.baseItem()->child(0);
|
|
QVERIFY(grandparent != nullptr);
|
|
|
|
itemType.treeWidget()->setCurrentItem(grandparent);
|
|
itemType.addFolder();
|
|
if (itemType.viewType == EditorViewType::cmKeysView ||
|
|
itemType.viewType == EditorViewType::cmActionView) {
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
QTreeWidgetItem *parent = grandparent->child(0);
|
|
QVERIFY(parent != nullptr);
|
|
|
|
// Add 5 children to the parent
|
|
itemType.treeWidget()->setCurrentItem(parent);
|
|
for (int i = 0; i < 5; i++) {
|
|
itemType.addItem();
|
|
}
|
|
|
|
int childrenCount = parent->childCount();
|
|
QCOMPARE(childrenCount, 5);
|
|
|
|
// Delete the grandparent (should delete entire tree)
|
|
itemType.treeWidget()->setCurrentItem(grandparent);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
QCOMPARE(itemType.baseItem()->childCount(), initialCount);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
QTreeWidgetItem *restoredGP = itemType.baseItem()->child(0);
|
|
QVERIFY2(restoredGP != nullptr && restoredGP->childCount() == 1,
|
|
qPrintable(itemTypeName +
|
|
": Grandparent with parent should be restored"));
|
|
|
|
QTreeWidgetItem *restoredP = restoredGP->child(0);
|
|
QVERIFY2(
|
|
restoredP != nullptr && restoredP->childCount() == childrenCount,
|
|
qPrintable(itemTypeName +
|
|
": All 5 children should be under parent, not at root"));
|
|
|
|
// Verify all children are accessible
|
|
for (int i = 0; i < childrenCount; i++) {
|
|
QVERIFY2(restoredP->child(i) != nullptr,
|
|
qPrintable(itemTypeName + ": Child " + QString::number(i) +
|
|
" should exist under parent"));
|
|
}
|
|
|
|
cleanupAll(itemType);
|
|
}
|
|
|
|
// Test: Stack command count consistency
|
|
{
|
|
int countBefore = mpEditor->mpUndoStack->count();
|
|
itemType.addItem();
|
|
int countAfter = mpEditor->mpUndoStack->count();
|
|
|
|
QVERIFY2(countAfter > countBefore,
|
|
qPrintable(itemTypeName +
|
|
": Stack count should increase with operations"));
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
QCOMPARE(mpEditor->mpUndoStack->count(), 0);
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 10: Error Recovery Tests
|
|
// ========================================================================
|
|
void testErrorRecovery_data() {
|
|
QTest::addColumn<int>("itemTypeIndex");
|
|
QTest::addColumn<QString>("itemTypeName");
|
|
|
|
QTest::newRow("Trigger") << 0 << "Trigger";
|
|
QTest::newRow("Timer") << 1 << "Timer";
|
|
QTest::newRow("Alias") << 2 << "Alias";
|
|
QTest::newRow("Script") << 3 << "Script";
|
|
QTest::newRow("Key") << 4 << "Key";
|
|
QTest::newRow("Action") << 5 << "Action";
|
|
}
|
|
|
|
void testErrorRecovery() {
|
|
QFETCH(int, itemTypeIndex);
|
|
QFETCH(QString, itemTypeName);
|
|
|
|
const auto &itemType = mItemTypes[itemTypeIndex];
|
|
itemType.showView();
|
|
cleanupAll(itemType);
|
|
|
|
// Test: Stack integrity after many operations
|
|
{
|
|
itemType.addItem();
|
|
itemType.addFolder();
|
|
mpEditor->mpUndoStack->undo();
|
|
itemType.addItem();
|
|
mpEditor->mpUndoStack->undo();
|
|
mpEditor->mpUndoStack->redo();
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
itemType.addItem();
|
|
for (int i = 0; i < 10 && itemType.baseItem()->childCount() > 0 &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
QCOMPARE(itemType.baseItem()->childCount(), 0);
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// Test: Cleanup verification
|
|
{
|
|
for (int i = 0; i < 5; i++) {
|
|
itemType.addItem();
|
|
}
|
|
|
|
for (int i = 0; i < 50 && itemType.baseItem()->childCount() > 0 &&
|
|
mpEditor->mpUndoStack->canUndo();
|
|
i++) {
|
|
mpEditor->mpUndoStack->undo();
|
|
}
|
|
|
|
QCOMPARE(itemType.baseItem()->childCount(), 0);
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 11: Edit Operations Tests
|
|
// ========================================================================
|
|
void testTriggerPatternEdits() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
QString initialPattern = qsl("initial pattern");
|
|
if (mpEditor->mTriggerPatternEdit.size() > 0) {
|
|
mpEditor->mTriggerPatternEdit[0]
|
|
->singleLineTextEdit_pattern->setPlainText(initialPattern);
|
|
mpEditor->saveTrigger();
|
|
}
|
|
|
|
QString originalPattern = pT->getPatternsList().value(0);
|
|
QString newPattern = qsl("test pattern edit");
|
|
|
|
if (mpEditor->mTriggerPatternEdit.size() > 0) {
|
|
mpEditor->mTriggerPatternEdit[0]
|
|
->singleLineTextEdit_pattern->setPlainText(newPattern);
|
|
mpEditor->saveTrigger();
|
|
}
|
|
|
|
QCOMPARE(pT->getPatternsList().value(0), newPattern);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(pT->getPatternsList().value(0), originalPattern);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QCOMPARE(pT->getPatternsList().value(0), newPattern);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testTriggerNameEdits() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
QString originalName = pT->getName();
|
|
QString newName = qsl("Edited Trigger Name");
|
|
|
|
mpEditor->mpTriggersMainArea->lineEdit_trigger_name->setText(newName);
|
|
mpEditor->saveTrigger();
|
|
|
|
QCOMPARE(pT->getName(), newName);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(pT->getName(), originalName);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QCOMPARE(pT->getName(), newName);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testTimerTimeValues() {
|
|
mpEditor->slot_showTimers();
|
|
cleanupAll(mItemTypes[1]);
|
|
|
|
mpEditor->addTimer(false);
|
|
QVERIFY(mpEditor->mpTimerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *timer = mpEditor->mpTimerBaseItem->child(0);
|
|
int timerID = timer->data(0, Qt::UserRole).toInt();
|
|
TTimer *pTimer = mpHost->getTimerUnit()->getTimer(timerID);
|
|
QVERIFY(pTimer != nullptr);
|
|
|
|
mpEditor->treeWidget_timers->setCurrentItem(timer);
|
|
mpEditor->slot_timerSelected(timer);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
QTime originalTime = pTimer->getTime();
|
|
QTime newMinutes(0, 1, 0, 0);
|
|
QTime newSeconds(0, 0, 30, 0);
|
|
QTime newMsecs(0, 0, 0, 500);
|
|
|
|
mpEditor->mpTimersMainArea->timeEdit_timer_minutes->setTime(newMinutes);
|
|
mpEditor->mpTimersMainArea->timeEdit_timer_seconds->setTime(newSeconds);
|
|
mpEditor->mpTimersMainArea->timeEdit_timer_msecs->setTime(newMsecs);
|
|
mpEditor->saveTimer();
|
|
|
|
QTime expectedTime(0, 1, 30, 500);
|
|
QCOMPARE(pTimer->getTime(), expectedTime);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(pTimer->getTime(), originalTime);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QCOMPARE(pTimer->getTime(), expectedTime);
|
|
|
|
cleanupAll(mItemTypes[1]);
|
|
}
|
|
|
|
void testTriggerPatternTypeChanges() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
// Set initial pattern and type
|
|
QString testPattern = qsl("test pattern");
|
|
QVERIFY(mpEditor->mTriggerPatternEdit.size() > 0);
|
|
mpEditor->mTriggerPatternEdit[0]->singleLineTextEdit_pattern->setPlainText(
|
|
testPattern);
|
|
mpEditor->mTriggerPatternEdit[0]->comboBox_patternType->setCurrentIndex(
|
|
REGEX_SUBSTRING);
|
|
mpEditor->saveTrigger();
|
|
|
|
int originalType = pT->getRegexCodePropertyList().value(0);
|
|
QCOMPARE(originalType, REGEX_SUBSTRING);
|
|
|
|
// Change pattern type to Perl regex
|
|
mpEditor->mTriggerPatternEdit[0]->comboBox_patternType->setCurrentIndex(
|
|
REGEX_PERL);
|
|
mpEditor->saveTrigger();
|
|
|
|
int newType = pT->getRegexCodePropertyList().value(0);
|
|
QCOMPARE(newType, REGEX_PERL);
|
|
|
|
// Undo should restore original type
|
|
mpEditor->mpUndoStack->undo();
|
|
int typeAfterUndo = pT->getRegexCodePropertyList().value(0);
|
|
QCOMPARE(typeAfterUndo, REGEX_SUBSTRING);
|
|
|
|
// Redo should restore new type
|
|
mpEditor->mpUndoStack->redo();
|
|
int typeAfterRedo = pT->getRegexCodePropertyList().value(0);
|
|
QCOMPARE(typeAfterRedo, REGEX_PERL);
|
|
|
|
// Pattern text should remain unchanged throughout
|
|
QCOMPARE(pT->getPatternsList().value(0), testPattern);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testAddTriggerDoesNotClearSelectedMultilineState() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *firstTriggerItem = mpEditor->mpTriggerBaseItem->child(0);
|
|
QVERIFY(firstTriggerItem != nullptr);
|
|
|
|
const int firstTriggerID = firstTriggerItem->data(0, Qt::UserRole).toInt();
|
|
TTrigger *firstTrigger =
|
|
mpHost->getTriggerUnit()->getTrigger(firstTriggerID);
|
|
QVERIFY(firstTrigger != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(firstTriggerItem);
|
|
mpEditor->slot_triggerSelected(firstTriggerItem);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
mpEditor->showPatternItems(2);
|
|
QVERIFY(mpEditor->mTriggerPatternEdit.size() >= 2);
|
|
mpEditor->mTriggerPatternEdit[0]->singleLineTextEdit_pattern->setPlainText(
|
|
qsl("first line"));
|
|
mpEditor->mTriggerPatternEdit[1]->singleLineTextEdit_pattern->setPlainText(
|
|
qsl("second line"));
|
|
mpEditor->mpTriggersMainArea->spinBox_lineMargin->setValue(2);
|
|
QCoreApplication::processEvents();
|
|
mpEditor->saveTrigger();
|
|
|
|
QVERIFY(firstTrigger->isMultiline());
|
|
QCOMPARE(firstTrigger->getConditionLineDelta(), 2);
|
|
QCOMPARE(firstTrigger->getPatternsList().size(), 2);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QCoreApplication::processEvents();
|
|
|
|
QCOMPARE(mpEditor->mpTriggerBaseItem->childCount(), 2);
|
|
QVERIFY2(firstTrigger->isMultiline(),
|
|
"Adding a sibling trigger should not clear the previously "
|
|
"selected trigger's multi-line state");
|
|
QCOMPARE(firstTrigger->getConditionLineDelta(), 2);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(firstTriggerItem);
|
|
mpEditor->slot_triggerSelected(firstTriggerItem);
|
|
|
|
QCOMPARE(mpEditor->mpTriggersMainArea->spinBox_lineMargin->value(), 2);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testTriggerHighlightingColor() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
QColor originalFgColor = pT->getFgColor();
|
|
QColor newFgColor(255, 0, 0); // Red
|
|
|
|
// Set color directly (simulating color picker)
|
|
pT->setColorizerFgColor(newFgColor);
|
|
|
|
// Save to create undo command
|
|
mpEditor->saveTrigger();
|
|
|
|
QCOMPARE(pT->getFgColor(), newFgColor);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(pT->getFgColor(), originalFgColor);
|
|
|
|
mpEditor->mpUndoStack->redo();
|
|
QCOMPARE(pT->getFgColor(), newFgColor);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testActionButtonRotation() {
|
|
mpEditor->slot_showActions();
|
|
cleanupAll(mItemTypes[5]);
|
|
|
|
mpEditor->addAction(false);
|
|
QVERIFY(mpEditor->mpActionBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *action = mpEditor->mpActionBaseItem->child(0);
|
|
mpEditor->treeWidget_actions->setCurrentItem(action);
|
|
mpEditor->slot_actionSelected(action);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
// Get original rotation from combobox
|
|
int originalRotationIndex =
|
|
mpEditor->mpActionsMainArea->comboBox_action_button_rotation
|
|
->currentIndex();
|
|
|
|
// Set new rotation - changing combobox triggers per-property save
|
|
int newRotationIndex = 1;
|
|
mpEditor->mpActionsMainArea->comboBox_action_button_rotation
|
|
->setCurrentIndex(newRotationIndex);
|
|
|
|
QCOMPARE(mpEditor->mpActionsMainArea->comboBox_action_button_rotation
|
|
->currentIndex(),
|
|
newRotationIndex);
|
|
|
|
// Undo the rotation change
|
|
mpEditor->mpUndoStack->undo();
|
|
|
|
// Re-fetch tree widget item after undo (item may have been rebuilt)
|
|
QVERIFY(mpEditor->mpActionBaseItem->childCount() > 0);
|
|
action = mpEditor->mpActionBaseItem->child(0);
|
|
mpEditor->slot_actionSelected(action);
|
|
|
|
QCOMPARE(mpEditor->mpActionsMainArea->comboBox_action_button_rotation
|
|
->currentIndex(),
|
|
originalRotationIndex);
|
|
|
|
// Redo the rotation change
|
|
mpEditor->mpUndoStack->redo();
|
|
|
|
// Re-fetch tree widget item after redo
|
|
QVERIFY(mpEditor->mpActionBaseItem->childCount() > 0);
|
|
action = mpEditor->mpActionBaseItem->child(0);
|
|
mpEditor->slot_actionSelected(action);
|
|
|
|
QCOMPARE(mpEditor->mpActionsMainArea->comboBox_action_button_rotation
|
|
->currentIndex(),
|
|
newRotationIndex);
|
|
|
|
cleanupAll(mItemTypes[5]);
|
|
}
|
|
|
|
void testAddActionDoesNotClearSelectedPushDownButton() {
|
|
mpEditor->slot_showActions();
|
|
cleanupAll(mItemTypes[5]);
|
|
|
|
mpEditor->addAction(false);
|
|
QVERIFY(mpEditor->mpActionBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *firstActionItem = mpEditor->mpActionBaseItem->child(0);
|
|
QVERIFY(firstActionItem != nullptr);
|
|
|
|
const int firstActionID = firstActionItem->data(0, Qt::UserRole).toInt();
|
|
TAction *firstAction = mpHost->getActionUnit()->getAction(firstActionID);
|
|
QVERIFY(firstAction != nullptr);
|
|
|
|
mpEditor->treeWidget_actions->setCurrentItem(firstActionItem);
|
|
mpEditor->slot_actionSelected(firstActionItem);
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
mpEditor->mpActionsMainArea->checkBox_action_button_isPushDown->setChecked(
|
|
true);
|
|
QCoreApplication::processEvents();
|
|
|
|
QVERIFY(firstAction->isPushDownButton());
|
|
QVERIFY(
|
|
mpEditor->mpActionsMainArea->checkBox_action_button_isPushDown
|
|
->isChecked());
|
|
|
|
mpEditor->addAction(false);
|
|
QCoreApplication::processEvents();
|
|
|
|
QCOMPARE(mpEditor->mpActionBaseItem->childCount(), 2);
|
|
QVERIFY2(firstAction->isPushDownButton(),
|
|
"Adding a sibling action should not clear the previously "
|
|
"selected action's push-down state");
|
|
|
|
mpEditor->treeWidget_actions->setCurrentItem(firstActionItem);
|
|
mpEditor->slot_actionSelected(firstActionItem);
|
|
|
|
QVERIFY(
|
|
mpEditor->mpActionsMainArea->checkBox_action_button_isPushDown
|
|
->isChecked());
|
|
|
|
cleanupAll(mItemTypes[5]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 12: Crash Prevention Tests
|
|
// ========================================================================
|
|
void testEmptyObjectDeletion() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->treeWidget_triggers->clearSelection();
|
|
mpEditor->treeWidget_triggers->setCurrentItem(nullptr);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
QVERIFY2(true, "No crash when nothing selected");
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(mpEditor->mpTriggerBaseItem);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
QVERIFY2(true, "No crash when base item selected");
|
|
}
|
|
|
|
void testPatternTypeSwitch() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
mpEditor->mTriggerPatternEdit[0]->comboBox_patternType->setCurrentIndex(
|
|
0);
|
|
mpEditor->saveTrigger();
|
|
mpEditor->mTriggerPatternEdit[0]->comboBox_patternType->setCurrentIndex(
|
|
1);
|
|
mpEditor->saveTrigger();
|
|
mpEditor->mTriggerPatternEdit[0]->comboBox_patternType->setCurrentIndex(
|
|
2);
|
|
mpEditor->saveTrigger();
|
|
}
|
|
QVERIFY2(true, "No crash on multiple pattern type switches");
|
|
|
|
if (mpEditor->mpUndoStack->canUndo()) {
|
|
mpEditor->mpUndoStack->undo();
|
|
QVERIFY2(true, "No crash on undo after type switches");
|
|
}
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 13: Bug-Specific Tests
|
|
// ========================================================================
|
|
void testPrematureUndoActivation() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
int initialCount = mpEditor->mpTriggerBaseItem->childCount();
|
|
mpEditor->treeWidget_triggers->setCurrentItem(mpEditor->mpTriggerBaseItem);
|
|
|
|
bool canUndoBefore = mpEditor->mpUndoStack->canUndo();
|
|
mpEditor->mpUndoStack->undo();
|
|
int countAfterUndo = mpEditor->mpTriggerBaseItem->childCount();
|
|
|
|
QVERIFY2(!canUndoBefore || countAfterUndo == initialCount,
|
|
"No items should disappear when undo clicked without changes");
|
|
}
|
|
|
|
void testTriggerNameWiped() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
QString triggerName = qsl("Test Trigger Name");
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
mpEditor->mpTriggersMainArea->lineEdit_trigger_name->setText(triggerName);
|
|
mpEditor->saveTrigger();
|
|
|
|
QString nameAfterSave = pT->getName();
|
|
mpEditor->addTrigger(true);
|
|
QString nameAfterGroupCreation = pT->getName();
|
|
|
|
QCOMPARE(nameAfterGroupCreation, triggerName);
|
|
QCOMPARE(nameAfterGroupCreation, nameAfterSave);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testScriptDuplication() {
|
|
mpEditor->slot_showScripts();
|
|
cleanupAll(mItemTypes[3]);
|
|
|
|
mpEditor->addScript(false);
|
|
QCOMPARE(mpEditor->mpScriptsBaseItem->childCount(), 1);
|
|
|
|
QTreeWidgetItem *script = mpEditor->mpScriptsBaseItem->child(0);
|
|
mpEditor->treeWidget_scripts->setCurrentItem(script);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
QCOMPARE(mpEditor->mpScriptsBaseItem->childCount(), 0);
|
|
|
|
mpEditor->mpUndoStack->undo();
|
|
QCOMPARE(mpEditor->mpScriptsBaseItem->childCount(), 1);
|
|
|
|
cleanupAll(mItemTypes[3]);
|
|
}
|
|
|
|
void testMultiTriggerPasteIntoGroup() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(true);
|
|
QCOMPARE(mpEditor->mpTriggerBaseItem->childCount(), 1);
|
|
|
|
QTreeWidgetItem *group = mpEditor->mpTriggerBaseItem->child(0);
|
|
int groupID = group->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pGroup = mpHost->getTriggerUnit()->getTrigger(groupID);
|
|
QVERIFY(pGroup != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(group);
|
|
mpEditor->addTrigger(false);
|
|
mpEditor->addTrigger(false);
|
|
QCOMPARE(group->childCount(), 2);
|
|
|
|
// copy both triggers, then paste them into the group
|
|
mpEditor->treeWidget_triggers->clearSelection();
|
|
mpEditor->treeWidget_triggers->setCurrentItem(group->child(0));
|
|
group->child(0)->setSelected(true);
|
|
group->child(1)->setSelected(true);
|
|
mpEditor->slot_copyXml();
|
|
|
|
mpEditor->treeWidget_triggers->clearSelection();
|
|
mpEditor->treeWidget_triggers->setCurrentItem(group);
|
|
mpEditor->slot_pasteXml();
|
|
|
|
// each pasted trigger must be linked into the group exactly once
|
|
QCOMPARE(static_cast<int>(pGroup->getChildrenList()->size()), 4);
|
|
|
|
// re-render the tree and verify the group shows exactly four children
|
|
QEnterEvent enterEvent{QPointF(), QPointF(), QPointF()};
|
|
QApplication::sendEvent(mpEditor, &enterEvent);
|
|
QCOMPARE(mpEditor->mpTriggerBaseItem->childCount(), 1);
|
|
QCOMPARE(mpEditor->mpTriggerBaseItem->child(0)->childCount(), 4);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testMultiTriggerPasteAfterSibling() {
|
|
mpEditor->slot_showTriggers();
|
|
cleanupAll(mItemTypes[0]);
|
|
|
|
mpEditor->addTrigger(true);
|
|
QTreeWidgetItem *group = mpEditor->mpTriggerBaseItem->child(0);
|
|
int groupID = group->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pGroup = mpHost->getTriggerUnit()->getTrigger(groupID);
|
|
QVERIFY(pGroup != nullptr);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(group);
|
|
mpEditor->addTrigger(false);
|
|
mpEditor->addTrigger(false);
|
|
QCOMPARE(group->childCount(), 2);
|
|
const int firstID = group->child(0)->data(0, Qt::UserRole).toInt();
|
|
const int secondID = group->child(1)->data(0, Qt::UserRole).toInt();
|
|
|
|
// copy both triggers, then paste them onto the first (non-folder) trigger
|
|
mpEditor->treeWidget_triggers->clearSelection();
|
|
mpEditor->treeWidget_triggers->setCurrentItem(group->child(0));
|
|
group->child(0)->setSelected(true);
|
|
group->child(1)->setSelected(true);
|
|
mpEditor->slot_copyXml();
|
|
|
|
mpEditor->treeWidget_triggers->clearSelection();
|
|
mpEditor->treeWidget_triggers->setCurrentItem(group->child(0));
|
|
mpEditor->slot_pasteXml();
|
|
|
|
// pasted triggers must be inserted right after the selected sibling,
|
|
// not appended at the end of the group
|
|
auto *children = pGroup->getChildrenList();
|
|
QCOMPARE(static_cast<int>(children->size()), 4);
|
|
QList<int> childIDs;
|
|
for (auto *child : *children) {
|
|
childIDs << child->getID();
|
|
}
|
|
QCOMPARE(childIDs.at(0), firstID);
|
|
QVERIFY(childIDs.at(1) != firstID && childIDs.at(1) != secondID);
|
|
QVERIFY(childIDs.at(2) != firstID && childIDs.at(2) != secondID);
|
|
QCOMPARE(childIDs.at(3), secondID);
|
|
|
|
cleanupAll(mItemTypes[0]);
|
|
}
|
|
|
|
void testMultiAliasPasteIntoGroup() {
|
|
mpEditor->slot_showAliases();
|
|
cleanupAll(mItemTypes[2]);
|
|
|
|
mpEditor->addAlias(true);
|
|
QCOMPARE(mpEditor->mpAliasBaseItem->childCount(), 1);
|
|
|
|
QTreeWidgetItem *group = mpEditor->mpAliasBaseItem->child(0);
|
|
int groupID = group->data(0, Qt::UserRole).toInt();
|
|
TAlias *pGroup = mpHost->getAliasUnit()->getAlias(groupID);
|
|
QVERIFY(pGroup != nullptr);
|
|
|
|
mpEditor->treeWidget_aliases->setCurrentItem(group);
|
|
mpEditor->addAlias(false);
|
|
mpEditor->addAlias(false);
|
|
QCOMPARE(group->childCount(), 2);
|
|
|
|
// copy both aliases, then paste them into the group
|
|
mpEditor->treeWidget_aliases->clearSelection();
|
|
mpEditor->treeWidget_aliases->setCurrentItem(group->child(0));
|
|
group->child(0)->setSelected(true);
|
|
group->child(1)->setSelected(true);
|
|
mpEditor->slot_copyXml();
|
|
|
|
mpEditor->treeWidget_aliases->clearSelection();
|
|
mpEditor->treeWidget_aliases->setCurrentItem(group);
|
|
mpEditor->slot_pasteXml();
|
|
|
|
// both pasted aliases must land inside the group, each linked exactly once
|
|
QCOMPARE(static_cast<int>(pGroup->getChildrenList()->size()), 4);
|
|
|
|
// re-render the tree: the group shows four children and none at the root
|
|
QEnterEvent enterEvent{QPointF(), QPointF(), QPointF()};
|
|
QApplication::sendEvent(mpEditor, &enterEvent);
|
|
QCOMPARE(mpEditor->mpAliasBaseItem->childCount(), 1);
|
|
QCOMPARE(mpEditor->mpAliasBaseItem->child(0)->childCount(), 4);
|
|
|
|
cleanupAll(mItemTypes[2]);
|
|
}
|
|
|
|
void testMultiKeyPasteIntoGroup() {
|
|
mpEditor->slot_showKeys();
|
|
cleanupAll(mItemTypes[4]);
|
|
|
|
mpEditor->addKey(true);
|
|
QCOMPARE(mpEditor->mpKeyBaseItem->childCount(), 1);
|
|
|
|
QTreeWidgetItem *group = mpEditor->mpKeyBaseItem->child(0);
|
|
int groupID = group->data(0, Qt::UserRole).toInt();
|
|
TKey *pGroup = mpHost->getKeyUnit()->getKey(groupID);
|
|
QVERIFY(pGroup != nullptr);
|
|
|
|
mpEditor->treeWidget_keys->setCurrentItem(group);
|
|
mpEditor->addKey(false);
|
|
mpEditor->addKey(false);
|
|
QCOMPARE(group->childCount(), 2);
|
|
|
|
// copy both keys, then paste them into the group
|
|
mpEditor->treeWidget_keys->clearSelection();
|
|
mpEditor->treeWidget_keys->setCurrentItem(group->child(0));
|
|
group->child(0)->setSelected(true);
|
|
group->child(1)->setSelected(true);
|
|
mpEditor->slot_copyXml();
|
|
|
|
mpEditor->treeWidget_keys->clearSelection();
|
|
mpEditor->treeWidget_keys->setCurrentItem(group);
|
|
mpEditor->slot_pasteXml();
|
|
|
|
// both pasted keys must land inside the group, each linked exactly once
|
|
QCOMPARE(static_cast<int>(pGroup->getChildrenList()->size()), 4);
|
|
|
|
// re-render the tree: the group shows four children and none at the root
|
|
QEnterEvent enterEvent{QPointF(), QPointF(), QPointF()};
|
|
QApplication::sendEvent(mpEditor, &enterEvent);
|
|
QCOMPARE(mpEditor->mpKeyBaseItem->childCount(), 1);
|
|
QCOMPARE(mpEditor->mpKeyBaseItem->child(0)->childCount(), 4);
|
|
|
|
cleanupAll(mItemTypes[4]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 14: UI Pattern Clearing Tests
|
|
// ========================================================================
|
|
void testTriggerPatternUIClearing() {
|
|
mpEditor->slot_showTriggers();
|
|
|
|
while (mpEditor->mpTriggerBaseItem->childCount() > 0) {
|
|
mpEditor->treeWidget_triggers->setCurrentItem(
|
|
mpEditor->mpTriggerBaseItem->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
}
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
QStringList patterns;
|
|
QList<int> patternTypes;
|
|
patterns << qsl("pattern1") << qsl("pattern2") << qsl("pattern3");
|
|
patternTypes << REGEX_SUBSTRING << REGEX_PERL
|
|
<< REGEX_BEGIN_OF_LINE_SUBSTRING;
|
|
pT->setRegexCodeList(patterns, patternTypes);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
|
|
bool patternsLoaded = true;
|
|
for (int i = 0; i < 3; i++) {
|
|
QString uiPattern = mpEditor->mTriggerPatternEdit[i]
|
|
->singleLineTextEdit_pattern->toPlainText();
|
|
if (uiPattern != patterns[i]) {
|
|
patternsLoaded = false;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY2(patternsLoaded, "Patterns should be loaded in UI");
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
|
|
QList<int> affectedIDs;
|
|
affectedIDs << triggerID;
|
|
mpEditor->slot_itemsChanged(EditorViewType::cmTriggerView, affectedIDs);
|
|
|
|
bool patternsCleared = true;
|
|
for (int i = 0; i < 3; i++) {
|
|
QString uiPattern = mpEditor->mTriggerPatternEdit[i]
|
|
->singleLineTextEdit_pattern->toPlainText();
|
|
if (!uiPattern.isEmpty()) {
|
|
patternsCleared = false;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY2(patternsCleared, "Patterns should be cleared when item not found");
|
|
|
|
bool fieldsCleared =
|
|
mpEditor->mpTriggersMainArea->lineEdit_trigger_name->text().isEmpty() &&
|
|
mpEditor->mpTriggersMainArea->label_idNumber->text().isEmpty();
|
|
QVERIFY2(fieldsCleared, "Name/ID fields should be cleared");
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
void testTriggerPatternUIClearingWithEmptyIds() {
|
|
mpEditor->slot_showTriggers();
|
|
|
|
while (mpEditor->mpTriggerBaseItem->childCount() > 0) {
|
|
mpEditor->treeWidget_triggers->setCurrentItem(
|
|
mpEditor->mpTriggerBaseItem->child(0));
|
|
mpEditor->slot_deleteItemOrGroup();
|
|
}
|
|
mpEditor->mpUndoStack->clear();
|
|
|
|
mpEditor->addTrigger(false);
|
|
QVERIFY(mpEditor->mpTriggerBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *trigger = mpEditor->mpTriggerBaseItem->child(0);
|
|
int triggerID = trigger->data(0, Qt::UserRole).toInt();
|
|
TTrigger *pT = mpHost->getTriggerUnit()->getTrigger(triggerID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
QStringList patterns;
|
|
QList<int> patternTypes;
|
|
patterns << qsl("test1") << qsl("test2");
|
|
patternTypes << REGEX_SUBSTRING << REGEX_PERL;
|
|
pT->setRegexCodeList(patterns, patternTypes);
|
|
|
|
mpEditor->treeWidget_triggers->setCurrentItem(trigger);
|
|
mpEditor->slot_triggerSelected(trigger);
|
|
|
|
QList<int> emptyList;
|
|
mpEditor->slot_itemsChanged(EditorViewType::cmTriggerView, emptyList);
|
|
|
|
bool patternsCleared = true;
|
|
for (int i = 0; i < 2; i++) {
|
|
QString uiPattern = mpEditor->mTriggerPatternEdit[i]
|
|
->singleLineTextEdit_pattern->toPlainText();
|
|
if (!uiPattern.isEmpty()) {
|
|
patternsCleared = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
QVERIFY2(patternsCleared,
|
|
"Patterns should be cleared when affectedItemIDs is empty");
|
|
|
|
mpEditor->mpUndoStack->clear();
|
|
}
|
|
|
|
// ========================================================================
|
|
// CATEGORY 15: Alias autosave guards mirror the explicit Save button
|
|
// ========================================================================
|
|
void testAliasAutosaveFlagsInvalidRegex() {
|
|
mpEditor->slot_showAliases();
|
|
cleanupAll(mItemTypes[2]);
|
|
|
|
mpEditor->addAlias(false);
|
|
QVERIFY(mpEditor->mpAliasBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *item = mpEditor->mpAliasBaseItem->child(0);
|
|
const int aliasID = item->data(0, Qt::UserRole).toInt();
|
|
TAlias *pT = mpHost->getAliasUnit()->getAlias(aliasID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_aliases->setCurrentItem(item);
|
|
mpEditor->slot_aliasSelected(item);
|
|
|
|
// Type an invalid pattern and finish editing (the autosave path).
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_pattern->setText(qsl("("));
|
|
mpEditor->slot_saveProperty_AliasPattern();
|
|
|
|
// The broken pattern must be flagged, just like clicking Save would.
|
|
QVERIFY2(!pT->state(),
|
|
"invalid regex should leave the alias in an error state");
|
|
QCOMPARE(item->data(0, Qt::AccessibleDescriptionRole).toString(),
|
|
mpEditor->descError);
|
|
|
|
// Fixing the pattern must clear the error flag on autosave. This alias was
|
|
// never explicitly saved, so it recovers to the "unsaved/new" state.
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_pattern->setText(qsl("^hello$"));
|
|
mpEditor->slot_saveProperty_AliasPattern();
|
|
QVERIFY2(pT->state(), "valid regex should clear the error state");
|
|
QCOMPARE(item->data(0, Qt::AccessibleDescriptionRole).toString(),
|
|
mpEditor->descNewItem);
|
|
|
|
cleanupAll(mItemTypes[2]);
|
|
}
|
|
|
|
void testAliasAutosaveGuardsInfiniteLoop() {
|
|
mpEditor->slot_showAliases();
|
|
cleanupAll(mItemTypes[2]);
|
|
|
|
mpEditor->addAlias(false);
|
|
QVERIFY(mpEditor->mpAliasBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *item = mpEditor->mpAliasBaseItem->child(0);
|
|
const int aliasID = item->data(0, Qt::UserRole).toInt();
|
|
TAlias *pT = mpHost->getAliasUnit()->getAlias(aliasID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_aliases->setCurrentItem(item);
|
|
mpEditor->slot_aliasSelected(item);
|
|
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_pattern->setText(qsl("^say"));
|
|
mpEditor->slot_saveProperty_AliasPattern();
|
|
QVERIFY(pT->state());
|
|
|
|
// A substitution that matches its own pattern would call the alias forever;
|
|
// the autosave path must reject it, just like the explicit Save button.
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_command->setText(qsl("say hello"));
|
|
mpEditor->slot_saveProperty_AliasCommand();
|
|
|
|
QVERIFY2(pT->getCommand() != qsl("say hello"),
|
|
"a self-matching substitution must not be saved");
|
|
QCOMPARE(item->data(0, Qt::AccessibleDescriptionRole).toString(),
|
|
mpEditor->descError);
|
|
|
|
cleanupAll(mItemTypes[2]);
|
|
}
|
|
|
|
void testAliasAutosavePatternLoopGuard() {
|
|
mpEditor->slot_showAliases();
|
|
cleanupAll(mItemTypes[2]);
|
|
|
|
mpEditor->addAlias(false);
|
|
QVERIFY(mpEditor->mpAliasBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *item = mpEditor->mpAliasBaseItem->child(0);
|
|
const int aliasID = item->data(0, Qt::UserRole).toInt();
|
|
TAlias *pT = mpHost->getAliasUnit()->getAlias(aliasID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_aliases->setCurrentItem(item);
|
|
mpEditor->slot_aliasSelected(item);
|
|
|
|
// Give it a command that does not loop with the (empty) pattern.
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_command->setText(qsl("wave"));
|
|
mpEditor->slot_saveProperty_AliasCommand();
|
|
QCOMPARE(pT->getCommand(), qsl("wave"));
|
|
|
|
// Editing the pattern so it matches the command must be rejected too - the
|
|
// loop guard has to fire from the pattern slot, not just the command slot.
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_pattern->setText(qsl("^wave"));
|
|
mpEditor->slot_saveProperty_AliasPattern();
|
|
|
|
QVERIFY2(pT->getRegexCode() != qsl("^wave"),
|
|
"a pattern matching its own substitution must not be saved");
|
|
QCOMPARE(item->data(0, Qt::AccessibleDescriptionRole).toString(),
|
|
mpEditor->descError);
|
|
|
|
cleanupAll(mItemTypes[2]);
|
|
}
|
|
|
|
void testAliasAutosaveClearsErrorAfterFix() {
|
|
mpEditor->slot_showAliases();
|
|
cleanupAll(mItemTypes[2]);
|
|
|
|
mpEditor->addAlias(false);
|
|
QVERIFY(mpEditor->mpAliasBaseItem->childCount() > 0);
|
|
|
|
QTreeWidgetItem *item = mpEditor->mpAliasBaseItem->child(0);
|
|
const int aliasID = item->data(0, Qt::UserRole).toInt();
|
|
TAlias *pT = mpHost->getAliasUnit()->getAlias(aliasID);
|
|
QVERIFY(pT != nullptr);
|
|
|
|
mpEditor->treeWidget_aliases->setCurrentItem(item);
|
|
mpEditor->slot_aliasSelected(item);
|
|
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_pattern->setText(qsl("^say"));
|
|
mpEditor->slot_saveProperty_AliasPattern();
|
|
QVERIFY(pT->state());
|
|
|
|
// A looping command is rejected and flags the item.
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_command->setText(qsl("say hi"));
|
|
mpEditor->slot_saveProperty_AliasCommand();
|
|
QCOMPARE(item->data(0, Qt::AccessibleDescriptionRole).toString(),
|
|
mpEditor->descError);
|
|
|
|
// Correcting the command must clear the flag and persist the new value.
|
|
mpEditor->mpAliasMainArea->lineEdit_alias_command->setText(qsl("wave"));
|
|
mpEditor->slot_saveProperty_AliasCommand();
|
|
QCOMPARE(pT->getCommand(), qsl("wave"));
|
|
QVERIFY2(item->data(0, Qt::AccessibleDescriptionRole).toString() !=
|
|
mpEditor->descError,
|
|
"correcting the command must clear the loop error flag");
|
|
|
|
cleanupAll(mItemTypes[2]);
|
|
}
|
|
};
|
|
|
|
#include "dlgTriggerEditorUndoRedoTest.moc"
|
|
QTEST_MAIN(dlgTriggerEditorUndoRedoTest)
|