// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "../TerminalSettingsModel/ColorScheme.h" #include "../TerminalSettingsModel/CascadiaSettings.h" #include "JsonTestClass.h" #include "TestUtils.h" #include #include using namespace Microsoft::Console; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace winrt::Microsoft::Terminal::Control; using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; namespace SettingsModelLocalTests { // TODO:microsoft/terminal#3838: // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for // an updated TAEF that will let us install framework packages when the test // package is deployed. Until then, these tests won't deploy in CI. class DeserializationTests : public JsonTestClass { // Use a custom AppxManifest to ensure that we can activate winrt types // from our test. This property will tell taef to manually use this as // the AppxManifest for this test class. // This does not yet work for anything XAML-y. See TabTests.cpp for more // details on that. BEGIN_TEST_CLASS(DeserializationTests) TEST_CLASS_PROPERTY(L"RunAs", L"UAP") TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") END_TEST_CLASS() TEST_METHOD(ValidateProfilesExist); TEST_METHOD(ValidateDefaultProfileExists); TEST_METHOD(ValidateDuplicateProfiles); TEST_METHOD(ValidateManyWarnings); TEST_METHOD(LayerGlobalProperties); TEST_METHOD(ValidateProfileOrdering); TEST_METHOD(ValidateHideProfiles); TEST_METHOD(TestReorderWithNullGuids); TEST_METHOD(TestReorderingWithoutGuid); TEST_METHOD(TestLayeringNameOnlyProfiles); TEST_METHOD(TestHideAllProfiles); TEST_METHOD(TestInvalidColorSchemeName); TEST_METHOD(TestHelperFunctions); TEST_METHOD(TestProfileBackgroundImageWithEnvVar); TEST_METHOD(TestProfileBackgroundImageWithDesktopWallpaper); TEST_METHOD(TestCloseOnExitParsing); TEST_METHOD(TestCloseOnExitCompatibilityShim); TEST_METHOD(TestLayerUserDefaultsBeforeProfiles); TEST_METHOD(TestDontLayerGuidFromUserDefaults); TEST_METHOD(TestLayerUserDefaultsOnDynamics); TEST_METHOD(FindMissingProfile); TEST_METHOD(ValidateKeybindingsWarnings); TEST_METHOD(ValidateColorSchemeInCommands); TEST_METHOD(ValidateExecuteCommandlineWarning); TEST_METHOD(TestTrailingCommas); TEST_METHOD(TestCommandsAndKeybindings); TEST_METHOD(TestNestedCommandWithoutName); TEST_METHOD(TestNestedCommandWithBadSubCommands); TEST_METHOD(TestUnbindNestedCommand); TEST_METHOD(TestRebindNestedCommand); TEST_METHOD(TestCopy); TEST_METHOD(TestCloneInheritanceTree); TEST_METHOD(TestValidDefaults); TEST_METHOD(TestInheritedCommand); TEST_METHOD(LoadFragmentsWithMultipleUpdates); private: static winrt::com_ptr createSettings(const std::string_view& userJSON) { static constexpr std::string_view inboxJSON{ R"({ "schemes": [ { "name": "Campbell", "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" } ] })" }; return winrt::make_self(userJSON, inboxJSON); } static void _logCommandNames(winrt::Windows::Foundation::Collections::IMapView commands, const int indentation = 1) { if (indentation == 1) { Log::Comment((commands.Size() == 0) ? L"Commands:\n " : L"Commands:"); } for (const auto& nameAndCommand : commands) { Log::Comment(fmt::format(L"{0:>{1}}* {2}->{3}", L"", indentation, nameAndCommand.Key(), nameAndCommand.Value().Name()) .c_str()); winrt::com_ptr cmdImpl; cmdImpl.copy_from(winrt::get_self(nameAndCommand.Value())); if (cmdImpl->HasNestedCommands()) { _logCommandNames(cmdImpl->_subcommands.GetView(), indentation + 2); } } } }; void DeserializationTests::ValidateProfilesExist() { static constexpr std::string_view settingsWithProfiles{ R"( { "profiles": [ { "name" : "profile0" } ] })" }; static constexpr std::string_view settingsWithoutProfiles{ R"( { "defaultProfile": "{6239a42c-1de4-49a3-80bd-e8fdd045185c}" })" }; static constexpr std::string_view settingsWithEmptyProfiles{ R"( { "profiles": [] })" }; { // Case 1: Good settings auto settings = winrt::make_self(settingsWithProfiles); } { // Case 2: Bad settings bool caughtExpectedException = false; try { auto settings = winrt::make_self(settingsWithoutProfiles); } catch (const implementation::SettingsException& ex) { VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::NoProfiles); caughtExpectedException = true; } VERIFY_IS_TRUE(caughtExpectedException); } { // Case 3: Bad settings bool caughtExpectedException = false; try { auto settings = winrt::make_self(settingsWithEmptyProfiles); } catch (const implementation::SettingsException& ex) { VERIFY_IS_TRUE(ex.Error() == SettingsLoadErrors::NoProfiles); caughtExpectedException = true; } VERIFY_IS_TRUE(caughtExpectedException); } } void DeserializationTests::ValidateDefaultProfileExists() { static constexpr std::string_view goodProfiles{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" }, { "name" : "profile0", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" } ] })" }; static constexpr std::string_view badProfiles{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" } ] })" }; static constexpr std::string_view goodProfilesSpecifiedByName{ R"( { "defaultProfile": "profile1", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" } ] })" }; { // Case 1: Good settings Log::Comment(NoThrowString().Format( L"Testing a pair of profiles with unique guids, and the defaultProfile is one of those guids")); const auto settings = createSettings(goodProfiles); VERIFY_ARE_EQUAL(static_cast(0), settings->Warnings().Size()); VERIFY_ARE_EQUAL(static_cast(2), settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(0).Guid()); } { // Case 2: Bad settings Log::Comment(NoThrowString().Format( L"Testing a pair of profiles with unique guids, but the defaultProfile is NOT one of those guids")); const auto settings = createSettings(badProfiles); VERIFY_ARE_EQUAL(static_cast(1), settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(static_cast(2), settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(0).Guid()); } { // Case 2: Bad settings Log::Comment(NoThrowString().Format( L"Testing a pair of profiles with unique guids, and no defaultProfile at all")); const auto settings = createSettings(badProfiles); VERIFY_ARE_EQUAL(static_cast(1), settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(static_cast(2), settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(0).Guid()); } { // Case 4: Good settings, default profile is a string Log::Comment(NoThrowString().Format( L"Testing a pair of profiles with unique guids, and the defaultProfile is one of the profile names")); const auto settings = createSettings(goodProfilesSpecifiedByName); VERIFY_ARE_EQUAL(static_cast(0), settings->Warnings().Size()); VERIFY_ARE_EQUAL(static_cast(2), settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), settings->AllProfiles().GetAt(1).Guid()); } } void DeserializationTests::ValidateDuplicateProfiles() { static constexpr std::string_view veryBadProfiles{ R"( { "profiles": [ { "name" : "profile0", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" }, { "name" : "profile2", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" }, { "name" : "profile3", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" }, { "name" : "profile4", "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" }, { "name" : "profile5", "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" }, { "name" : "profile6", "guid": "{6239a42c-7777-49a3-80bd-e8fdd045185c}" } ] })" }; const auto settings = createSettings(veryBadProfiles); VERIFY_ARE_EQUAL(static_cast(1), settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(static_cast(4), settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(L"profile0", settings->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(L"profile1", settings->AllProfiles().GetAt(1).Name()); VERIFY_ARE_EQUAL(L"profile4", settings->AllProfiles().GetAt(2).Name()); VERIFY_ARE_EQUAL(L"profile6", settings->AllProfiles().GetAt(3).Name()); } void DeserializationTests::ValidateManyWarnings() { static constexpr std::string_view badProfiles{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" }, { "name" : "profile2", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" }, { "name" : "profile3", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" }, { "name" : "profile4", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}" } ] })" }; const auto settings = createSettings(badProfiles); VERIFY_ARE_EQUAL(2u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::DuplicateProfile, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingDefaultProfile, settings->Warnings().GetAt(1)); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Guid(), settings->GlobalSettings().DefaultProfile()); } void DeserializationTests::LayerGlobalProperties() { static constexpr std::string_view inboxSettings{ R"({ "alwaysShowTabs": true, "initialCols" : 120, "initialRows" : 30 })" }; static constexpr std::string_view userSettings{ R"({ "showTabsInTitlebar": false, "initialCols" : 240, "initialRows" : 60, "profiles": [ { "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" } ] })" }; const auto settings = winrt::make_self(userSettings, inboxSettings); VERIFY_ARE_EQUAL(true, settings->GlobalSettings().AlwaysShowTabs()); VERIFY_ARE_EQUAL(240, settings->GlobalSettings().InitialCols()); VERIFY_ARE_EQUAL(60, settings->GlobalSettings().InitialRows()); VERIFY_ARE_EQUAL(false, settings->GlobalSettings().ShowTabsInTitlebar()); } void DeserializationTests::ValidateProfileOrdering() { static constexpr std::string_view userProfiles0String{ R"( { "profiles": [ { "name" : "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } ] })" }; static constexpr std::string_view defaultProfilesString{ R"( { "profiles": [ { "name" : "profile2", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" }, { "name" : "profile3", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" } ] })" }; static constexpr std::string_view userProfiles1String{ R"( { "profiles": [ { "name" : "profile4", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" }, { "name" : "profile5", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" } ] })" }; { Log::Comment(NoThrowString().Format( L"Case 1: Simple swapping of the ordering. The user has the " L"default profiles in the opposite order of the default ordering.")); const auto settings = winrt::make_self(userProfiles0String, defaultProfilesString); VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(L"profile0", settings->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(L"profile1", settings->AllProfiles().GetAt(1).Name()); } { Log::Comment(NoThrowString().Format( L"Case 2: Make sure all the user's profiles appear before the defaults.")); const auto settings = winrt::make_self(userProfiles1String, defaultProfilesString); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(L"profile4", settings->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(L"profile5", settings->AllProfiles().GetAt(1).Name()); VERIFY_ARE_EQUAL(L"profile2", settings->AllProfiles().GetAt(2).Name()); } } void DeserializationTests::ValidateHideProfiles() { static constexpr std::string_view defaultProfilesString{ R"( { "profiles": [ { "name" : "profile2", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" }, { "name" : "profile3", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}" } ] })" }; static constexpr std::string_view userProfiles0String{ R"( { "profiles": [ { "name" : "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "hidden": true }, { "name" : "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" } ] })" }; static constexpr std::string_view userProfiles1String{ R"( { "profiles": [ { "name" : "profile4", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "hidden": true }, { "name" : "profile5", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" }, { "name" : "profile6", "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", "hidden": true } ] })" }; { const auto settings = winrt::make_self(userProfiles0String, defaultProfilesString); VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(1u, settings->ActiveProfiles().Size()); VERIFY_ARE_EQUAL(L"profile1", settings->ActiveProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(false, settings->ActiveProfiles().GetAt(0).Hidden()); } { const auto settings = winrt::make_self(userProfiles1String, defaultProfilesString); VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(2u, settings->ActiveProfiles().Size()); VERIFY_ARE_EQUAL(L"profile5", settings->ActiveProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(L"profile2", settings->ActiveProfiles().GetAt(1).Name()); VERIFY_ARE_EQUAL(false, settings->ActiveProfiles().GetAt(0).Hidden()); VERIFY_ARE_EQUAL(false, settings->ActiveProfiles().GetAt(1).Hidden()); } } void DeserializationTests::TestReorderWithNullGuids() { static constexpr std::string_view settings0String{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid" : "{6239a42c-1111-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1" }, { "name" : "cmdFromUserSettings", "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}" // from defaults.json } ] })" }; const auto settings = winrt::make_self(settings0String, DefaultJson); VERIFY_ARE_EQUAL(0u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(0).HasGuid()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(1).HasGuid()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(2).HasGuid()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(3).HasGuid()); VERIFY_ARE_EQUAL(L"profile0", settings->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(L"profile1", settings->AllProfiles().GetAt(1).Name()); VERIFY_ARE_EQUAL(L"cmdFromUserSettings", settings->AllProfiles().GetAt(2).Name()); VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->AllProfiles().GetAt(3).Name()); } void DeserializationTests::TestReorderingWithoutGuid() { Log::Comment(NoThrowString().Format( L"During the GH#2515 PR, this set of settings was found to cause an" L" exception, crashing the terminal. This test ensures that it doesn't.")); Log::Comment(NoThrowString().Format( L"While similar to TestReorderWithNullGuids, there's something else" L" about this scenario specifically that causes a crash, when " L" TestReorderWithNullGuids did _not_.")); static constexpr std::string_view settings0String{ R"( { "defaultProfile" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "profiles": [ { "guid" : "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "acrylicOpacity" : 0.5, "closeOnExit" : true, "background" : "#8A00FF", "foreground" : "#F2F2F2", "commandline" : "cmd.exe", "cursorColor" : "#FFFFFF", "fontFace" : "Cascadia Code", "fontSize" : 10, "historySize" : 9001, "padding" : "20", "snapOnInput" : true, "startingDirectory" : "%USERPROFILE%", "useAcrylic" : true }, { "name" : "ThisProfileShouldNotCrash", "tabTitle" : "Ubuntu", "acrylicOpacity" : 0.5, "background" : "#2C001E", "closeOnExit" : true, "colorScheme" : "Campbell", "commandline" : "wsl.exe", "cursorColor" : "#FFFFFF", "cursorShape" : "bar", "fontSize" : 10, "historySize" : 9001, "padding" : "0, 0, 0, 0", "snapOnInput" : true, "useAcrylic" : true }, { // This is the same profile that would be generated by the WSL profile generator. "name" : "Ubuntu", "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", "acrylicOpacity" : 0.5, "background" : "#2C001E", "closeOnExit" : false, "cursorColor" : "#FFFFFF", "cursorShape" : "bar", "fontSize" : 10, "historySize" : 9001, "snapOnInput" : true, "useAcrylic" : true } ] })" }; const auto settings = winrt::make_self(settings0String, DefaultJson); VERIFY_ARE_EQUAL(0u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(0).HasGuid()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(1).HasGuid()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(2).HasGuid()); VERIFY_IS_TRUE(settings->AllProfiles().GetAt(3).HasGuid()); VERIFY_ARE_EQUAL(L"Command Prompt", settings->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(L"ThisProfileShouldNotCrash", settings->AllProfiles().GetAt(1).Name()); VERIFY_ARE_EQUAL(L"Ubuntu", settings->AllProfiles().GetAt(2).Name()); VERIFY_ARE_EQUAL(L"Windows PowerShell", settings->AllProfiles().GetAt(3).Name()); } void DeserializationTests::TestLayeringNameOnlyProfiles() { // This is a test discovered during GH#2782. When we add a name-only // profile, it should only layer with other name-only profiles with the // _same name_ static constexpr std::string_view settings0String{ R"( { "defaultProfile" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", "profiles": [ { "guid" : "{00000000-0000-5f56-a8ff-afceeeaa6101}", "name" : "ThisProfileIsGood" }, { "name" : "ThisProfileShouldNotLayer" }, { "name" : "NeitherShouldThisOne" } ] })" }; const auto settings = winrt::make_self(settings0String, DefaultJson); const auto profiles = settings->AllProfiles(); VERIFY_ARE_EQUAL(5u, profiles.Size()); VERIFY_ARE_EQUAL(L"ThisProfileIsGood", profiles.GetAt(0).Name()); VERIFY_ARE_EQUAL(L"ThisProfileShouldNotLayer", profiles.GetAt(1).Name()); VERIFY_ARE_EQUAL(L"NeitherShouldThisOne", profiles.GetAt(2).Name()); VERIFY_ARE_EQUAL(L"Windows PowerShell", profiles.GetAt(3).Name()); VERIFY_ARE_EQUAL(L"Command Prompt", profiles.GetAt(4).Name()); } void DeserializationTests::TestHideAllProfiles() { static constexpr std::string_view settingsWithProfiles{ R"( { "profiles": [ { "name" : "profile0", "hidden": false }, { "name" : "profile1", "hidden": true } ] })" }; static constexpr std::string_view settingsWithoutProfiles{ R"( { "profiles": [ { "name" : "profile0", "hidden": true }, { "name" : "profile1", "hidden": true } ] })" }; { // Case 1: Good settings const auto settings = createSettings(settingsWithProfiles); VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(1u, settings->ActiveProfiles().Size()); } { // Case 2: Bad settings VERIFY_THROWS_SPECIFIC(winrt::make_self(settingsWithoutProfiles), const implementation::SettingsException, [](const auto& ex) { return ex.Error() == SettingsLoadErrors::AllProfilesHidden; }); } } void DeserializationTests::TestInvalidColorSchemeName() { Log::Comment(NoThrowString().Format( L"Ensure that setting a profile's scheme to a non-existent scheme causes a warning.")); static constexpr std::string_view settings0String{ R"({ "profiles": [ { "name" : "profile0", "colorScheme": "Campbell" }, { "name" : "profile1", "colorScheme": "InvalidSchemeName" }, { "name" : "profile2" // Will use the Profile default value, "Campbell" } ] })" }; const auto settings = createSettings(settings0String); VERIFY_ARE_EQUAL(1u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::UnknownColorScheme, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); for (const auto& profile : settings->AllProfiles()) { VERIFY_ARE_EQUAL(L"Campbell", profile.DefaultAppearance().ColorSchemeName()); } } void DeserializationTests::ValidateColorSchemeInCommands() { Log::Comment(NoThrowString().Format( L"Ensure that setting a command's color scheme to a non-existent scheme causes a warning.")); static constexpr std::string_view settings0String{ R"( { "profiles": [ { "name" : "profile0", "colorScheme": "Campbell" } ], "actions": [ { "command": { "action": "setColorScheme", "colorScheme": "Campbell" } }, { "command": { "action": "setColorScheme", "colorScheme": "invalidScheme" } } ] })" }; static constexpr std::string_view settings1String{ R"( { "profiles": [ { "name" : "profile0", "colorScheme": "Campbell" } ], "actions": [ { "command": { "action": "setColorScheme", "colorScheme": "Campbell" } }, { "name": "parent", "commands": [ { "command": { "action": "setColorScheme", "colorScheme": "invalidScheme" } } ] } ] })" }; static constexpr std::string_view settings2String{ R"( { "profiles": [ { "name" : "profile0", "colorScheme": "Campbell" } ], "actions": [ { "command": { "action": "setColorScheme", "colorScheme": "Campbell" } }, { "name": "grandparent", "commands": [ { "name": "parent", "commands": [ { "command": { "action": "setColorScheme", "colorScheme": "invalidScheme" } } ] } ] } ] })" }; { // Case 1: setColorScheme command with invalid scheme Log::Comment(NoThrowString().Format( L"Testing a simple command with invalid scheme")); const auto settings = createSettings(settings0String); VERIFY_ARE_EQUAL(1u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::InvalidColorSchemeInCmd, settings->Warnings().GetAt(0)); } { // Case 2: nested setColorScheme command with invalid scheme Log::Comment(NoThrowString().Format( L"Testing a nested command with invalid scheme")); const auto settings = createSettings(settings1String); VERIFY_ARE_EQUAL(1u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::InvalidColorSchemeInCmd, settings->Warnings().GetAt(0)); } { // Case 3: nested-in-nested setColorScheme command with invalid scheme Log::Comment(NoThrowString().Format( L"Testing a nested-in-nested command with invalid scheme")); const auto settings = createSettings(settings2String); VERIFY_ARE_EQUAL(1u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::InvalidColorSchemeInCmd, settings->Warnings().GetAt(0)); } } void DeserializationTests::TestHelperFunctions() { static constexpr std::string_view settings0String{ R"( { "defaultProfile" : "{2C4DE342-38B7-51CF-B940-2309A097F518}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-5555-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-6666-49a3-80bd-e8fdd045185c}" }, { "name" : "ThisProfileShouldNotThrow" }, { "name" : "Ubuntu", "guid" : "{2C4DE342-38B7-51CF-B940-2309A097F518}" } ] })" }; const auto name0{ L"profile0" }; const auto name1{ L"profile1" }; const auto name2{ L"Ubuntu" }; const auto name3{ L"ThisProfileShouldNotThrow" }; const auto badName{ L"DoesNotExist" }; const winrt::guid guid0{ Utils::GuidFromString(L"{6239a42c-5555-49a3-80bd-e8fdd045185c}") }; const winrt::guid guid1{ Utils::GuidFromString(L"{6239a42c-6666-49a3-80bd-e8fdd045185c}") }; const winrt::guid guid2{ Utils::GuidFromString(L"{2C4DE342-38B7-51CF-B940-2309A097F518}") }; const winrt::guid fakeGuid{ Utils::GuidFromString(L"{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}") }; const winrt::guid autogeneratedGuid{ implementation::Profile::_GenerateGuidForProfile(name3, L"") }; const auto settings = createSettings(settings0String); VERIFY_ARE_EQUAL(guid0, settings->GetProfileByName(name0).Guid()); VERIFY_ARE_EQUAL(guid1, settings->GetProfileByName(name1).Guid()); VERIFY_ARE_EQUAL(guid2, settings->GetProfileByName(name2).Guid()); VERIFY_ARE_EQUAL(autogeneratedGuid, settings->GetProfileByName(name3).Guid()); VERIFY_IS_NULL(settings->GetProfileByName(badName)); VERIFY_ARE_EQUAL(name0, settings->FindProfile(guid0).Name()); VERIFY_ARE_EQUAL(name1, settings->FindProfile(guid1).Name()); VERIFY_ARE_EQUAL(name2, settings->FindProfile(guid2).Name()); VERIFY_IS_NULL(settings->FindProfile(fakeGuid)); } void DeserializationTests::TestProfileBackgroundImageWithEnvVar() { const auto expectedPath = wil::ExpandEnvironmentStringsW(L"%WINDIR%\\System32\\x_80.png"); static constexpr std::string_view settingsJson{ R"( { "profiles": [ { "name": "profile0", "backgroundImage": "%WINDIR%\\System32\\x_80.png" } ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_NOT_EQUAL(0u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(expectedPath, settings->AllProfiles().GetAt(0).DefaultAppearance().ExpandedBackgroundImagePath()); } void DeserializationTests::TestProfileBackgroundImageWithDesktopWallpaper() { const winrt::hstring expectedBackgroundImagePath{ L"desktopWallpaper" }; static constexpr std::string_view settingsJson{ R"( { "profiles": [ { "name": "profile0", "backgroundImage": "desktopWallpaper" } ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_EQUAL(expectedBackgroundImagePath, settings->AllProfiles().GetAt(0).DefaultAppearance().BackgroundImagePath()); VERIFY_ARE_NOT_EQUAL(expectedBackgroundImagePath, settings->AllProfiles().GetAt(0).DefaultAppearance().ExpandedBackgroundImagePath()); } void DeserializationTests::TestCloseOnExitParsing() { static constexpr std::string_view settingsJson{ R"( { "profiles": [ { "name": "profile0", "closeOnExit": "graceful" }, { "name": "profile1", "closeOnExit": "always" }, { "name": "profile2", "closeOnExit": "never" }, { "name": "profile3", "closeOnExit": null } ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(0).CloseOnExit()); VERIFY_ARE_EQUAL(CloseOnExitMode::Always, settings->AllProfiles().GetAt(1).CloseOnExit()); VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings->AllProfiles().GetAt(2).CloseOnExit()); // Unknown modes parse as "Graceful" VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(3).CloseOnExit()); } void DeserializationTests::TestCloseOnExitCompatibilityShim() { static constexpr std::string_view settingsJson{ R"( { "profiles": [ { "name": "profile0", "closeOnExit": true }, { "name": "profile1", "closeOnExit": false } ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_EQUAL(CloseOnExitMode::Graceful, settings->AllProfiles().GetAt(0).CloseOnExit()); VERIFY_ARE_EQUAL(CloseOnExitMode::Never, settings->AllProfiles().GetAt(1).CloseOnExit()); } void DeserializationTests::TestLayerUserDefaultsBeforeProfiles() { // Test for microsoft/terminal#2325. For this test, we'll be setting the // "historySize" in the "defaultSettings", so it should apply to all // profiles, unless they override it. In one of the user's profiles, // we'll override that value, and in the other, we'll leave it // untouched. static constexpr std::string_view settings0String{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": { "defaults": { "historySize": 1234 }, "list": [ { "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "name": "profile0", "historySize": 2345 }, { "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", "name": "profile1" } ] } })" }; const auto settings = createSettings(settings0String); VERIFY_IS_NOT_NULL(settings->ProfileDefaults()); VERIFY_ARE_EQUAL(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}", settings->GlobalSettings().UnparsedDefaultProfile()); VERIFY_ARE_EQUAL(2u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(2345, settings->AllProfiles().GetAt(0).HistorySize()); VERIFY_ARE_EQUAL(1234, settings->AllProfiles().GetAt(1).HistorySize()); } void DeserializationTests::TestDontLayerGuidFromUserDefaults() { // Test for microsoft/terminal#2325. We don't want the user to put a // "guid" in the "defaultSettings", and have that apply to all the other // profiles static constexpr std::string_view settings0String{ R"({ "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": { "defaults": { "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" }, "list": [ { "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "name": "profile0", "historySize": 2345 }, { // Doesn't have a GUID, we'll auto-generate one "name": "profile1" } ] } })" }; const auto guid1String = L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"; const winrt::guid guid1{ Utils::GuidFromString(guid1String) }; const auto settings = winrt::make_self(settings0String, DefaultJson); VERIFY_ARE_EQUAL(guid1String, settings->GlobalSettings().UnparsedDefaultProfile()); VERIFY_ARE_EQUAL(4u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(guid1, settings->AllProfiles().GetAt(0).Guid()); VERIFY_ARE_NOT_EQUAL(guid1, settings->AllProfiles().GetAt(1).Guid()); } void DeserializationTests::TestLayerUserDefaultsOnDynamics() { // Test for microsoft/terminal#2325. For this test, we'll be setting the // "historySize" in the "defaultSettings", so it should apply to all // profiles, unless they override it. The dynamic profiles will _also_ // set this value, but from discussion in GH#2325, we decided that // settings in defaultSettings should apply _on top_ of settings from // dynamic profiles. const winrt::guid guid1{ Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}") }; const winrt::guid guid2{ Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}") }; const winrt::guid guid3{ Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}") }; const winrt::guid guid4{ Utils::GuidFromString(L"{6239a42c-4444-49a3-80bd-e8fdd045185c}") }; static constexpr std::string_view dynamicProfiles{ R"({ "profiles": [ { "name": "profile0", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "source": "Terminal.App.UnitTest.0", "historySize": 1111 }, { "name": "profile1", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", "source": "Terminal.App.UnitTest.1", "historySize": 2222 }, { "name": "profile2", "guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}", "source": "Terminal.App.UnitTest.1", "historySize": 4444 } ] })" }; static constexpr std::string_view userProfiles{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": { "defaults": { "historySize": 1234 }, "list": [ { "name" : "profile0FromUserSettings", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "source": "Terminal.App.UnitTest.0" }, { "name" : "profile1FromUserSettings", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", "source": "Terminal.App.UnitTest.1", "historySize": 4444 }, { "name" : "profile2FromUserSettings", "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}", "historySize": 5555 } ] } })" }; const auto settings = winrt::make_self(userProfiles, dynamicProfiles); const auto allProfiles = settings->AllProfiles(); Log::Comment(NoThrowString().Format( L"All profiles with the same name have the same GUID. However, they" L" will not be layered, because they have different source's")); VERIFY_ARE_EQUAL(4u, allProfiles.Size()); VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.0", allProfiles.GetAt(0).Source()); VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", allProfiles.GetAt(1).Source()); VERIFY_ARE_EQUAL(L"", allProfiles.GetAt(2).Source()); VERIFY_ARE_EQUAL(L"Terminal.App.UnitTest.1", allProfiles.GetAt(3).Source()); VERIFY_ARE_EQUAL(guid1, allProfiles.GetAt(0).Guid()); VERIFY_ARE_EQUAL(guid2, allProfiles.GetAt(1).Guid()); VERIFY_ARE_EQUAL(guid3, allProfiles.GetAt(2).Guid()); VERIFY_ARE_EQUAL(guid4, allProfiles.GetAt(3).Guid()); VERIFY_ARE_EQUAL(L"profile0FromUserSettings", allProfiles.GetAt(0).Name()); VERIFY_ARE_EQUAL(L"profile1FromUserSettings", allProfiles.GetAt(1).Name()); VERIFY_ARE_EQUAL(L"profile2FromUserSettings", allProfiles.GetAt(2).Name()); VERIFY_ARE_EQUAL(L"profile2", allProfiles.GetAt(3).Name()); Log::Comment(NoThrowString().Format( L"This is the real meat of the test: The two dynamic profiles that " L"_didn't_ have historySize set in the userSettings should have " L"1234 as their historySize(from the defaultSettings).The other two" L" profiles should have their custom historySize value.")); VERIFY_ARE_EQUAL(1234, allProfiles.GetAt(0).HistorySize()); VERIFY_ARE_EQUAL(4444, allProfiles.GetAt(1).HistorySize()); VERIFY_ARE_EQUAL(5555, allProfiles.GetAt(2).HistorySize()); VERIFY_ARE_EQUAL(1234, allProfiles.GetAt(3).HistorySize()); } void DeserializationTests::FindMissingProfile() { // Test that CascadiaSettings::FindProfile returns null for a GUID that // doesn't exist static constexpr std::string_view settingsString{ R"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" } ] })" }; const auto settings = createSettings(settingsString); const auto guid1 = Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}"); const auto guid2 = Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}"); const auto guid3 = Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}"); const auto profile1 = settings->FindProfile(guid1); const auto profile2 = settings->FindProfile(guid2); const auto profile3 = settings->FindProfile(guid3); VERIFY_IS_NOT_NULL(profile1); VERIFY_IS_NOT_NULL(profile2); VERIFY_IS_NULL(profile3); VERIFY_ARE_EQUAL(L"profile0", profile1.Name()); VERIFY_ARE_EQUAL(L"profile1", profile2.Name()); } void DeserializationTests::ValidateKeybindingsWarnings() { static constexpr std::string_view badSettings{ R"( { "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" } ], "keybindings": [ { "command": { "action": "splitPane", "split":"auto" }, "keys": [ "ctrl+alt+t", "ctrl+a" ] }, { "command": { "action": "moveFocus" }, "keys": [ "ctrl+a" ] }, { "command": { "action": "resizePane" }, "keys": [ "ctrl+b" ] }, { "name": "invalid nested", "commands":[ { "name" : "hello" }, { "name" : "world" } ] } ] })" }; const auto settings = createSettings(badSettings); // KeyMap: ctrl+a/b are mapped to "invalid" // ActionMap: "splitPane" and "invalid" are the only deserialized actions // NameMap: "splitPane" has no key binding, but it is still added to the name map const auto actionMap = winrt::get_self(settings->GlobalSettings().ActionMap()); VERIFY_ARE_EQUAL(2u, actionMap->_KeyMap.size()); VERIFY_ARE_EQUAL(2u, actionMap->_ActionMap.size()); VERIFY_ARE_EQUAL(1u, actionMap->NameMap().Size()); VERIFY_ARE_EQUAL(5u, settings->Warnings().Size()); const auto globalAppSettings = winrt::get_self(settings->GlobalSettings()); const auto& keybindingsWarnings = globalAppSettings->KeybindingsWarnings(); VERIFY_ARE_EQUAL(4u, keybindingsWarnings.size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::TooManyKeysForChord, keybindingsWarnings.at(0)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(1)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(2)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::FailedToParseSubCommands, keybindingsWarnings.at(3)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::TooManyKeysForChord, settings->Warnings().GetAt(1)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(2)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(3)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::FailedToParseSubCommands, settings->Warnings().GetAt(4)); } void DeserializationTests::ValidateExecuteCommandlineWarning() { static constexpr std::string_view badSettings{ R"( { "defaultProfile": "{6239a42c-2222-49a3-80bd-e8fdd045185c}", "profiles": [ { "name" : "profile0", "guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}" }, { "name" : "profile1", "guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}" } ], "keybindings": [ { "name":null, "command": { "action": "wt" }, "keys": [ "ctrl+a" ] }, { "name":null, "command": { "action": "wt", "commandline":"" }, "keys": [ "ctrl+b" ] }, { "name":null, "command": { "action": "wt", "commandline":null }, "keys": [ "ctrl+c" ] } ] })" }; const auto settings = createSettings(badSettings); const auto actionMap = winrt::get_self(settings->GlobalSettings().ActionMap()); VERIFY_ARE_EQUAL(3u, actionMap->_KeyMap.size()); VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('A'), 0 })); VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('B'), 0 })); VERIFY_IS_NULL(actionMap->GetActionByKeyChord({ VirtualKeyModifiers::Control, static_cast('C'), 0 })); const auto globalAppSettings = winrt::get_self(settings->GlobalSettings()); const auto& keybindingsWarnings = globalAppSettings->KeybindingsWarnings(); VERIFY_ARE_EQUAL(3u, keybindingsWarnings.size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(0)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(1)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, keybindingsWarnings.at(2)); VERIFY_ARE_EQUAL(4u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(1)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(2)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::MissingRequiredParameter, settings->Warnings().GetAt(3)); } void DeserializationTests::TestTrailingCommas() { static constexpr std::string_view badSettings{ R"({ "profiles": [{ "name": "profile0" }], })" }; try { const auto settings = createSettings(badSettings); } catch (...) { VERIFY_IS_TRUE(false, L"This call to LayerJson should succeed, even with the trailing comma"); } } void DeserializationTests::TestCommandsAndKeybindings() { static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { "name": "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "historySize": 1, "commandline": "cmd.exe" }, { "name": "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "historySize": 2, "commandline": "pwsh.exe" }, { "name": "profile2", "historySize": 3, "commandline": "wsl.exe" } ], "actions": [ { "keys": "ctrl+a", "command": { "action": "splitPane", "split": "vertical" } }, { "name": "ctrl+b", "command": { "action": "splitPane", "split": "vertical" } }, { "keys": "ctrl+c", "name": "ctrl+c", "command": { "action": "splitPane", "split": "vertical" } }, { "keys": "ctrl+d", "command": { "action": "splitPane", "split": "vertical" } }, { "keys": "ctrl+e", "command": { "action": "splitPane", "split": "horizontal" } }, { "keys": "ctrl+f", "name":null, "command": { "action": "splitPane", "split": "horizontal" } } ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); const auto profile2Guid = settings->AllProfiles().GetAt(2).Guid(); VERIFY_ARE_NOT_EQUAL(winrt::guid{}, profile2Guid); auto actionMap = winrt::get_self(settings->GlobalSettings().ActionMap()); VERIFY_ARE_EQUAL(5u, actionMap->KeyBindings().Size()); // A/D, B, C, E will be in the list of commands, for 4 total. // * A and D share the same name, so they'll only generate a single action. // * F's name is set manually to `null` const auto& nameMap{ actionMap->NameMap() }; VERIFY_ARE_EQUAL(1u, nameMap.Size()); { const KeyChord kc{ true, false, false, false, static_cast('A'), 0 }; const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } Log::Comment(L"Note that we're skipping ctrl+B, since that doesn't have `keys` set."); { const KeyChord kc{ true, false, false, false, static_cast('C'), 0 }; const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { const KeyChord kc{ true, false, false, false, static_cast('D'), 0 }; const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { const KeyChord kc{ true, false, false, false, static_cast('E'), 0 }; const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Down, realArgs.SplitDirection()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { const KeyChord kc{ true, false, false, false, static_cast('F'), 0 }; const auto actionAndArgs = TestUtils::GetActionAndArgs(*actionMap, kc); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Down, realArgs.SplitDirection()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } Log::Comment(L"Now verify the commands"); _logCommandNames(nameMap); { // This was renamed to "ctrl+c" in C. So this does not exist. const auto command = nameMap.TryLookup(L"Split pane, split: vertical"); VERIFY_IS_NULL(command); } { // This was renamed to "ctrl+c" in C. So this does not exist. const auto command = nameMap.TryLookup(L"ctrl+b"); VERIFY_IS_NULL(command); } { const auto command = nameMap.TryLookup(L"ctrl+c"); VERIFY_IS_NOT_NULL(command); const auto actionAndArgs = command.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); // Verify the args have the expected value VERIFY_ARE_EQUAL(SplitDirection::Right, realArgs.SplitDirection()); VERIFY_IS_NOT_NULL(realArgs.TerminalArgs()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Commandline().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().StartingDirectory().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().TabTitle().empty()); VERIFY_IS_TRUE(realArgs.TerminalArgs().Profile().empty()); } { // This was renamed to null (aka removed from the name map) in F. So this does not exist. const auto command = nameMap.TryLookup(L"Split pane, split: horizontal"); VERIFY_IS_NULL(command); } } void DeserializationTests::TestNestedCommandWithoutName() { // This test tests a nested command without a name specified. This type // of command should just be ignored, since we can't auto-generate names // for nested commands, they _must_ have names specified. static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { "name": "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "historySize": 1, "commandline": "cmd.exe" }, { "name": "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "historySize": 2, "commandline": "pwsh.exe" }, { "name": "profile2", "historySize": 3, "commandline": "wsl.exe" } ], "actions": [ { "commands": [ { "name": "child1", "command": { "action": "newTab", "commandline": "ssh me@first.com" } }, { "name": "child2", "command": { "action": "newTab", "commandline": "ssh me@second.com" } } ] }, ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_EQUAL(0u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); // Because the "parent" command didn't have a name, it couldn't be // placed into the list of commands. It and it's children are just // ignored. VERIFY_ARE_EQUAL(0u, settings->ActionMap().NameMap().Size()); } void DeserializationTests::TestNestedCommandWithBadSubCommands() { // This test tests a nested command without a name specified. This type // of command should just be ignored, since we can't auto-generate names // for nested commands, they _must_ have names specified. static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { "name": "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "historySize": 1, "commandline": "cmd.exe" } ], "actions": [ { "name": "nested command", "commands": [ { "name": "child1" }, { "name": "child2" } ] }, ] })" }; const auto settings = createSettings(settingsJson); VERIFY_ARE_EQUAL(2u, settings->Warnings().Size()); VERIFY_ARE_EQUAL(SettingsLoadWarnings::AtLeastOneKeybindingWarning, settings->Warnings().GetAt(0)); VERIFY_ARE_EQUAL(SettingsLoadWarnings::FailedToParseSubCommands, settings->Warnings().GetAt(1)); const auto& nameMap{ settings->ActionMap().NameMap() }; VERIFY_ARE_EQUAL(0u, nameMap.Size()); } void DeserializationTests::TestUnbindNestedCommand() { // Test that layering a command with `"commands": null` set will unbind a command that already exists. static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { "name": "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "historySize": 1, "commandline": "cmd.exe" }, { "name": "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "historySize": 2, "commandline": "pwsh.exe" }, { "name": "profile2", "historySize": 3, "commandline": "wsl.exe" } ], "actions": [ { "name": "parent", "commands": [ { "name": "child1", "command": { "action": "newTab", "commandline": "ssh me@first.com" } }, { "name": "child2", "command": { "action": "newTab", "commandline": "ssh me@second.com" } } ] }, ] })" }; static constexpr std::string_view settings1Json{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "actions": [ { "name": "parent", "commands": null }, ], })" }; const auto settings = winrt::make_self(settings1Json, settingsJson); VERIFY_ARE_EQUAL(3u, settings->AllProfiles().Size()); VERIFY_ARE_EQUAL(0u, settings->ActionMap().NameMap().Size()); } void DeserializationTests::TestRebindNestedCommand() { // Test that layering a command with an action set on top of a command // with nested commands replaces the nested commands with an action. static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { "name": "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "historySize": 1, "commandline": "cmd.exe" }, { "name": "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "historySize": 2, "commandline": "pwsh.exe" }, { "name": "profile2", "historySize": 3, "commandline": "wsl.exe" } ], "actions": [ { "name": "parent", "commands": [ { "name": "child1", "command": { "action": "newTab", "commandline": "ssh me@first.com" } }, { "name": "child2", "command": { "action": "newTab", "commandline": "ssh me@second.com" } } ] }, ] })" }; static constexpr std::string_view settings1Json{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "actions": [ { "name": "parent", "command": "newTab" }, ], })" }; const auto settings = winrt::make_self(settings1Json, settingsJson); const auto nameMap = settings->ActionMap().NameMap(); VERIFY_ARE_EQUAL(1u, nameMap.Size()); { const winrt::hstring commandName{ L"parent" }; const auto commandProj = nameMap.TryLookup(commandName); VERIFY_IS_NOT_NULL(commandProj); const auto actionAndArgs = commandProj.ActionAndArgs(); VERIFY_IS_NOT_NULL(actionAndArgs); VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actionAndArgs.Action()); const auto& realArgs = actionAndArgs.Args().try_as(); VERIFY_IS_NOT_NULL(realArgs); winrt::com_ptr commandImpl; commandImpl.copy_from(winrt::get_self(commandProj)); VERIFY_IS_FALSE(commandImpl->HasNestedCommands()); } } void DeserializationTests::TestCopy() { static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "initialCols": 50, "profiles": [ { "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "name": "Custom Profile", "fontFace": "Cascadia Code" } ], "schemes": [ { "name": "Campbell, but for a test", "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" } ], "actions": [ { "command": "openSettings", "keys": "ctrl+," }, { "command": { "action": "openSettings", "target": "defaultsFile" }, "keys": "ctrl+alt+," }, { "name": { "key": "SetColorSchemeParentCommandName" }, "commands": [ { "iterateOn": "schemes", "name": "${scheme.name}", "command": { "action": "setColorScheme", "colorScheme": "${scheme.name}" } } ] } ] })" }; const auto settings{ winrt::make_self(settingsJson) }; const auto copy{ settings->Copy() }; const auto copyImpl{ winrt::get_self(copy) }; // test globals VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), copyImpl->GlobalSettings().DefaultProfile()); // test profiles VERIFY_ARE_EQUAL(settings->AllProfiles().Size(), copyImpl->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Name(), copyImpl->AllProfiles().GetAt(0).Name()); // test schemes const auto schemeName{ L"Campbell, but for a test" }; VERIFY_ARE_EQUAL(settings->GlobalSettings().ColorSchemes().Size(), copyImpl->GlobalSettings().ColorSchemes().Size()); VERIFY_ARE_EQUAL(settings->GlobalSettings().ColorSchemes().HasKey(schemeName), copyImpl->GlobalSettings().ColorSchemes().HasKey(schemeName)); // test actions VERIFY_ARE_EQUAL(settings->GlobalSettings().ActionMap().KeyBindings().Size(), copyImpl->GlobalSettings().ActionMap().KeyBindings().Size()); const auto& nameMapOriginal{ settings->GlobalSettings().ActionMap().NameMap() }; const auto& nameMapCopy{ copyImpl->GlobalSettings().ActionMap().NameMap() }; VERIFY_ARE_EQUAL(nameMapOriginal.Size(), nameMapCopy.Size()); // Test that changing the copy should not change the original VERIFY_ARE_EQUAL(settings->GlobalSettings().WordDelimiters(), copyImpl->GlobalSettings().WordDelimiters()); copyImpl->GlobalSettings().WordDelimiters(L"changed value"); VERIFY_ARE_NOT_EQUAL(settings->GlobalSettings().WordDelimiters(), copyImpl->GlobalSettings().WordDelimiters()); } void DeserializationTests::TestCloneInheritanceTree() { static constexpr std::string_view settingsJson{ R"( { "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", "profiles": { "defaults": { "name": "PROFILE DEFAULTS" }, "list": [ { "guid": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", "name": "CMD" }, { "guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}", "name": "PowerShell" }, { "guid": "{61c54bbd-3333-5271-96e7-009a87ff44bf}" } ] } })" }; const auto settings{ winrt::make_self(settingsJson) }; const auto copy{ settings->Copy() }; const auto copyImpl{ winrt::get_self(copy) }; // test globals VERIFY_ARE_EQUAL(settings->GlobalSettings().DefaultProfile(), copyImpl->GlobalSettings().DefaultProfile()); // test profiles VERIFY_ARE_EQUAL(settings->AllProfiles().Size(), copyImpl->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Name(), copyImpl->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(1).Name(), copyImpl->AllProfiles().GetAt(1).Name()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(2).Name(), copyImpl->AllProfiles().GetAt(2).Name()); VERIFY_ARE_EQUAL(settings->ProfileDefaults().Name(), copyImpl->ProfileDefaults().Name()); // Modifying profile.defaults should... VERIFY_ARE_EQUAL(settings->ProfileDefaults().HasName(), copyImpl->ProfileDefaults().HasName()); copyImpl->ProfileDefaults().Name(L"changed value"); // ...keep the same name for the first two profiles VERIFY_ARE_EQUAL(settings->AllProfiles().Size(), copyImpl->AllProfiles().Size()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(0).Name(), copyImpl->AllProfiles().GetAt(0).Name()); VERIFY_ARE_EQUAL(settings->AllProfiles().GetAt(1).Name(), copyImpl->AllProfiles().GetAt(1).Name()); // ...but change the name for the one that inherited it from profile.defaults VERIFY_ARE_NOT_EQUAL(settings->AllProfiles().GetAt(2).Name(), copyImpl->AllProfiles().GetAt(2).Name()); // profile.defaults should be different between the two graphs VERIFY_ARE_EQUAL(settings->ProfileDefaults().HasName(), copyImpl->ProfileDefaults().HasName()); VERIFY_ARE_NOT_EQUAL(settings->ProfileDefaults().Name(), copyImpl->ProfileDefaults().Name()); Log::Comment(L"Test empty profiles.defaults"); static constexpr std::string_view emptyPDJson{ R"( { "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", "profiles": { "defaults": { }, "list": [ { "guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}", "name": "PowerShell" } ] } })" }; static constexpr std::string_view missingPDJson{ R"( { "defaultProfile": "{61c54bbd-1111-5271-96e7-009a87ff44bf}", "profiles": [ { "guid": "{61c54bbd-2222-5271-96e7-009a87ff44bf}", "name": "PowerShell" } ] })" }; auto verifyEmptyPD = [this](const auto json) { const auto settings{ winrt::make_self(json) }; const auto copy{ settings->Copy() }; const auto copyImpl{ winrt::get_self(copy) }; // if we don't have profiles.defaults, it should still be in the tree VERIFY_IS_NOT_NULL(settings->ProfileDefaults()); VERIFY_IS_NOT_NULL(copyImpl->ProfileDefaults()); VERIFY_ARE_EQUAL(settings->ActiveProfiles().Size(), 1u); VERIFY_ARE_EQUAL(settings->ActiveProfiles().Size(), copyImpl->ActiveProfiles().Size()); // so we should only have one parent, instead of two const auto srcProfile{ winrt::get_self(settings->ActiveProfiles().GetAt(0)) }; const auto copyProfile{ winrt::get_self(copyImpl->ActiveProfiles().GetAt(0)) }; VERIFY_ARE_EQUAL(srcProfile->Parents().size(), 1u); VERIFY_ARE_EQUAL(srcProfile->Parents().size(), copyProfile->Parents().size()); }; verifyEmptyPD(emptyPDJson); verifyEmptyPD(missingPDJson); } void DeserializationTests::TestValidDefaults() { // GH#8146: A LoadDefaults call should populate the list of active profiles const auto settings{ CascadiaSettings::LoadDefaults() }; VERIFY_ARE_EQUAL(settings.ActiveProfiles().Size(), settings.AllProfiles().Size()); VERIFY_ARE_EQUAL(settings.AllProfiles().Size(), 2u); } void DeserializationTests::TestInheritedCommand() { // Test unbinding a command's key chord or name that originated in another layer. static constexpr std::string_view settings1Json{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "profiles": [ { "name": "profile0", "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "historySize": 1, "commandline": "cmd.exe" }, { "name": "profile1", "guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", "historySize": 2, "commandline": "pwsh.exe" }, { "name": "profile2", "historySize": 3, "commandline": "wsl.exe" } ], "actions": [ { "name": "foo", "command": "closePane", "keys": "ctrl+shift+w" } ] })" }; static constexpr std::string_view settings2Json{ R"( { "defaultProfile": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "actions": [ { "command": null, "keys": "ctrl+shift+w" }, { "name": "bar", "command": "closePane" }, ], })" }; const auto settings = winrt::make_self(settings2Json, settings1Json); const KeyChord expectedKeyChord{ true, false, true, false, static_cast('W'), 0 }; const auto nameMap = settings->ActionMap().NameMap(); VERIFY_ARE_EQUAL(1u, nameMap.Size()); { // Verify NameMap returns correct value const auto& cmd{ nameMap.TryLookup(L"bar") }; VERIFY_IS_NOT_NULL(cmd); VERIFY_IS_NULL(cmd.Keys()); VERIFY_ARE_EQUAL(L"bar", cmd.Name()); } { // Verify ActionMap::GetActionByKeyChord API const auto& cmd{ settings->ActionMap().GetActionByKeyChord(expectedKeyChord) }; VERIFY_IS_NULL(cmd); } { // Verify ActionMap::GetKeyBindingForAction API const auto& actualKeyChord{ settings->ActionMap().GetKeyBindingForAction(ShortcutAction::ClosePane) }; VERIFY_IS_NULL(actualKeyChord); } } // This test ensures GH#11597 doesn't regress. void DeserializationTests::LoadFragmentsWithMultipleUpdates() { static constexpr std::wstring_view fragmentSource{ L"fragment" }; static constexpr std::string_view fragmentJson{ R"({ "profiles": [ { "updates": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "cursorShape": "filledBox" }, { "updates": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "cursorShape": "filledBox" }, { "guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}", "commandline": "cmd.exe" } ] })" }; implementation::SettingsLoader loader{ std::string_view{}, DefaultJson }; loader.MergeInboxIntoUserSettings(); loader.MergeFragmentIntoUserSettings(winrt::hstring{ fragmentSource }, fragmentJson); loader.FinalizeLayering(); VERIFY_IS_FALSE(loader.duplicateProfile); VERIFY_ARE_EQUAL(3u, loader.userSettings.profiles.size()); } }