Compare commits

...

1 Commits

Author SHA1 Message Date
Carlos Zamora
af68d54bd1 Add more settings model unit tests 2026-04-14 17:15:37 -07:00
3 changed files with 429 additions and 0 deletions

View File

@@ -1843,6 +1843,15 @@ namespace SettingsModelUnitTests
VERIFY_ARE_EQUAL(settings->ProfileDefaults().HasTabTitle(), copyImpl->ProfileDefaults().HasTabTitle());
VERIFY_ARE_NOT_EQUAL(settings->ProfileDefaults().TabTitle(), copyImpl->ProfileDefaults().TabTitle());
// Verify HasXxx independence: setting a previously-inherited value on the clone
// should make HasXxx true on the clone but remain false on the original.
// SnapOnInput is not set in the JSON, so both should inherit the default.
VERIFY_IS_FALSE(settings->AllProfiles().GetAt(0).HasSnapOnInput());
VERIFY_IS_FALSE(copyImpl->AllProfiles().GetAt(0).HasSnapOnInput());
copyImpl->AllProfiles().GetAt(0).SnapOnInput(false);
VERIFY_IS_FALSE(settings->AllProfiles().GetAt(0).HasSnapOnInput());
VERIFY_IS_TRUE(copyImpl->AllProfiles().GetAt(0).HasSnapOnInput());
Log::Comment(L"Test empty profiles.defaults");
static constexpr std::string_view emptyPDJson{ R"(
{

View File

@@ -31,6 +31,10 @@ namespace SettingsModelUnitTests
TEST_METHOD(TestGenGuidsForProfiles);
TEST_METHOD(TestCorrectOldDefaultShellPaths);
TEST_METHOD(ProfileDefaultsProhibitedSettings);
TEST_METHOD(SettingInheritanceFallback);
TEST_METHOD(ClearSettingRestoresInheritance);
TEST_METHOD(HasSettingAtSpecificLayer);
};
void ProfileTests::ProfileGeneratesGuid()
@@ -532,4 +536,130 @@ namespace SettingsModelUnitTests
VERIFY_ARE_NOT_EQUAL(L"Default Profile Source", allProfiles.GetAt(2).Source());
VERIFY_ARE_NOT_EQUAL(L"foo.exe", allProfiles.GetAt(2).Commandline());
}
void ProfileTests::SettingInheritanceFallback()
{
// Verify that when no layer defines a setting, the default value is used.
// Also verify that when only user defaults defines it, profiles inherit from there.
static constexpr std::string_view userSettings{ R"({
"profiles": {
"defaults": {
"historySize": 5000
},
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"snapOnInput": false
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userSettings);
const auto allProfiles = settings->AllProfiles();
VERIFY_ARE_EQUAL(2u, allProfiles.Size());
// profile0: historySize inherited from defaults
VERIFY_ARE_EQUAL(5000, allProfiles.GetAt(0).HistorySize());
// profile0: snapOnInput not set anywhere, falls back to default (true)
VERIFY_ARE_EQUAL(true, allProfiles.GetAt(0).SnapOnInput());
// profile1: historySize inherited from defaults
VERIFY_ARE_EQUAL(5000, allProfiles.GetAt(1).HistorySize());
// profile1: snapOnInput explicitly set to false
VERIFY_ARE_EQUAL(false, allProfiles.GetAt(1).SnapOnInput());
}
void ProfileTests::ClearSettingRestoresInheritance()
{
// Verify that clearing a setting at the profile layer causes it to
// fall back to the parent's value.
static constexpr std::string_view parentString{ R"({
"name": "parent",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"tabTitle": "ParentTitle"
})" };
static constexpr std::string_view childString{ R"({
"name": "child",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 2000,
"tabTitle": "ChildTitle"
})" };
const auto parentJson = VerifyParseSucceeded(parentString);
const auto childJson = VerifyParseSucceeded(childString);
auto parent = implementation::Profile::FromJson(parentJson);
auto child = parent->CreateChild();
child->LayerJson(childJson);
// Verify child has its own values
VERIFY_ARE_EQUAL(2000, child->HistorySize());
VERIFY_ARE_EQUAL(L"ChildTitle", child->TabTitle());
VERIFY_IS_TRUE(child->HasHistorySize());
VERIFY_IS_TRUE(child->HasTabTitle());
// Clear historySize on child: should fall back to parent
child->ClearHistorySize();
VERIFY_IS_FALSE(child->HasHistorySize());
VERIFY_ARE_EQUAL(1000, child->HistorySize());
// Clear tabTitle on child: should fall back to parent
child->ClearTabTitle();
VERIFY_IS_FALSE(child->HasTabTitle());
VERIFY_ARE_EQUAL(L"ParentTitle", child->TabTitle());
}
void ProfileTests::HasSettingAtSpecificLayer()
{
// Verify that HasXxx() correctly reports whether a setting is defined
// at the current layer vs inherited from a parent.
static constexpr std::string_view userSettings{ R"({
"profiles": {
"defaults": {
"historySize": 5000,
"tabTitle": "DefaultTitle"
},
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 9001
},
{
"name": "profile1",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(userSettings);
const auto allProfiles = settings->AllProfiles();
VERIFY_ARE_EQUAL(2u, allProfiles.Size());
// profile0: historySize is explicitly set
VERIFY_IS_TRUE(allProfiles.GetAt(0).HasHistorySize());
VERIFY_ARE_EQUAL(9001, allProfiles.GetAt(0).HistorySize());
// profile0: tabTitle is NOT set at this layer (inherited from defaults)
VERIFY_IS_FALSE(allProfiles.GetAt(0).HasTabTitle());
VERIFY_ARE_EQUAL(L"DefaultTitle", allProfiles.GetAt(0).TabTitle());
// profile1: historySize is NOT set at this layer (inherited from defaults)
VERIFY_IS_FALSE(allProfiles.GetAt(1).HasHistorySize());
VERIFY_ARE_EQUAL(5000, allProfiles.GetAt(1).HistorySize());
// ProfileDefaults: historySize is set
VERIFY_IS_TRUE(settings->ProfileDefaults().HasHistorySize());
VERIFY_ARE_EQUAL(5000, settings->ProfileDefaults().HistorySize());
}
}

View File

@@ -60,6 +60,12 @@ namespace SettingsModelUnitTests
TEST_METHOD(ProfileWithInvalidIcon);
TEST_METHOD(ModifyProfileSettingAndRoundtrip);
TEST_METHOD(ModifyGlobalSettingAndRoundtrip);
TEST_METHOD(ModifyColorSchemeAndRoundtrip);
TEST_METHOD(FixupUserSettingsDetectsChanges);
TEST_METHOD(FixupCommandlinePatching);
private:
// Method Description:
// - deserializes and reserializes a json string representing a settings object model of type T
@@ -1325,4 +1331,288 @@ namespace SettingsModelUnitTests
// what was written in the settings file.
VERIFY_ARE_EQUAL(R"(c:\this_icon_had_better_not_exist.tiff)", newResult["profiles"]["list"][0]["icon"].asString());
}
void SerializationTests::ModifyProfileSettingAndRoundtrip()
{
// Load settings, modify a profile setting via setter, serialize,
// and verify the JSON output reflects the change.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"commandline": "cmd.exe"
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
// Verify initial value
VERIFY_ARE_EQUAL(1000, settings->AllProfiles().GetAt(0).HistorySize());
// Modify the setting
settings->AllProfiles().GetAt(0).HistorySize(5000);
VERIFY_ARE_EQUAL(5000, settings->AllProfiles().GetAt(0).HistorySize());
// Serialize and verify the change is reflected in JSON
const auto result{ settings->ToJson() };
VERIFY_ARE_EQUAL(5000, result["profiles"]["list"][0]["historySize"].asInt());
// Verify other settings are preserved
VERIFY_ARE_EQUAL("cmd.exe", result["profiles"]["list"][0]["commandline"].asString());
// Also verify: modify a setting that wasn't previously set
settings->AllProfiles().GetAt(0).TabTitle(L"NewTitle");
const auto result2{ settings->ToJson() };
VERIFY_ARE_EQUAL("NewTitle", result2["profiles"]["list"][0]["tabTitle"].asString());
}
void SerializationTests::ModifyGlobalSettingAndRoundtrip()
{
// Load settings, modify a global setting, serialize, verify JSON.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"initialRows": 30,
"alwaysOnTop": false,
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
// Verify initial values
VERIFY_ARE_EQUAL(30, settings->GlobalSettings().InitialRows());
VERIFY_ARE_EQUAL(false, settings->GlobalSettings().AlwaysOnTop());
// Modify global settings
settings->GlobalSettings().InitialRows(50);
settings->GlobalSettings().AlwaysOnTop(true);
// Verify in-memory changes
VERIFY_ARE_EQUAL(50, settings->GlobalSettings().InitialRows());
VERIFY_ARE_EQUAL(true, settings->GlobalSettings().AlwaysOnTop());
// Serialize and verify
const auto result{ settings->ToJson() };
VERIFY_ARE_EQUAL(50, result["initialRows"].asInt());
VERIFY_ARE_EQUAL(true, result["alwaysOnTop"].asBool());
}
void SerializationTests::ModifyColorSchemeAndRoundtrip()
{
// Load settings with a user color scheme, modify it, serialize, verify.
static constexpr std::string_view settingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}"
}
],
"schemes": [
{
"name": "MyScheme",
"foreground": "#CCCCCC",
"background": "#0C0C0C",
"cursorColor": "#FFFFFF",
"black": "#0C0C0C",
"red": "#C50F1F",
"green": "#13A10E",
"yellow": "#C19C00",
"blue": "#0037DA",
"purple": "#881798",
"cyan": "#3A96DD",
"white": "#CCCCCC",
"brightBlack": "#767676",
"brightRed": "#E74856",
"brightGreen": "#16C60C",
"brightYellow": "#F9F1A5",
"brightBlue": "#3B78FF",
"brightPurple": "#B4009E",
"brightCyan": "#61D6D6",
"brightWhite": "#F2F2F2"
}
]
})" };
const auto settings{ winrt::make_self<implementation::CascadiaSettings>(settingsJson) };
// Find and modify the color scheme
const auto schemes = settings->GlobalSettings().ColorSchemes();
VERIFY_IS_TRUE(schemes.HasKey(L"MyScheme"));
auto myScheme = schemes.Lookup(L"MyScheme");
const auto origForeground = myScheme.Foreground();
myScheme.Foreground(til::color{ 0xAA, 0xBB, 0xCC });
// Serialize and verify the change persists
const auto result{ settings->ToJson() };
const auto& schemesJson = result["schemes"];
bool found = false;
for (const auto& scheme : schemesJson)
{
if (scheme["name"].asString() == "MyScheme")
{
VERIFY_ARE_EQUAL("#AABBCC", scheme["foreground"].asString());
found = true;
break;
}
}
VERIFY_IS_TRUE(found, L"MyScheme should be present in serialized output");
}
void SerializationTests::FixupUserSettingsDetectsChanges()
{
// Verify that FixupUserSettings returns true when settings need
// to be written back (e.g., migration), and false when clean.
static constexpr std::string_view cleanSettingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"commandline": "cmd.exe"
}
]
})" };
// Load, fixup, serialize. Reload and verify fixup returns false.
implementation::SettingsLoader loader1{ cleanSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader1.MergeInboxIntoUserSettings();
loader1.FinalizeLayering();
loader1.FixupUserSettings();
const auto settings1 = winrt::make_self<implementation::CascadiaSettings>(std::move(loader1));
const auto result1{ settings1->ToJson() };
// Reload from the serialized output (should be stable)
implementation::SettingsLoader loader2{ toString(result1), implementation::LoadStringResource(IDR_DEFAULTS) };
loader2.MergeInboxIntoUserSettings();
loader2.FinalizeLayering();
const auto fixupNeeded = loader2.FixupUserSettings();
// After a clean roundtrip, no further fixups should be needed
VERIFY_IS_FALSE(fixupNeeded, L"A clean roundtrip should not require further fixups");
}
void SerializationTests::FixupCommandlinePatching()
{
// Verify that FixupUserSettings patches "cmd.exe" to the full path
// for the Command Prompt profile, and "powershell.exe" for the
// Windows PowerShell profile, and returns true to indicate changes.
// Case 1: CMD profile with short commandline should be patched
static constexpr std::string_view cmdSettingsJson{ R"(
{
"defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"profiles": [
{
"name": "Command Prompt",
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"commandline": "cmd.exe"
}
]
})" };
{
implementation::SettingsLoader loader{ cmdSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
const auto fixupNeeded = loader.FixupUserSettings();
VERIFY_IS_TRUE(fixupNeeded, L"FixupUserSettings should return true when cmd.exe is patched");
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto cmdProfile = settings->FindProfile(Utils::GuidFromString(L"{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"));
VERIFY_IS_NOT_NULL(cmdProfile);
VERIFY_ARE_EQUAL(L"%SystemRoot%\\System32\\cmd.exe", cmdProfile.Commandline());
}
// Case 2: PowerShell profile with short commandline should be patched
static constexpr std::string_view psSettingsJson{ R"(
{
"defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"profiles": [
{
"name": "Windows PowerShell",
"guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
"commandline": "powershell.exe"
}
]
})" };
{
implementation::SettingsLoader loader{ psSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
const auto fixupNeeded = loader.FixupUserSettings();
VERIFY_IS_TRUE(fixupNeeded, L"FixupUserSettings should return true when powershell.exe is patched");
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto psProfile = settings->FindProfile(Utils::GuidFromString(L"{61c54bbd-c2c6-5271-96e7-009a87ff44bf}"));
VERIFY_IS_NOT_NULL(psProfile);
VERIFY_ARE_EQUAL(L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", psProfile.Commandline());
}
// Case 3: CMD profile with the full path should NOT trigger fixup
static constexpr std::string_view cleanCmdSettingsJson{ R"(
{
"defaultProfile": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
"profiles": [
{
"name": "Command Prompt",
"guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"
}
]
})" };
{
implementation::SettingsLoader loader{ cleanCmdSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
const auto fixupNeeded = loader.FixupUserSettings();
VERIFY_IS_FALSE(fixupNeeded, L"FixupUserSettings should return false when no patching is needed");
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto cmdProfile = settings->FindProfile(Utils::GuidFromString(L"{0caa0dad-35be-5f56-a8ff-afceeeaa6101}"));
VERIFY_IS_NOT_NULL(cmdProfile);
// Should still resolve to the full path via inbox defaults
VERIFY_ARE_EQUAL(L"%SystemRoot%\\System32\\cmd.exe", cmdProfile.Commandline());
}
// Case 4: A non-builtin profile with "cmd.exe" should NOT be patched
static constexpr std::string_view customCmdSettingsJson{ R"(
{
"defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name": "My Custom CMD",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"commandline": "cmd.exe"
}
]
})" };
{
implementation::SettingsLoader loader{ customCmdSettingsJson, implementation::LoadStringResource(IDR_DEFAULTS) };
loader.MergeInboxIntoUserSettings();
loader.FinalizeLayering();
loader.FixupUserSettings();
const auto settings = winrt::make_self<implementation::CascadiaSettings>(std::move(loader));
const auto customProfile = settings->FindProfile(Utils::GuidFromString(L"{6239a42c-0000-49a3-80bd-e8fdd045185c}"));
VERIFY_IS_NOT_NULL(customProfile);
// Custom profile should keep "cmd.exe" unchanged
VERIFY_ARE_EQUAL(L"cmd.exe", customProfile.Commandline());
}
}
}