mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add context menu icons, expand empty-area menu, add test_context_menu
- Right-click on empty area: Add Hex64, Add Struct, Append 128 bytes, Undo/Redo, Copy All as Text (with icons for each) - Right-click on node: icons for Edit Value, Rename, Change Type, Add Field, Add Child, Expand/Collapse, Duplicate, Delete, Copy Address, Copy Offset, Copy All as Text - Multi-select menu: icons for batch delete and batch change type - Add test_context_menu.cpp (20 tests): insert adds exactly one node, auto-offset placement, duplicate adds exactly one copy, duplicate preserves original, duplicate undo, struct duplicate no-op, root insert, append 128 bytes (16 nodes), append undo (macro), insert child, remove+undo, multiple duplicates, parent correctness, struct with children, batch remove, invalid parent, invalid index guards - Add vsicons to qrc: trash, clippy, link, diff-added, expand-all, collapse-all, rename, whole-word, list-selection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,4 +143,13 @@ if(BUILD_TESTING)
|
||||
target_include_directories(test_generator PRIVATE src)
|
||||
target_link_libraries(test_generator PRIVATE Qt6::Core Qt6::Test)
|
||||
add_test(NAME test_generator COMMAND test_generator)
|
||||
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui)
|
||||
target_include_directories(test_context_menu PRIVATE src)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
endif()
|
||||
|
||||
@@ -740,10 +740,19 @@ void RcxController::duplicateNode(int nodeIdx) {
|
||||
|
||||
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
int subLine, const QPoint& globalPos) {
|
||||
// Empty area or CommandRow: show limited menu
|
||||
auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); };
|
||||
|
||||
// Empty area or CommandRow: show full creation menu
|
||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) {
|
||||
QMenu menu;
|
||||
menu.addAction("Append 128 bytes", [this]() {
|
||||
menu.addAction(icon("add.svg"), "&Add Hex64 Field", [this]() {
|
||||
insertNode(0, -1, NodeKind::Hex64, "newField");
|
||||
});
|
||||
menu.addAction(icon("symbol-structure.svg"), "Add &Struct", [this]() {
|
||||
insertNode(0, -1, NodeKind::Struct, "NewClass");
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction(icon("diff-added.svg"), "Append &128 bytes", [this]() {
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes"));
|
||||
for (int i = 0; i < 16; i++)
|
||||
@@ -753,6 +762,17 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
m_suppressRefresh = false;
|
||||
refresh();
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction(icon("arrow-left.svg"), "&Undo", [this]() {
|
||||
m_doc->undoStack.undo();
|
||||
})->setEnabled(m_doc->undoStack.canUndo());
|
||||
menu.addAction(icon("arrow-right.svg"), "&Redo", [this]() {
|
||||
m_doc->undoStack.redo();
|
||||
})->setEnabled(m_doc->undoStack.canRedo());
|
||||
menu.addSeparator();
|
||||
menu.addAction(icon("clippy.svg"), "Copy All as &Text", [editor]() {
|
||||
QApplication::clipboard()->setText(editor->scintilla()->text());
|
||||
});
|
||||
menu.exec(globalPos);
|
||||
return;
|
||||
}
|
||||
@@ -771,7 +791,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
QMenu menu;
|
||||
int count = m_selIds.size();
|
||||
QSet<uint64_t> ids = m_selIds;
|
||||
menu.addAction(QString("Delete %1 nodes").arg(count), [this, ids]() {
|
||||
menu.addAction(icon("trash.svg"), QString("Delete %1 nodes").arg(count), [this, ids]() {
|
||||
QVector<int> indices;
|
||||
for (uint64_t id : ids) {
|
||||
int idx = m_doc->tree.indexOfId(id);
|
||||
@@ -779,7 +799,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
}
|
||||
batchRemoveNodes(indices);
|
||||
});
|
||||
menu.addAction(QString("Change type of %1 nodes...").arg(count),
|
||||
menu.addAction(icon("symbol-structure.svg"), QString("Change type of %1 nodes...").arg(count),
|
||||
[this, ids]() {
|
||||
QStringList types;
|
||||
for (const auto& e : kKindMeta) types << e.name;
|
||||
@@ -810,48 +830,54 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
&& node.kind != NodeKind::Padding && node.kind != NodeKind::Mat4x4
|
||||
&& m_doc->provider->isWritable();
|
||||
if (isEditable) {
|
||||
menu.addAction("Edit &Value\tEnter", [editor, line]() {
|
||||
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
|
||||
editor->beginInlineEdit(EditTarget::Value, line);
|
||||
});
|
||||
}
|
||||
|
||||
menu.addAction("Re&name\tF2", [editor, line]() {
|
||||
menu.addAction(icon("rename.svg"), "Re&name\tF2", [editor, line]() {
|
||||
editor->beginInlineEdit(EditTarget::Name, line);
|
||||
});
|
||||
|
||||
menu.addAction("Change &Type\tT", [editor, line]() {
|
||||
menu.addAction(icon("symbol-structure.svg"), "Change &Type\tT", [editor, line]() {
|
||||
editor->beginInlineEdit(EditTarget::Type, line);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction("&Add Field Below\tInsert", [this, parentId]() {
|
||||
menu.addAction(icon("add.svg"), "&Add Field Below\tInsert", [this, parentId]() {
|
||||
insertNode(parentId, -1, NodeKind::Hex64, "newField");
|
||||
});
|
||||
|
||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
||||
menu.addAction("Add &Child", [this, nodeId]() {
|
||||
menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
|
||||
insertNode(nodeId, 0, NodeKind::Hex64, "newField");
|
||||
});
|
||||
QString colText = node.collapsed ? "&Expand" : "&Collapse";
|
||||
menu.addAction(colText, [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) toggleCollapse(ni);
|
||||
});
|
||||
if (node.collapsed) {
|
||||
menu.addAction(icon("expand-all.svg"), "&Expand", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) toggleCollapse(ni);
|
||||
});
|
||||
} else {
|
||||
menu.addAction(icon("collapse-all.svg"), "&Collapse", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) toggleCollapse(ni);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu.addAction("D&uplicate\tCtrl+D", [this, nodeId]() {
|
||||
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) duplicateNode(ni);
|
||||
});
|
||||
menu.addAction("&Delete\tDelete", [this, nodeId]() {
|
||||
menu.addAction(icon("trash.svg"), "&Delete\tDelete", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) removeNode(ni);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction("Copy &Address", [this, nodeId]() {
|
||||
menu.addAction(icon("link.svg"), "Copy &Address", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
||||
@@ -859,7 +885,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
|
||||
});
|
||||
|
||||
menu.addAction("Copy &Offset", [this, nodeId]() {
|
||||
menu.addAction(icon("whole-word.svg"), "Copy &Offset", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
int off = m_doc->tree.nodes[ni].offset;
|
||||
@@ -867,7 +893,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
|
||||
});
|
||||
|
||||
menu.addAction("Copy All as &Text", [editor]() {
|
||||
menu.addAction(icon("clippy.svg"), "Copy All as &Text", [editor]() {
|
||||
QApplication::clipboard()->setText(editor->scintilla()->text());
|
||||
});
|
||||
|
||||
|
||||
@@ -31,5 +31,14 @@
|
||||
<file alias="code.svg">vsicons/code.svg</file>
|
||||
<file alias="export.svg">vsicons/export.svg</file>
|
||||
<file alias="preview.svg">vsicons/preview.svg</file>
|
||||
<file alias="trash.svg">vsicons/trash.svg</file>
|
||||
<file alias="clippy.svg">vsicons/clippy.svg</file>
|
||||
<file alias="link.svg">vsicons/link.svg</file>
|
||||
<file alias="diff-added.svg">vsicons/diff-added.svg</file>
|
||||
<file alias="expand-all.svg">vsicons/expand-all.svg</file>
|
||||
<file alias="collapse-all.svg">vsicons/collapse-all.svg</file>
|
||||
<file alias="rename.svg">vsicons/rename.svg</file>
|
||||
<file alias="whole-word.svg">vsicons/whole-word.svg</file>
|
||||
<file alias="selection.svg">vsicons/list-selection.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
400
tests/test_context_menu.cpp
Normal file
400
tests/test_context_menu.cpp
Normal file
@@ -0,0 +1,400 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QApplication>
|
||||
#include <QSplitter>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include "controller.h"
|
||||
#include "core.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
static void buildTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "Player";
|
||||
root.name = "Player";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
auto field = [&](int off, NodeKind k, const char* name) {
|
||||
Node n;
|
||||
n.kind = k;
|
||||
n.name = name;
|
||||
n.parentId = rootId;
|
||||
n.offset = off;
|
||||
tree.addNode(n);
|
||||
};
|
||||
|
||||
field(0, NodeKind::Int32, "health");
|
||||
field(4, NodeKind::Int32, "armor");
|
||||
field(8, NodeKind::Float, "speed");
|
||||
field(12, NodeKind::Hex32, "flags");
|
||||
}
|
||||
|
||||
static QByteArray makeBuffer() {
|
||||
QByteArray data(128, '\0');
|
||||
int32_t health = 100;
|
||||
memcpy(data.data() + 0, &health, 4);
|
||||
int32_t armor = 50;
|
||||
memcpy(data.data() + 4, &armor, 4);
|
||||
float speed = 3.5f;
|
||||
memcpy(data.data() + 8, &speed, 4);
|
||||
uint32_t flags = 0xFF00FF00;
|
||||
memcpy(data.data() + 12, &flags, 4);
|
||||
return data;
|
||||
}
|
||||
|
||||
class TestContextMenu : public QObject {
|
||||
Q_OBJECT
|
||||
private:
|
||||
RcxDocument* m_doc = nullptr;
|
||||
RcxController* m_ctrl = nullptr;
|
||||
QSplitter* m_splitter = nullptr;
|
||||
RcxEditor* m_editor = nullptr;
|
||||
|
||||
int findNode(const QString& name) const {
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++)
|
||||
if (m_doc->tree.nodes[i].name == name) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int countNodes() const { return m_doc->tree.nodes.size(); }
|
||||
|
||||
private slots:
|
||||
void init() {
|
||||
m_doc = new RcxDocument();
|
||||
buildTree(m_doc->tree);
|
||||
m_doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
m_splitter = new QSplitter();
|
||||
m_ctrl = new RcxController(m_doc, nullptr);
|
||||
m_editor = m_ctrl->addSplitEditor(m_splitter);
|
||||
|
||||
m_splitter->resize(800, 600);
|
||||
m_splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(m_splitter));
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
delete m_ctrl; m_ctrl = nullptr;
|
||||
m_editor = nullptr;
|
||||
delete m_splitter; m_splitter = nullptr;
|
||||
delete m_doc; m_doc = nullptr;
|
||||
}
|
||||
|
||||
// ── Insert adds exactly one node ──
|
||||
|
||||
void testInsertAddsOneNode() {
|
||||
int before = countNodes();
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "inserted");
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
|
||||
int idx = findNode("inserted");
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::Hex64);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offset, 16);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].parentId, rootId);
|
||||
}
|
||||
|
||||
// ── Insert at auto-offset places after last sibling ──
|
||||
|
||||
void testInsertAutoOffset() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
// Last child is "flags" at offset 12, size 4 → end = 16
|
||||
m_ctrl->insertNode(rootId, -1, NodeKind::Hex64, "autoPlaced");
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = findNode("autoPlaced");
|
||||
QVERIFY(idx >= 0);
|
||||
// Hex64 is 8-byte aligned, next aligned offset after 16 is 16
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offset, 16);
|
||||
}
|
||||
|
||||
// ── Duplicate creates exactly one copy ──
|
||||
|
||||
void testDuplicateAddsOneNode() {
|
||||
int flagsIdx = findNode("flags");
|
||||
QVERIFY(flagsIdx >= 0);
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->duplicateNode(flagsIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
|
||||
int copyIdx = findNode("flags_copy");
|
||||
QVERIFY2(copyIdx >= 0, "Expected a node named 'flags_copy'");
|
||||
QCOMPARE(m_doc->tree.nodes[copyIdx].kind, NodeKind::Hex32);
|
||||
QCOMPARE(m_doc->tree.nodes[copyIdx].offset, 16); // flags(12) + 4 = 16
|
||||
}
|
||||
|
||||
// ── Duplicate preserves original node unchanged ──
|
||||
|
||||
void testDuplicatePreservesOriginal() {
|
||||
int flagsIdx = findNode("flags");
|
||||
QVERIFY(flagsIdx >= 0);
|
||||
NodeKind origKind = m_doc->tree.nodes[flagsIdx].kind;
|
||||
int origOffset = m_doc->tree.nodes[flagsIdx].offset;
|
||||
QString origName = m_doc->tree.nodes[flagsIdx].name;
|
||||
|
||||
m_ctrl->duplicateNode(flagsIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Original should be unchanged (re-find in case index shifted)
|
||||
flagsIdx = findNode("flags");
|
||||
QVERIFY(flagsIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[flagsIdx].kind, origKind);
|
||||
QCOMPARE(m_doc->tree.nodes[flagsIdx].offset, origOffset);
|
||||
QCOMPARE(m_doc->tree.nodes[flagsIdx].name, origName);
|
||||
}
|
||||
|
||||
// ── Duplicate undo removes the copy ──
|
||||
|
||||
void testDuplicateUndo() {
|
||||
int before = countNodes();
|
||||
int flagsIdx = findNode("flags");
|
||||
QVERIFY(flagsIdx >= 0);
|
||||
|
||||
m_ctrl->duplicateNode(flagsIdx);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
QCOMPARE(findNode("flags_copy"), -1);
|
||||
}
|
||||
|
||||
// ── Duplicate on struct is no-op ──
|
||||
|
||||
void testDuplicateStructNoOp() {
|
||||
int rootIdx = findNode("Player");
|
||||
QVERIFY(rootIdx >= 0);
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->duplicateNode(rootIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
|
||||
// ── Insert at root level (parentId=0) ──
|
||||
|
||||
void testInsertAtRootLevel() {
|
||||
int before = countNodes();
|
||||
m_ctrl->insertNode(0, -1, NodeKind::Hex64, "rootField");
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
int idx = findNode("rootField");
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].parentId, (uint64_t)0);
|
||||
}
|
||||
|
||||
// ── Append 128 bytes adds exactly 16 Hex64 nodes ──
|
||||
|
||||
void testAppend128Bytes() {
|
||||
int before = countNodes();
|
||||
|
||||
// Simulate what "Append 128 bytes" does
|
||||
m_ctrl->document()->undoStack.beginMacro("Append 128 bytes");
|
||||
for (int i = 0; i < 16; i++)
|
||||
m_ctrl->insertNode(0, -1, NodeKind::Hex64,
|
||||
QStringLiteral("field_%1").arg(i));
|
||||
m_ctrl->document()->undoStack.endMacro();
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(countNodes(), before + 16);
|
||||
|
||||
// All should be root-level Hex64
|
||||
int foundCount = 0;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
const auto& n = m_doc->tree.nodes[i];
|
||||
if (n.name.startsWith("field_") && n.parentId == 0
|
||||
&& n.kind == NodeKind::Hex64) {
|
||||
foundCount++;
|
||||
}
|
||||
}
|
||||
QCOMPARE(foundCount, 16);
|
||||
}
|
||||
|
||||
// ── Append 128 bytes undo removes all 16 at once ──
|
||||
|
||||
void testAppend128BytesUndo() {
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->document()->undoStack.beginMacro("Append 128 bytes");
|
||||
for (int i = 0; i < 16; i++)
|
||||
m_ctrl->insertNode(0, -1, NodeKind::Hex64,
|
||||
QStringLiteral("field_%1").arg(i));
|
||||
m_ctrl->document()->undoStack.endMacro();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 16);
|
||||
|
||||
// Single undo undoes the entire macro
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
|
||||
// ── Insert child into struct ──
|
||||
|
||||
void testInsertChildIntoStruct() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->insertNode(rootId, 0, NodeKind::Hex64, "childField");
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
int idx = findNode("childField");
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].parentId, rootId);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].offset, 0);
|
||||
}
|
||||
|
||||
// ── Remove node then undo restores it ──
|
||||
|
||||
void testRemoveAndUndoNode() {
|
||||
int flagsIdx = findNode("flags");
|
||||
QVERIFY(flagsIdx >= 0);
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->removeNode(flagsIdx);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before - 1);
|
||||
QCOMPARE(findNode("flags"), -1);
|
||||
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
QVERIFY(findNode("flags") >= 0);
|
||||
}
|
||||
|
||||
// ── Multiple duplicates each add exactly one ──
|
||||
|
||||
void testMultipleDuplicates() {
|
||||
int before = countNodes();
|
||||
int healthIdx = findNode("health");
|
||||
QVERIFY(healthIdx >= 0);
|
||||
|
||||
m_ctrl->duplicateNode(healthIdx);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
|
||||
int copyIdx = findNode("health_copy");
|
||||
QVERIFY(copyIdx >= 0);
|
||||
|
||||
m_ctrl->duplicateNode(copyIdx);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 2);
|
||||
|
||||
int copy2Idx = findNode("health_copy_copy");
|
||||
QVERIFY(copy2Idx >= 0);
|
||||
}
|
||||
|
||||
// ── Duplicate copy has correct parent ──
|
||||
|
||||
void testDuplicateCopyParent() {
|
||||
int healthIdx = findNode("health");
|
||||
QVERIFY(healthIdx >= 0);
|
||||
uint64_t parentId = m_doc->tree.nodes[healthIdx].parentId;
|
||||
|
||||
m_ctrl->duplicateNode(healthIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
int copyIdx = findNode("health_copy");
|
||||
QVERIFY(copyIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[copyIdx].parentId, parentId);
|
||||
}
|
||||
|
||||
// ── Insert struct at root then add children ──
|
||||
|
||||
void testInsertStructAndChildren() {
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->insertNode(0, -1, NodeKind::Struct, "NewClass");
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
|
||||
int structIdx = findNode("NewClass");
|
||||
QVERIFY(structIdx >= 0);
|
||||
uint64_t structId = m_doc->tree.nodes[structIdx].id;
|
||||
|
||||
m_ctrl->insertNode(structId, 0, NodeKind::Int32, "x");
|
||||
m_ctrl->insertNode(structId, -1, NodeKind::Int32, "y");
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 3);
|
||||
|
||||
int xIdx = findNode("x");
|
||||
int yIdx = findNode("y");
|
||||
QVERIFY(xIdx >= 0);
|
||||
QVERIFY(yIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[xIdx].parentId, structId);
|
||||
QCOMPARE(m_doc->tree.nodes[yIdx].parentId, structId);
|
||||
}
|
||||
|
||||
// ── Batch remove deletes multiple nodes ──
|
||||
|
||||
void testBatchRemove() {
|
||||
int healthIdx = findNode("health");
|
||||
int armorIdx = findNode("armor");
|
||||
QVERIFY(healthIdx >= 0);
|
||||
QVERIFY(armorIdx >= 0);
|
||||
int before = countNodes();
|
||||
|
||||
m_ctrl->batchRemoveNodes({healthIdx, armorIdx});
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before - 2);
|
||||
QCOMPARE(findNode("health"), -1);
|
||||
QCOMPARE(findNode("armor"), -1);
|
||||
|
||||
// Undo restores both
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
QVERIFY(findNode("health") >= 0);
|
||||
QVERIFY(findNode("armor") >= 0);
|
||||
}
|
||||
|
||||
// ── Insert with invalid parent still works (root-level) ──
|
||||
|
||||
void testInsertInvalidParent() {
|
||||
int before = countNodes();
|
||||
// parentId=999 doesn't exist, but insertNode doesn't validate parent
|
||||
m_ctrl->insertNode(999, 0, NodeKind::Hex32, "orphan");
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before + 1);
|
||||
}
|
||||
|
||||
// ── Duplicate out-of-range index is no-op ──
|
||||
|
||||
void testDuplicateInvalidIndex() {
|
||||
int before = countNodes();
|
||||
m_ctrl->duplicateNode(-1);
|
||||
m_ctrl->duplicateNode(9999);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
|
||||
// ── Remove out-of-range index is no-op ──
|
||||
|
||||
void testRemoveInvalidIndex() {
|
||||
int before = countNodes();
|
||||
m_ctrl->removeNode(-1);
|
||||
m_ctrl->removeNode(9999);
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestContextMenu)
|
||||
#include "test_context_menu.moc"
|
||||
Reference in New Issue
Block a user