// XenoSettings persistence tests: the per-command key sequence override // map. Overrides round-trip through save/load via the settings JSON at // ${XDG_CONFIG_HOME}/xenomorphic/settings.json; init() points // XDG_CONFIG_HOME at a fresh temp dir per test so the user's real settings // file is never touched. #include #include "XenoSettings.h" #include #include #include #include #include #include class TestXenoSettings : public QObject { Q_OBJECT std::unique_ptr m_settings; // Recreated per test in init(): tests share one object, so the config // dir must reset too or a previous test's settings.json leaks in. std::unique_ptr m_configDir; [[nodiscard]] QString settingsPath() const { return QDir(m_configDir->path()).filePath( QStringLiteral("xenomorphic/settings.json")); } private slots: void init(); // Override round-trips through save and load. void overrideRoundTripsThroughSaveAndLoad(); // An absent entry means default: no key, and empty overrides load to an // empty map. void absentEntryMeansDefault(); // Clearing an entry (single and all) removes it and it survives reload. void clearingRestoresDefault(); // Malformed JSON entries (non-string, empty values) are dropped on load. void malformedEntriesDroppedOnLoad(); // The editor's own font size and line height round-trip through save/load. void editorFontSettingsRoundTrip(); // The line-height setter clamps to [1.0, 3.0], and the clamped value // survives a reload. void editorLineHeightClamps(); // A settings file without the editor keys leaves the defaults (0.0, 1.3). void absentEditorKeysUseDefaults(); }; void TestXenoSettings::init() { m_configDir = std::make_unique(); QVERIFY(m_configDir->isValid()); qputenv("XDG_CONFIG_HOME", m_configDir->path().toUtf8()); m_settings = std::make_unique(); } void TestXenoSettings::overrideRoundTripsThroughSaveAndLoad() { m_settings->setCommandSequence(QStringLiteral("splitHorizontal"), QStringLiteral("Ctrl+Alt+H")); QCOMPARE(m_settings->commandSequences().value( QStringLiteral("splitHorizontal")), QStringLiteral("Ctrl+Alt+H")); // The JSON file carries the key and value. QFile file(settingsPath()); QVERIFY(file.open(QIODevice::ReadOnly)); const QJsonObject object = QJsonDocument::fromJson(file.readAll()).object(); file.close(); const QJsonValue entry = object.value(QStringLiteral("commandSequences")); QVERIFY(entry.isObject()); QCOMPARE(entry.toObject().value(QStringLiteral("splitHorizontal")), QStringLiteral("Ctrl+Alt+H")); // A fresh instance loads the override back. const XenoSettings reloaded; QCOMPARE(reloaded.commandSequences().value( QStringLiteral("splitHorizontal")), QStringLiteral("Ctrl+Alt+H")); } void TestXenoSettings::absentEntryMeansDefault() { // Nothing written: no overrides. QVERIFY(m_settings->commandSequences().isEmpty()); // Existing settings keys (fontFamily) load; commandSequences absent from // the file stays empty. const XenoSettings reloaded; QVERIFY(reloaded.commandSequences().isEmpty()); } void TestXenoSettings::clearingRestoresDefault() { m_settings->setCommandSequence(QStringLiteral("quit"), QStringLiteral("Ctrl+Shift+Q")); m_settings->setCommandSequence(QStringLiteral("closePane"), QStringLiteral("Ctrl+Alt+K")); QCOMPARE(m_settings->commandSequences().size(), 2); // Clear one. m_settings->clearCommandSequence(QStringLiteral("quit")); QVERIFY(!m_settings->commandSequences().contains(QStringLiteral("quit"))); QCOMPARE(m_settings->commandSequences().value(QStringLiteral("closePane")), QStringLiteral("Ctrl+Alt+K")); const XenoSettings afterSingle; QVERIFY(!afterSingle.commandSequences().contains(QStringLiteral("quit"))); // Clear all. m_settings->clearAllCommandSequences(); QVERIFY(m_settings->commandSequences().isEmpty()); const XenoSettings afterAll; QVERIFY(afterAll.commandSequences().isEmpty()); // Setting "" clears too. m_settings->setCommandSequence(QStringLiteral("closePane"), QString()); QVERIFY(m_settings->commandSequences().isEmpty()); } void TestXenoSettings::malformedEntriesDroppedOnLoad() { QDir().mkpath(QDir(m_configDir->path()).filePath(QStringLiteral("xenomorphic"))); QFile file(settingsPath()); QVERIFY(file.open(QIODevice::WriteOnly)); file.write(QByteArrayLiteral( R"({"commandSequences": {"good": "Ctrl+G", "empty": "", "bad": 12, "": "Ctrl+X"}})")); file.close(); const XenoSettings reloaded; QCOMPARE(reloaded.commandSequences().size(), 1); QCOMPARE(reloaded.commandSequences().value(QStringLiteral("good")), QStringLiteral("Ctrl+G")); } void TestXenoSettings::editorFontSettingsRoundTrip() { QSignalSpy sizeSpy(m_settings.get(), &XenoSettings::editorFontSizeChanged); QSignalSpy heightSpy(m_settings.get(), &XenoSettings::editorLineHeightChanged); QSignalSpy wrapSpy(m_settings.get(), &XenoSettings::editorWrapChanged); m_settings->setEditorFontSize(13.5); m_settings->setEditorLineHeight(1.6); m_settings->setEditorWrap(true); QCOMPARE(sizeSpy.count(), 1); QCOMPARE(heightSpy.count(), 1); QCOMPARE(wrapSpy.count(), 1); QVERIFY(qFuzzyCompare(m_settings->editorFontSize(), 13.5)); QVERIFY(qFuzzyCompare(m_settings->editorLineHeight(), 1.6)); QVERIFY(m_settings->editorWrap()); // The JSON file carries both keys. QFile file(settingsPath()); QVERIFY(file.open(QIODevice::ReadOnly)); const QJsonObject object = QJsonDocument::fromJson(file.readAll()).object(); file.close(); QVERIFY(qFuzzyCompare( object.value(QStringLiteral("editorFontSize")).toDouble(), 13.5)); QVERIFY(qFuzzyCompare( object.value(QStringLiteral("editorLineHeight")).toDouble(), 1.6)); QVERIFY(object.value(QStringLiteral("editorWrap")).toBool()); // A fresh instance loads both back. const XenoSettings reloaded; QVERIFY(qFuzzyCompare(reloaded.editorFontSize(), 13.5)); QVERIFY(qFuzzyCompare(reloaded.editorLineHeight(), 1.6)); QVERIFY(reloaded.editorWrap()); } void TestXenoSettings::editorLineHeightClamps() { m_settings->setEditorLineHeight(0.5); QVERIFY(qFuzzyCompare(m_settings->editorLineHeight(), 1.0)); m_settings->setEditorLineHeight(9.0); QVERIFY(qFuzzyCompare(m_settings->editorLineHeight(), 3.0)); // The file carries the clamped value, and so does a reload. const XenoSettings reloaded; QVERIFY(qFuzzyCompare(reloaded.editorLineHeight(), 3.0)); } void TestXenoSettings::absentEditorKeysUseDefaults() { // A fresh object loaded no file, so the in-class defaults stand. QVERIFY(qFuzzyIsNull(m_settings->editorFontSize())); QVERIFY(qFuzzyCompare(m_settings->editorLineHeight(), 1.3)); QVERIFY(!m_settings->editorWrap()); // A file that omits both keys must not disturb them either. QDir().mkpath(QDir(m_configDir->path()).filePath(QStringLiteral("xenomorphic"))); QFile file(settingsPath()); QVERIFY(file.open(QIODevice::WriteOnly)); file.write(QByteArrayLiteral(R"({"fontSize": 12.0})")); file.close(); const XenoSettings reloaded; QVERIFY(qFuzzyIsNull(reloaded.editorFontSize())); QVERIFY(qFuzzyCompare(reloaded.editorLineHeight(), 1.3)); QVERIFY(!reloaded.editorWrap()); } QTEST_MAIN(TestXenoSettings) #include "tst_settings.moc"