feat: fix heatmap false-heat on offset shift, hover flicker, type chooser cleanup

- Clear value history when node offsets change (insert/delete/resize/
  manual offset edit) so stale values from old addresses don't show
  false heat coloring
- Invalidate in-flight async reads (bump refreshGen) when tree layout
  changes, preventing stale snapshot data from re-introducing heat
- Fix command bar hover cursor flicker: remove premature
  applyHoverCursor() from applyDocument() — runs correctly via
  applySelectionOverlays() after text is finalized
- Fix hover indicator survival: reorder refresh() so text-modifying
  passes (updateCommandRow) run before overlay passes
- Guard synthetic Leave events during setText() to preserve hover state
- Remove primitives from type chooser when pointer modifier (* / **)
  is active; remove primitives entirely in Root command bar mode
- Add test_editor and test_controller test coverage for heat clearing,
  hover survival, and mixed hex/non-hex type scenarios
This commit is contained in:
IChooseYou
2026-02-17 11:41:46 -07:00
parent 5ae9ca0979
commit 1c3b4af045
18 changed files with 996 additions and 498 deletions

View File

@@ -8,6 +8,26 @@
using namespace rcx;
// Provider with a configurable base address (for testing source-switch logic)
class BaseAwareProvider : public Provider {
QByteArray m_data;
uint64_t m_base;
public:
BaseAwareProvider(QByteArray data, uint64_t base)
: m_data(std::move(data)), m_base(base) {}
bool read(uint64_t addr, void* buf, int len) const override {
if (addr + len > (uint64_t)m_data.size()) return false;
std::memcpy(buf, m_data.constData() + addr, len);
return true;
}
int size() const override { return m_data.size(); }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
bool isLive() const override { return true; }
QString name() const override { return QStringLiteral("test"); }
QString kind() const override { return QStringLiteral("Process"); }
};
// Small tree: one root struct with a few typed fields at known offsets.
// Keeps tests fast and deterministic (no giant PEB tree).
static void buildSmallTree(NodeTree& tree) {
@@ -383,6 +403,48 @@ private slots:
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
}
// ── Test: source switch preserves existing base address ──
void testSourceSwitchPreservesBase() {
// Document already has baseAddress = 0x1000 from buildSmallTree()
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x400000);
uint64_t newBase = prov->base();
QCOMPARE(newBase, (uint64_t)0x400000);
m_doc->provider = prov;
// This is the controller logic under test:
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// baseAddress must stay at the original value
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// provider base must be synced to match
QCOMPARE(m_doc->provider->base(), (uint64_t)0x1000);
}
// ── Test: source switch on fresh doc uses provider default ──
void testSourceSwitchFreshDocUsesProviderBase() {
// Simulate a fresh document (no loaded .rcx → baseAddress == 0)
m_doc->tree.baseAddress = 0;
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x7FFE0000);
uint64_t newBase = prov->base();
m_doc->provider = prov;
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// Fresh doc should adopt the provider's default base
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
QCOMPARE(m_doc->provider->base(), (uint64_t)0x7FFE0000);
}
// ── Test: toggleCollapse + undo ──
void testToggleCollapse() {
// Root is index 0, a Struct node
@@ -406,6 +468,181 @@ private slots:
QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
}
// ── Test: value history popup only appears during inline editing ──
void testValueHistoryPopupOnlyDuringEdit() {
// Record value history for field_u32 so it has heat
auto& tree = m_doc->tree;
int idx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == "field_u32") { idx = i; break; }
}
QVERIFY(idx >= 0);
uint64_t nodeId = tree.nodes[idx].id;
QHash<uint64_t, ValueHistory> history;
history[nodeId].record("100");
history[nodeId].record("200");
history[nodeId].record("300");
QVERIFY(history[nodeId].uniqueCount() > 1);
m_editor->setValueHistoryRef(&history);
// Refresh and compose so editor has meta with heatLevel
m_ctrl->refresh();
QApplication::processEvents();
ComposeResult result = m_doc->compose();
// Manually set heat on the node's line meta
for (auto& lm : result.meta) {
if (lm.nodeId == nodeId) lm.heatLevel = 2;
}
m_editor->applyDocument(result);
QApplication::processEvents();
// Popup should not exist or not be visible (no editing active)
auto* popup = m_editor->findChild<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
// Even if popup widget exists, it should not be visible
bool popupVisible = false;
for (auto* child : m_editor->findChildren<QFrame*>(QString(), Qt::FindDirectChildrenOnly)) {
if (child->isVisible() && child->windowFlags() & Qt::ToolTip)
popupVisible = true;
}
QVERIFY2(!popupVisible, "Popup should not be visible when not editing");
// Start inline edit on value column of field_u32
int fieldLine = -1;
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].nodeId == nodeId && result.meta[i].lineKind == LineKind::Field) {
fieldLine = i; break;
}
}
QVERIFY(fieldLine >= 0);
bool ok = m_editor->beginInlineEdit(EditTarget::Value, fieldLine);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Trigger hover cursor update (simulates mouse move during editing)
QApplication::processEvents();
// Cancel edit to clean up
m_editor->cancelInlineEdit();
QApplication::processEvents();
m_editor->setValueHistoryRef(nullptr);
}
// ── Test: delete node clears value history for shifted siblings ──
void testDeleteClearsHeatForShiftedNodes() {
// Replace with a live provider so refresh() actually records values
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0x1000);
m_ctrl->refresh();
QApplication::processEvents();
auto& tree = m_doc->tree;
// Locate field_u32 (the node we'll delete) and the siblings after it.
// The small tree has: field_u32(0), field_float(4), field_u8(8),
// pad0/Hex16(9), pad1/Hex8(11), field_hex/Hex32(12)
// field_float and field_u8 are regular (non-hex) types.
int delIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == "field_u32") { delIdx = i; break; }
}
QVERIFY(delIdx >= 0);
uint64_t delId = tree.nodes[delIdx].id;
// Collect sibling node IDs that come after field_u32 (will be shifted)
uint64_t parentId = tree.nodes[delIdx].parentId;
int deletedSize = tree.nodes[delIdx].byteSize(); // 4 bytes
int deletedEnd = tree.nodes[delIdx].offset + deletedSize;
QVector<uint64_t> shiftedIds;
QHash<uint64_t, QString> nameMap; // for debug messages
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].parentId == parentId && i != delIdx
&& tree.nodes[i].offset >= deletedEnd) {
shiftedIds.append(tree.nodes[i].id);
nameMap[tree.nodes[i].id] = tree.nodes[i].name;
}
}
QVERIFY2(!shiftedIds.isEmpty(), "Should have siblings after field_u32");
// Seed value history for shifted siblings (simulate accumulated heat)
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
for (uint64_t id : shiftedIds) {
history[id].record("old_val_1");
history[id].record("old_val_2");
history[id].record("old_val_3");
QVERIFY2(history[id].heatLevel() >= 2,
qPrintable(QString("Pre-delete: %1 should have heat>=2")
.arg(nameMap[id])));
}
// Also seed the to-be-deleted node
history[delId].record("del_1");
history[delId].record("del_2");
QVERIFY(history.contains(delId));
// Delete field_u32 — this shifts all subsequent siblings
m_ctrl->removeNode(delIdx);
QApplication::processEvents();
// The deleted node's history should be gone
QVERIFY2(!m_ctrl->valueHistory().contains(delId),
"Deleted node's value history should be cleared");
// All shifted siblings should have heat=0 after the delete.
// With a live provider, refresh() inside removeNode re-records one new
// value at the new offset → count=1 → heatLevel=0.
for (uint64_t id : shiftedIds) {
int heat = m_ctrl->valueHistory().contains(id)
? m_ctrl->valueHistory()[id].heatLevel() : 0;
QVERIFY2(heat == 0,
qPrintable(QString("Shifted node '%1' (id=%2) should have heat=0, got %3")
.arg(nameMap[id]).arg(id).arg(heat)));
}
}
// ── Test: value history records and cycles correctly ──
void testValueHistoryRingBuffer() {
ValueHistory vh;
QCOMPARE(vh.count, 0);
QCOMPARE(vh.heatLevel(), 0);
vh.record("10");
QCOMPARE(vh.count, 1);
QCOMPARE(vh.heatLevel(), 0); // 1 unique = static
// Duplicate should not increase count
vh.record("10");
QCOMPARE(vh.count, 1);
vh.record("20");
QCOMPARE(vh.count, 2);
QCOMPARE(vh.heatLevel(), 1); // cold
vh.record("30");
QCOMPARE(vh.count, 3);
QCOMPARE(vh.heatLevel(), 2); // warm
vh.record("40");
vh.record("50");
QCOMPARE(vh.count, 5);
QCOMPARE(vh.heatLevel(), 3); // hot
QCOMPARE(vh.last(), QString("50"));
// Ring buffer: uniqueCount() caps at kCapacity
for (int i = 0; i < 20; i++)
vh.record(QString::number(100 + i));
QCOMPARE(vh.uniqueCount(), ValueHistory::kCapacity);
QVERIFY(vh.count > ValueHistory::kCapacity);
// forEach iterates oldest→newest within ring
QStringList vals;
vh.forEach([&](const QString& v) { vals.append(v); });
QCOMPARE(vals.size(), ValueHistory::kCapacity);
QCOMPARE(vals.last(), vh.last());
}
};
QTEST_MAIN(TestController)

