2608e94822
Introduces a new TerminalSettingsModel (TSM) project. This project is responsible for (de)serializing and exposing Windows Terminal's settings as WinRT objects. ## References #885: TSM epic #1564: Settings UI is dependent on this for data binding and settings access #6904: TSM Spec In the process of ripping out TSM from TerminalApp, a few other changes were made to make this possible: 1. AppLogic's `ApplicationDisplayName` and `ApplicationVersion` was moved to `CascadiaSettings` - These are defined as static functions. They also no longer check if `AppLogic::Current()` is nullptr. 2. `enum LaunchMode` was moved from TerminalApp to TSM 3. `AzureConnectionType` and `TelnetConnectionType` were moved from the profile generators to their respective TerminalConnections 4. CascadiaSettings' `SettingsPath` and `DefaultSettingsPath` are exposed as `hstring` instead of `std::filesystem::path` 5. `Command::ExpandCommands()` was exposed via the IDL - This required some of the warnings to be saved to an `IVector` instead of `std::vector`, among some other small changes. 6. The localization resources had to be split into two halves. - Resource file linked in init.cpp. Verified at runtime thanks to the StaticResourceLoader. 7. Added constructors to some `ActionArgs` 8. Utils.h/cpp were moved to `cascadia/inc`. `JsonKey()` was moved to `JsonUtils`. Both TermApp and TSM need access to Utils.h/cpp. A large amount of work includes moving to the new namespace (`TerminalApp` --> `Microsoft::Terminal::Settings::Model`). Fixing the tests had its own complications. Testing required us to split up TSM into a DLL and LIB, similar to TermApp. Discussion on creating a non-local test variant can be found in #7743. Closes #885
336 lines
16 KiB
C++
336 lines
16 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
|
|
#include "../TerminalSettingsModel/CascadiaSettings.h"
|
|
#include "JsonTestClass.h"
|
|
#include "TestUtils.h"
|
|
|
|
using namespace Microsoft::Console;
|
|
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
|
using namespace winrt::Microsoft::Terminal::TerminalControl;
|
|
using namespace winrt::Windows::Foundation::Collections;
|
|
using namespace WEX::Logging;
|
|
using namespace WEX::TestExecution;
|
|
using namespace WEX::Common;
|
|
|
|
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 CommandTests : 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(CommandTests)
|
|
TEST_CLASS_PROPERTY(L"RunAs", L"UAP")
|
|
TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml")
|
|
END_TEST_CLASS()
|
|
|
|
TEST_METHOD(ManyCommandsSameAction);
|
|
TEST_METHOD(LayerCommand);
|
|
TEST_METHOD(TestSplitPaneArgs);
|
|
TEST_METHOD(TestResourceKeyName);
|
|
TEST_METHOD(TestAutogeneratedName);
|
|
TEST_METHOD(TestLayerOnAutogeneratedName);
|
|
|
|
TEST_CLASS_SETUP(ClassSetup)
|
|
{
|
|
InitializeJsonReader();
|
|
return true;
|
|
}
|
|
};
|
|
|
|
void CommandTests::ManyCommandsSameAction()
|
|
{
|
|
const std::string commands0String{ R"([ { "name":"action0", "command": "copy" } ])" };
|
|
const std::string commands1String{ R"([ { "name":"action1", "command": { "action": "copy", "singleLine": false } } ])" };
|
|
const std::string commands2String{ R"([
|
|
{ "name":"action2", "command": "paste" },
|
|
{ "name":"action3", "command": "paste" }
|
|
])" };
|
|
|
|
const auto commands0Json = VerifyParseSucceeded(commands0String);
|
|
const auto commands1Json = VerifyParseSucceeded(commands1String);
|
|
const auto commands2Json = VerifyParseSucceeded(commands2String);
|
|
|
|
IMap<winrt::hstring, Command> commands = winrt::single_threaded_map<winrt::hstring, Command>();
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
}
|
|
VERIFY_ARE_EQUAL(1u, commands.Size());
|
|
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands1Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
}
|
|
VERIFY_ARE_EQUAL(2u, commands.Size());
|
|
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands2Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
}
|
|
VERIFY_ARE_EQUAL(4u, commands.Size());
|
|
}
|
|
|
|
void CommandTests::LayerCommand()
|
|
{
|
|
// Each one of the commands in this test should layer upon the previous, overriding the action.
|
|
const std::string commands0String{ R"([ { "name":"action0", "command": "copy" } ])" };
|
|
const std::string commands1String{ R"([ { "name":"action0", "command": "paste" } ])" };
|
|
const std::string commands2String{ R"([ { "name":"action0", "command": "newTab" } ])" };
|
|
const std::string commands3String{ R"([ { "name":"action0", "command": null } ])" };
|
|
|
|
const auto commands0Json = VerifyParseSucceeded(commands0String);
|
|
const auto commands1Json = VerifyParseSucceeded(commands1String);
|
|
const auto commands2Json = VerifyParseSucceeded(commands2String);
|
|
const auto commands3Json = VerifyParseSucceeded(commands3String);
|
|
|
|
IMap<winrt::hstring, Command> commands = winrt::single_threaded_map<winrt::hstring, Command>();
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(1u, commands.Size());
|
|
auto command = commands.Lookup(L"action0");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<CopyTextArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
}
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands1Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(1u, commands.Size());
|
|
auto command = commands.Lookup(L"action0");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::PasteText, command.Action().Action());
|
|
VERIFY_IS_NULL(command.Action().Args());
|
|
}
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands2Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(1u, commands.Size());
|
|
auto command = commands.Lookup(L"action0");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::NewTab, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<NewTabArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
}
|
|
{
|
|
// This last command should "unbind" the action.
|
|
auto warnings = implementation::Command::LayerJson(commands, commands3Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
}
|
|
}
|
|
|
|
void CommandTests::TestSplitPaneArgs()
|
|
{
|
|
// This is the same as KeyBindingsTests::TestSplitPaneArgs, but with
|
|
// looking up the action and its args from a map of commands, instead
|
|
// of from keybindings.
|
|
|
|
const std::string commands0String{ R"([
|
|
{ "name": "command0", "command": { "action": "splitPane", "split": null } },
|
|
{ "name": "command1", "command": { "action": "splitPane", "split": "vertical" } },
|
|
{ "name": "command2", "command": { "action": "splitPane", "split": "horizontal" } },
|
|
{ "name": "command4", "command": { "action": "splitPane" } },
|
|
{ "name": "command5", "command": { "action": "splitPane", "split": "auto" } }
|
|
])" };
|
|
|
|
const auto commands0Json = VerifyParseSucceeded(commands0String);
|
|
|
|
IMap<winrt::hstring, Command> commands = winrt::single_threaded_map<winrt::hstring, Command>();
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(5u, commands.Size());
|
|
|
|
{
|
|
auto command = commands.Lookup(L"command0");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle());
|
|
}
|
|
{
|
|
auto command = commands.Lookup(L"command1");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle());
|
|
}
|
|
{
|
|
auto command = commands.Lookup(L"command2");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle());
|
|
}
|
|
{
|
|
auto command = commands.Lookup(L"command4");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle());
|
|
}
|
|
{
|
|
auto command = commands.Lookup(L"command5");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle());
|
|
}
|
|
}
|
|
void CommandTests::TestResourceKeyName()
|
|
{
|
|
// This test checks looking up a name from a resource key.
|
|
|
|
const std::string commands0String{ R"([ { "name": { "key": "DuplicateTabCommandKey"}, "command": "copy" } ])" };
|
|
const auto commands0Json = VerifyParseSucceeded(commands0String);
|
|
|
|
IMap<winrt::hstring, Command> commands = winrt::single_threaded_map<winrt::hstring, Command>();
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
{
|
|
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(1u, commands.Size());
|
|
|
|
// NOTE: We're relying on DuplicateTabCommandKey being defined as
|
|
// "Duplicate Tab" here. If that string changes in our resources,
|
|
// this test will break.
|
|
auto command = commands.Lookup(L"Duplicate tab");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::CopyText, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<CopyTextArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
}
|
|
}
|
|
|
|
void CommandTests::TestAutogeneratedName()
|
|
{
|
|
// Tests run in Helix can't report Skipped until GH#7286 is resolved.
|
|
// Set ignore flag to make Helix run completely overlook it.
|
|
BEGIN_TEST_METHOD_PROPERTIES()
|
|
TEST_METHOD_PROPERTY(L"Ignore", L"True")
|
|
END_TEST_METHOD_PROPERTIES()
|
|
|
|
// This test to be corrected as a part of GH#7281
|
|
|
|
// This test ensures that we'll correctly create commands for actions
|
|
// that don't have given names, pursuant to the spec in GH#6532.
|
|
|
|
// NOTE: The keys used to look up these commands are partially generated
|
|
// from strings in our Resources.resw. If those string values should
|
|
// change, it's likely that this test will break.
|
|
|
|
const std::string commands0String{ R"([
|
|
{ "command": { "action": "splitPane", "split": null } },
|
|
{ "command": { "action": "splitPane", "split": "vertical" } },
|
|
{ "command": { "action": "splitPane", "split": "horizontal" } },
|
|
{ "command": { "action": "splitPane", "split": "none" } },
|
|
{ "command": { "action": "splitPane" } },
|
|
{ "command": { "action": "splitPane", "split": "auto" } },
|
|
{ "command": { "action": "splitPane", "split": "foo" } }
|
|
])" };
|
|
|
|
const auto commands0Json = VerifyParseSucceeded(commands0String);
|
|
|
|
IMap<winrt::hstring, Command> commands = winrt::single_threaded_map<winrt::hstring, Command>();
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
|
|
// There are only 3 commands here: all of the `"none"`, `"auto"`,
|
|
// `"foo"`, `null`, and <no args> bindings all generate the same action,
|
|
// which will generate just a single name for all of them.
|
|
VERIFY_ARE_EQUAL(3u, commands.Size());
|
|
|
|
{
|
|
auto command = commands.Lookup(L"Split pane");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Automatic, realArgs.SplitStyle());
|
|
}
|
|
{
|
|
auto command = commands.Lookup(L"Split pane, split: vertical");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle());
|
|
}
|
|
{
|
|
auto command = commands.Lookup(L"Split pane, split: horizontal");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Horizontal, realArgs.SplitStyle());
|
|
}
|
|
}
|
|
void CommandTests::TestLayerOnAutogeneratedName()
|
|
{
|
|
const std::string commands0String{ R"([
|
|
{ "command": { "action": "splitPane" } },
|
|
{ "name":"Split pane", "command": { "action": "splitPane", "split": "vertical" } },
|
|
])" };
|
|
|
|
const auto commands0Json = VerifyParseSucceeded(commands0String);
|
|
|
|
IMap<winrt::hstring, Command> commands = winrt::single_threaded_map<winrt::hstring, Command>();
|
|
VERIFY_ARE_EQUAL(0u, commands.Size());
|
|
auto warnings = implementation::Command::LayerJson(commands, commands0Json);
|
|
VERIFY_ARE_EQUAL(0u, warnings.size());
|
|
VERIFY_ARE_EQUAL(1u, commands.Size());
|
|
|
|
{
|
|
auto command = commands.Lookup(L"Split pane");
|
|
VERIFY_IS_NOT_NULL(command);
|
|
VERIFY_IS_NOT_NULL(command.Action());
|
|
VERIFY_ARE_EQUAL(ShortcutAction::SplitPane, command.Action().Action());
|
|
const auto& realArgs = command.Action().Args().try_as<SplitPaneArgs>();
|
|
VERIFY_IS_NOT_NULL(realArgs);
|
|
// Verify the args have the expected value
|
|
VERIFY_ARE_EQUAL(SplitState::Vertical, realArgs.SplitStyle());
|
|
}
|
|
}
|
|
}
|