2026-04-13 18:54:40 +02:00
|
|
|
/***************************************************************************
|
|
|
|
|
* 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>
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
#include <chrono>
|
2026-04-13 18:54:40 +02:00
|
|
|
|
|
|
|
|
#include "EditorUndoStack.h"
|
|
|
|
|
#include "Host.h"
|
Infrastructure: Swap out QtConcurrent module header for sub-module ones (#9246)
#### Brief overview of PR changes/additions
The Qt documentation for `QtConcurrent` points out:
> If you include the `<QtConcurrent>` header, the entire Qt Concurrent
module with the entire Qt Core module will be included, which may
increase compilation times and binary sizes. To use individual functions
from the QtConcurrent namespace, you can include more specific headers.
>
> The table below lists the functions in the QtConcurrent namespace and
their corresponding headers:
|Function|Header|
|--------|------|
|`QtConcurrent::run()`|`<QtConcurrentRun>`|
|`QtConcurrent::task()`| `<QtConcurrentTask>`|
|`QtConcurrent::filter()`,<br>`QtConcurrent::filtered()`,<br>`QtConcurrent::filteredReduced()`|`<QtConcurrentFilter>`|
|`QtConcurrent::map()`,<br>`QtConcurrent::mapped()`,<br>`QtConcurrent::mappedReduced()`|`<QtConcurrentMap>`|
#### Motivation for adding to Mudlet
To speed up the build a little by removing stuff that isn't needed.
#### Other info (issues closed, discussion etc)
In doing this I happened to start cleaning up a couple of header files
`T2DMap.h` and then `mudlet.h`, I then got into converting some
`#include`s into forward declarations in a "include-what-you-use" move.
This then rippled through into a (more than 10!) number of files but
should "improve" things.
Note that the ordering of `#include` in many files seems to be rather
haphazard and is due for some serious overhaul - I suggest that we
should actually declare an "official" style for this project so that
everyone knows what it is.
**During the CI/CB process I discovered that Linux and then MacOS builds
were failing because the file referred to by the `#include
<QtConcurrentTask>` header file was missing, yet was present on my local
PC when I was using the Qt framework from the On-line installer.
Initially I suspected a Debian (and then Devuan - as the packaged
version on my own machine also had this defect AND Ubuntu) package
problem; however it now seems to be an upstream Qt issue as the various
Qt versions & OS combinations suggest that Qt themselves fixed it for Qt
6.10:**
| OS | QtVersion | Missing header |
|--------|-----------------------|----------------|
| Windows| 6.11.0 package | No |
| Devuan | 6.8.2 package | Yes |
| Devuan | 6.10.0 online install | No |
| Ubuntu | 6.9.0 package | Yes |
| Debian | 6.8.2 package | Yes |
| MacOS | 6.9.0 package | Yes |
**To fix this I reverted to an `#include <qtconcurrenttask.h>` for Linux
and MacOS builds - although it would probably have been better to make
it conditional on the Qt Version instead...**
*I have reported this upstream to Debian - see:
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1135197*
---------
Signed-off-by: Stephen Lyons <slysven@virginmedia.com>
2026-04-29 13:35:29 +01:00
|
|
|
#include "MudletInstanceCoordinator.h"
|
2026-04-13 18:54:40 +02:00
|
|
|
#include "TAction.h"
|
fix: pasting several copied items now works for every item type (#9423)
#### Brief overview of PR changes/additions
- Pasting multiple copied triggers into a group created an extra copy of
the first trigger, and the surplus entry could not be deleted - the
redundant second reparent is now skipped
- For every other item type (aliases, timers, keys, scripts, buttons)
multi-item paste was worse: only the first copied item pasted at all (at
the top level), the rest were silently dropped - the multi-item
clipboard separator was only used by triggers; all six types now share
it and every pasted item is placed into the selected group
- Adds regression tests for trigger, alias, and key multi-paste
#### Motivation for adding to Mudlet
Copy/pasting several items at once has been broken since multi-select
was introduced: duplicates for triggers, dropped items for everything
else.
#### Other info (issues closed, discussion etc)
Fixes #8872
**Test case:** Create a group with two triggers, select both, Copy,
select the group, Paste - exactly two new triggers appear inside it
(previously three, with the first duplicated and undeletable). Repeat in
the Aliases view - both pasted aliases land inside the group (previously
one pasted at top level and the other vanished).
---------
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
Co-authored-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-18 11:27:20 +02:00
|
|
|
#include "TAlias.h"
|
|
|
|
|
#include "TKey.h"
|
2026-04-13 18:54:40 +02:00
|
|
|
#include "TTimer.h"
|
|
|
|
|
#include "TTreeWidget.h"
|
|
|
|
|
#include "TTrigger.h"
|
|
|
|
|
#include "TelnetServerStub.h"
|
|
|
|
|
#include "ctelnet.h"
|
|
|
|
|
#include "dlgActionMainArea.h"
|
fix: flag invalid aliases and block loops while typing them (#9499)
#### 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
2026-07-27 20:10:18 +02:00
|
|
|
#include "dlgAliasMainArea.h"
|
2026-04-13 18:54:40 +02:00
|
|
|
#include "dlgConnectionProfiles.h"
|
|
|
|
|
#include "dlgTimersMainArea.h"
|
|
|
|
|
#include "dlgTriggerEditor.h"
|
|
|
|
|
#include "dlgTriggerPatternEdit.h"
|
|
|
|
|
#include "dlgTriggersMainArea.h"
|
|
|
|
|
#include "mudlet.h"
|
|
|
|
|
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
using namespace std::chrono_literals;
|
|
|
|
|
|
2026-04-13 18:54:40 +02:00
|
|
|
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");
|
2026-07-20 09:01:51 +02:00
|
|
|
QString mPort; // assigned the stub's actual ephemeral port in initTestCase()
|
2026-04-13 18:54:40 +02:00
|
|
|
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) {
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTimer::singleShot(0ms, qApp, [profileName, address, port]() {
|
2026-04-13 18:54:40 +02:00
|
|
|
mudlet::self()->startAutoLogin({});
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
|
|
|
|
|
// 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);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
|
|
|
|
|
Q_ASSERT_X(QApplication::focusWidget(), "startProfile",
|
|
|
|
|
"No widget has focus after clicking new profile button");
|
|
|
|
|
|
|
|
|
|
QTest::keyClicks(QApplication::focusWidget(), profileName);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
QTest::keyClicks(QApplication::focusWidget(), address);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Tab);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
QTest::keyClicks(QApplication::focusWidget(), port);
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
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);
|
2026-07-20 09:01:51 +02:00
|
|
|
mpServer->start(mLocalhost, 0); // ephemeral OS-assigned port avoids collisions across concurrent test runs
|
2026-04-13 18:54:40 +02:00
|
|
|
QVERIFY2(mpServer->isListening(),
|
|
|
|
|
qPrintable(qsl("TelnetServerStub failed to start: %1")
|
|
|
|
|
.arg(mpServer->errorString())));
|
2026-07-20 09:01:51 +02:00
|
|
|
mPort = QString::number(mpServer->serverPort());
|
2026-04-13 18:54:40 +02:00
|
|
|
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();
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QTest::qWait(100ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
|
|
|
|
|
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();
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QThread::sleep(10ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QThread::sleep(10ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QThread::sleep(10ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2026-04-25 21:17:42 +01:00
|
|
|
for (auto *item : std::as_const(items)) {
|
2026-04-13 18:54:40 +02:00
|
|
|
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();
|
2026-04-25 21:17:42 +01:00
|
|
|
for (auto *item : std::as_const(items)) {
|
2026-04-13 18:54:40 +02:00
|
|
|
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();
|
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
|
|
|
QThread::sleep(10ms);
|
2026-04-13 18:54:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
Fix: trigger multi-line attribute resets when adding a sibling trigger (#9284)
## Summary
When a multi-line trigger is selected and the user clicks "Add new
trigger", `dlgTriggerEditor::addTrigger()` runs its "Reset UI" block to
clear the form for the new entry. That block calls
`spinBox_lineMargin->setValue(-1)`,
`checkBox_perlSlashGOption->setChecked(false)`,
`checkBox_filterTrigger->setChecked(false)`,
`spinBox_stayOpen->setValue(0)`, and
`groupBox_triggerColorizer->setChecked(false)`.
Each of those widget changes fires the matching
`slot_saveProperty_Trigger*` slot synchronously. `mpCurrentTriggerItem`
is not updated to the new item until later in `addTrigger()`, so the
slots write the reset values into the *previously selected* trigger —
flipping `isMultiline` to false, clearing colorizer settings, etc.
Re-selecting the original trigger shows it as plain `OR / Multi-item`.
The fix sets `mBlockPropertySave = true` immediately before the UI reset
block. `slot_triggerSelected()` clears the flag once the new item is
loaded, restoring normal behaviour.
Fixes #9216
## Test plan
- [ ] Reproduce the original bug on a build without this patch
(multi-line trigger reverts to OR/Multi-item after adding a sibling)
- [ ] On a build with this patch, perform the same steps and confirm the
multi-line trigger keeps its `AND / Multi-line (within: N lines)`
setting
- [ ] Confirm the other trigger properties (perl /g, filter, stay open,
colorizer) are also not clobbered on the previous trigger when adding a
sibling
- [ ] Confirm property edits on the *new* trigger still produce undo
entries as before (the flag is cleared by `slot_triggerSelected`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Ethan Hussong <ethan@ethanhussong.com>
2026-07-13 11:37:57 -05:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 18:54:40 +02:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
Fix: trigger multi-line attribute resets when adding a sibling trigger (#9284)
## Summary
When a multi-line trigger is selected and the user clicks "Add new
trigger", `dlgTriggerEditor::addTrigger()` runs its "Reset UI" block to
clear the form for the new entry. That block calls
`spinBox_lineMargin->setValue(-1)`,
`checkBox_perlSlashGOption->setChecked(false)`,
`checkBox_filterTrigger->setChecked(false)`,
`spinBox_stayOpen->setValue(0)`, and
`groupBox_triggerColorizer->setChecked(false)`.
Each of those widget changes fires the matching
`slot_saveProperty_Trigger*` slot synchronously. `mpCurrentTriggerItem`
is not updated to the new item until later in `addTrigger()`, so the
slots write the reset values into the *previously selected* trigger —
flipping `isMultiline` to false, clearing colorizer settings, etc.
Re-selecting the original trigger shows it as plain `OR / Multi-item`.
The fix sets `mBlockPropertySave = true` immediately before the UI reset
block. `slot_triggerSelected()` clears the flag once the new item is
loaded, restoring normal behaviour.
Fixes #9216
## Test plan
- [ ] Reproduce the original bug on a build without this patch
(multi-line trigger reverts to OR/Multi-item after adding a sibling)
- [ ] On a build with this patch, perform the same steps and confirm the
multi-line trigger keeps its `AND / Multi-line (within: N lines)`
setting
- [ ] Confirm the other trigger properties (perl /g, filter, stay open,
colorizer) are also not clobbered on the previous trigger when adding a
sibling
- [ ] Confirm property edits on the *new* trigger still produce undo
entries as before (the flag is cleared by `slot_triggerSelected`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Ethan Hussong <ethan@ethanhussong.com>
2026-07-13 11:37:57 -05:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 18:54:40 +02:00
|
|
|
// ========================================================================
|
|
|
|
|
// 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]);
|
|
|
|
|
}
|
|
|
|
|
|
fix: pasting several copied items now works for every item type (#9423)
#### Brief overview of PR changes/additions
- Pasting multiple copied triggers into a group created an extra copy of
the first trigger, and the surplus entry could not be deleted - the
redundant second reparent is now skipped
- For every other item type (aliases, timers, keys, scripts, buttons)
multi-item paste was worse: only the first copied item pasted at all (at
the top level), the rest were silently dropped - the multi-item
clipboard separator was only used by triggers; all six types now share
it and every pasted item is placed into the selected group
- Adds regression tests for trigger, alias, and key multi-paste
#### Motivation for adding to Mudlet
Copy/pasting several items at once has been broken since multi-select
was introduced: duplicates for triggers, dropped items for everything
else.
#### Other info (issues closed, discussion etc)
Fixes #8872
**Test case:** Create a group with two triggers, select both, Copy,
select the group, Paste - exactly two new triggers appear inside it
(previously three, with the first duplicated and undeletable). Repeat in
the Aliases view - both pasted aliases land inside the group (previously
one pasted at top level and the other vanished).
---------
Signed-off-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
Co-authored-by: Vadim Peretokin <vadim.peretokin@mudlet.org>
2026-07-18 11:27:20 +02:00
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 18:54:40 +02:00
|
|
|
// ========================================================================
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
fix: flag invalid aliases and block loops while typing them (#9499)
#### 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
2026-07-27 20:10:18 +02:00
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
// 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]);
|
|
|
|
|
}
|
2026-04-13 18:54:40 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#include "dlgTriggerEditorUndoRedoTest.moc"
|
|
|
|
|
QTEST_MAIN(dlgTriggerEditorUndoRedoTest)
|