View File

@@ -999,6 +999,144 @@ private slots:
"Root header should be suppressed from compose output");
}
// ── Test: command row hover indicator survives refresh cycle ──
void testCommandRowHoverSurvivesRefresh() {
// IND_HOVER_SPAN = 11 (defined in editor.cpp, replicate for test)
constexpr int IND_HOVER_SPAN = 11;
m_editor->applyDocument(m_result);
// Set command row text (simulates controller.updateCommandRow)
QString cmdText = QStringLiteral(
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {");
m_editor->setCommandRowText(cmdText);
QApplication::processEvents();
// Parse the source span on line 0
auto* sci = m_editor->scintilla();
int len = (int)sci->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
QVERIFY(len > 0);
QByteArray buf(len + 1, '\0');
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)0,
(void*)buf.data());
QString lineText = QString::fromUtf8(buf.constData(), len);
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
lineText.chop(1);
ColumnSpan srcSpan = commandRowSrcSpan(lineText);
QVERIFY2(srcSpan.valid, "Source span should be valid on command row");
// Programmatically move mouse to the source span
int hoverCol = srcSpan.start + 1;
QPoint hoverPos = colToViewport(sci, 0, hoverCol);
sendMouseMove(sci->viewport(), hoverPos);
QApplication::processEvents();
// Verify IND_HOVER_SPAN is set at the hover position
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
(unsigned long)0, (long)hoverCol);
sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT,
(unsigned long)IND_HOVER_SPAN);
int valBefore = (int)sci->SendScintilla(
QsciScintillaBase::SCI_INDICATORVALUEAT,
(unsigned long)IND_HOVER_SPAN, pos);
QVERIFY2(valBefore != 0,
"IND_HOVER_SPAN should be set on source span after hover");
// Verify cursor is PointingHand (Source target = clickable)
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
// ── Simulate a full refresh cycle (same order as controller.refresh) ──
ViewState vs = m_editor->saveViewState();
m_editor->applyDocument(m_result);
m_editor->restoreViewState(vs);
// Cursor must NOT have flipped to Arrow during applyDocument
// (applyHoverCursor is not called prematurely on composed text)
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
// updateCommandRow() — replaces line 0 text
m_editor->setCommandRowText(cmdText);
// applySelectionOverlays() — must run AFTER updateCommandRow
m_editor->applySelectionOverlay(QSet<uint64_t>());
QApplication::processEvents();
// Re-query the position (text was replaced, byte offset may have shifted)
long posAfter = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
(unsigned long)0, (long)hoverCol);
int valAfter = (int)sci->SendScintilla(
QsciScintillaBase::SCI_INDICATORVALUEAT,
(unsigned long)IND_HOVER_SPAN, posAfter);
QVERIFY2(valAfter != 0,
"IND_HOVER_SPAN must survive refresh on command row "
"(hover should not flicker)");
// Cursor must still be PointingHand after full refresh cycle
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
m_editor->applyDocument(m_result);
}
// ── Test: command row hover survives multiple rapid refresh cycles ──
void testCommandRowHoverSurvivesRepeatedRefresh() {
constexpr int IND_HOVER_SPAN = 11;
m_editor->applyDocument(m_result);
QString cmdText = QStringLiteral(
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {");
m_editor->setCommandRowText(cmdText);
QApplication::processEvents();
auto* sci = m_editor->scintilla();
int lineLen = (int)sci->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
QByteArray buf(lineLen + 1, '\0');
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)0,
(void*)buf.data());
QString lineText = QString::fromUtf8(buf.constData(), lineLen);
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
lineText.chop(1);
ColumnSpan srcSpan = commandRowSrcSpan(lineText);
QVERIFY(srcSpan.valid);
int hoverCol = srcSpan.start + 1;
// Move mouse into position
QPoint hoverPos = colToViewport(sci, 0, hoverCol);
sendMouseMove(sci->viewport(), hoverPos);
QApplication::processEvents();
// Simulate 5 rapid refresh cycles (like ~660ms timer x5)
for (int cycle = 0; cycle < 5; cycle++) {
ViewState vs = m_editor->saveViewState();
m_editor->applyDocument(m_result);
m_editor->restoreViewState(vs);
m_editor->setCommandRowText(cmdText);
m_editor->applySelectionOverlay(QSet<uint64_t>());
// Re-send mouse move each cycle (mouse is still there physically)
sendMouseMove(sci->viewport(), hoverPos);
QApplication::processEvents();
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
(unsigned long)0, (long)hoverCol);
int val = (int)sci->SendScintilla(
QsciScintillaBase::SCI_INDICATORVALUEAT,
(unsigned long)IND_HOVER_SPAN, pos);
QVERIFY2(val != 0,
qPrintable(QString(
"IND_HOVER_SPAN lost on refresh cycle %1").arg(cycle)));
QVERIFY2(viewportCursor(m_editor) == Qt::PointingHandCursor,
qPrintable(QString(
"Cursor flipped away from PointingHand on cycle %1").arg(cycle)));
}
m_editor->applyDocument(m_result);
}
// ── Test: MenuBarStyle gives QMenu items generous click targets ──
// ── Test: M_ACCENT marker appears on selected rows ──
void testAccentMarkerOnSelectedRows() {
@@ -1117,6 +1255,157 @@ private slots:
.arg(styled.height()).arg(base.height())));
}
// ── Test: non-hex nodes don't show false heat coloring after offset shift ──
void testDeleteClearsHeatOnShiftedNodes() {
// Heat indicator constants (replicated from editor.cpp)
constexpr int IND_HEAT_COLD = 13;
constexpr int IND_HEAT_WARM = 17;
constexpr int IND_HEAT_HOT = 18;
// Build a small tree: root struct with mixed regular (non-hex) + hex fields
NodeTree tree;
tree.baseAddress = 0x1000;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "SmallStruct";
root.name = "s";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// field0: UInt32 at offset 0 (4 bytes) — will be deleted
// field1: UInt32 at offset 4 (4 bytes) — regular type, will shift
// field2: Float at offset 8 (4 bytes) — regular type, will shift
// field3: Hex32 at offset 12 (4 bytes) — hex type, will shift
struct FieldDef { int off; NodeKind kind; const char* name; };
FieldDef defs[] = {
{ 0, NodeKind::UInt32, "count"},
{ 4, NodeKind::UInt32, "flags"},
{ 8, NodeKind::Float, "speed"},
{12, NodeKind::Hex32, "raw"},
};
QVector<uint64_t> fieldIds;
for (auto& d : defs) {
Node n;
n.kind = d.kind;
n.name = d.name;
n.parentId = rootId;
n.offset = d.off;
int idx = tree.addNode(n);
fieldIds.append(tree.nodes[idx].id);
}
// Create a provider with 16 bytes of recognizable data
QByteArray data(16, '\0');
uint32_t v0 = 42; memcpy(data.data() + 0, &v0, 4); // count=42
uint32_t v1 = 0xFF; memcpy(data.data() + 4, &v1, 4); // flags=255
float v2 = 3.14f; memcpy(data.data() + 8, &v2, 4); // speed=3.14
uint32_t v3 = 0xCAFE; memcpy(data.data() + 12, &v3, 4); // raw=0xCAFE
BufferProvider prov(data);
// Compose the initial document
ComposeResult result = compose(tree, prov);
// Inject heatLevel=2 (warm) on field1, field2, field3 — simulates
// heat accumulated before the delete
for (auto& lm : result.meta) {
for (int i = 1; i <= 3; i++) {
if (lm.nodeId == fieldIds[i])
lm.heatLevel = 2;
}
}
// Apply to editor — heat indicators should appear
m_editor->applyDocument(result);
QApplication::processEvents();
auto* sci = m_editor->scintilla();
// Helper: check if any heat indicator is set anywhere on a line
auto hasHeatOnLine = [&](int line) -> bool {
int lineLen = (int)sci->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
long lineStart = sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
for (long pos = lineStart; pos < lineStart + lineLen; pos++) {
for (int ind : { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT }) {
int val = (int)sci->SendScintilla(
QsciScintillaBase::SCI_INDICATORVALUEAT,
(unsigned long)ind, pos);
if (val != 0) return true;
}
}
return false;
};
// Find lines for each shifted field
auto findFieldLine = [&](const ComposeResult& cr, uint64_t nodeId) -> int {
for (int i = 0; i < cr.meta.size(); i++) {
if (cr.meta[i].nodeId == nodeId && cr.meta[i].lineKind == LineKind::Field)
return i;
}
return -1;
};
int line1 = findFieldLine(result, fieldIds[1]);
int line2 = findFieldLine(result, fieldIds[2]);
int line3 = findFieldLine(result, fieldIds[3]);
QVERIFY(line1 >= 0);
QVERIFY(line2 >= 0);
QVERIFY(line3 >= 0);
// Verify heat indicators ARE present (UInt32, Float, and Hex32)
QVERIFY2(hasHeatOnLine(line1),
"Heat should be present on UInt32 'flags' before delete");
QVERIFY2(hasHeatOnLine(line2),
"Heat should be present on Float 'speed' before delete");
QVERIFY2(hasHeatOnLine(line3),
"Heat should be present on Hex32 'raw' before delete");
// ── Simulate delete of field0 (UInt32 'count' at offset 0) ──
int field0Idx = tree.indexOfId(fieldIds[0]);
QVERIFY(field0Idx >= 0);
tree.nodes.remove(field0Idx);
tree.invalidateIdCache();
// Shift remaining fields' offsets down by 4
for (int i = 1; i <= 3; i++) {
int fi = tree.indexOfId(fieldIds[i]);
if (fi >= 0) tree.nodes[fi].offset -= 4;
}
// Recompose — heatLevel defaults to 0 (simulates cleared history)
ComposeResult afterResult = compose(tree, prov);
// Apply the post-delete document to the editor
m_editor->applyDocument(afterResult);
QApplication::processEvents();
// Find new line positions
int newLine1 = findFieldLine(afterResult, fieldIds[1]);
int newLine2 = findFieldLine(afterResult, fieldIds[2]);
int newLine3 = findFieldLine(afterResult, fieldIds[3]);
QVERIFY(newLine1 >= 0);
QVERIFY(newLine2 >= 0);
QVERIFY(newLine3 >= 0);
// After applying heatLevel=0, NO heat indicators should appear
QVERIFY2(!hasHeatOnLine(newLine1),
"UInt32 'flags' should NOT show heat after offset shift "
"(old values are from wrong address)");
QVERIFY2(!hasHeatOnLine(newLine2),
"Float 'speed' should NOT show heat after offset shift "
"(old values are from wrong address)");
QVERIFY2(!hasHeatOnLine(newLine3),
"Hex32 'raw' should NOT show heat after offset shift "
"(old values are from wrong address)");
// Restore original document
m_editor->applyDocument(m_result);
}
void testMenuHoverRendersAmberText() {
// Replicate MenuBarStyle with drawControl hover override
class TestMenuStyle : public QProxyStyle {

View File

@@ -8,6 +8,7 @@
#include <QPushButton>
#include <QGroupBox>
#include <QLineEdit>
#include <QSpinBox>
#include <QLabel>
#include "optionsdialog.h"
#include "themes/thememanager.h"
@@ -222,6 +223,45 @@ private slots:
QVERIFY(!aiItem->isHidden());
}
void refreshRateSpinBoxExists() {
OptionsResult defaults;
defaults.refreshMs = 660;
OptionsDialog dlg(defaults);
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
QVERIFY(spin);
QCOMPARE(spin->value(), 660);
QCOMPARE(spin->minimum(), 1);
QCOMPARE(spin->maximum(), 60000);
}
void refreshRateResultReflectsInput() {
OptionsResult input;
input.refreshMs = 200;
OptionsDialog dlg(input);
auto r = dlg.result();
QCOMPARE(r.refreshMs, 200);
// Change via spin box
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
QVERIFY(spin);
spin->setValue(100);
r = dlg.result();
QCOMPARE(r.refreshMs, 100);
}
void refreshRateClampsMin() {
OptionsResult input;
input.refreshMs = 0; // below minimum
OptionsDialog dlg(input);
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
QVERIFY(spin);
// QSpinBox clamps to minimum
QCOMPARE(spin->value(), 1);
}
void dialogInheritsPalette() {
auto& tm = ThemeManager::instance();
const auto& theme = tm.current();