// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "../TerminalControl/EventArgs.h" #include "../TerminalControl/ControlCore.h" #include "MockControlSettings.h" #include "MockConnection.h" #include "../../inc/TestUtils.h" using namespace Microsoft::Console; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; using namespace winrt; using namespace winrt::Microsoft::Terminal; namespace ControlUnitTests { class ControlCoreTests { BEGIN_TEST_CLASS(ControlCoreTests) TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout END_TEST_CLASS() TEST_METHOD(ComPtrSettings); TEST_METHOD(InstantiateCore); TEST_METHOD(TestInitialize); TEST_METHOD(TestAdjustAcrylic); TEST_METHOD(TestRuntimeOpacitySurvivesSettingsReload); TEST_METHOD(TestReadOnlySurvivesSettingsReload); TEST_METHOD(TestFreeAfterClose); TEST_METHOD(TestFontInitializedInCtor); TEST_METHOD(TestClearScrollback); TEST_METHOD(TestClearScreen); TEST_METHOD(TestClearAll); TEST_METHOD(TestReadEntireBuffer); TEST_METHOD(TestSelectCommandSimple); TEST_METHOD(TestSelectOutputSimple); TEST_METHOD(TestCommandContext); TEST_METHOD(TestCommandContextWithPwshGhostText); TEST_METHOD(TestSelectOutputScrolling); TEST_METHOD(TestSelectOutputExactWrap); TEST_METHOD(TestSimpleClickSelection); TEST_CLASS_SETUP(ModuleSetup) { winrt::init_apartment(winrt::apartment_type::single_threaded); return true; } TEST_CLASS_CLEANUP(ClassCleanup) { winrt::uninit_apartment(); return true; } std::tuple, winrt::com_ptr> _createSettingsAndConnection() { Log::Comment(L"Create settings object"); auto settings = winrt::make_self(); VERIFY_IS_NOT_NULL(settings); Log::Comment(L"Create connection object"); auto conn = winrt::make_self(); VERIFY_IS_NOT_NULL(conn); return { settings, conn }; } winrt::com_ptr createCore(Control::IControlSettings settings, TerminalConnection::ITerminalConnection conn) { Log::Comment(L"Create ControlCore object"); auto core = winrt::make_self(settings, settings, conn); core->_inUnitTests = true; return core; } void _standardInit(winrt::com_ptr core) { // "Consolas" ends up with an actual size of 9x19 at 96DPI. So // let's just arbitrarily start with a 270x380px (30x20 chars) window core->Initialize(270, 380, 1.0); #ifndef NDEBUG core->_terminal->_suppressLockChecks = true; #endif VERIFY_IS_TRUE(core->_initializedTerminal); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); } }; void ControlCoreTests::ComPtrSettings() { Log::Comment(L"Just make sure we can instantiate a settings obj in a com_ptr"); auto settings = winrt::make_self(); Log::Comment(L"Verify literally any setting, it doesn't matter"); VERIFY_ARE_EQUAL(DEFAULT_FOREGROUND, settings->DefaultForeground()); } void ControlCoreTests::InstantiateCore() { auto [settings, conn] = _createSettingsAndConnection(); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); } void ControlCoreTests::TestInitialize() { auto [settings, conn] = _createSettingsAndConnection(); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); VERIFY_IS_FALSE(core->_initializedTerminal); // "Consolas" ends up with an actual size of 9x19 at 96DPI. So // let's just arbitrarily start with a 270x380px (30x20 chars) window core->Initialize(270, 380, 1.0); #ifndef NDEBUG core->_terminal->_suppressLockChecks = true; #endif VERIFY_IS_TRUE(core->_initializedTerminal); VERIFY_ARE_EQUAL(30, core->_terminal->GetViewport().Width()); } void ControlCoreTests::TestAdjustAcrylic() { auto [settings, conn] = _createSettingsAndConnection(); settings->UseAcrylic(true); settings->Opacity(0.5f); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); // A callback to make sure that we're raising TransparencyChanged events auto expectedOpacity = 0.5f; auto opacityCallback = [&](auto&&, Control::TransparencyChangedEventArgs args) mutable { VERIFY_ARE_EQUAL(expectedOpacity, args.Opacity()); VERIFY_ARE_EQUAL(expectedOpacity, core->Opacity()); // The Settings object's opacity shouldn't be changed VERIFY_ARE_EQUAL(0.5f, settings->Opacity()); if (expectedOpacity < 1.0f) { VERIFY_IS_TRUE(settings->UseAcrylic()); VERIFY_IS_TRUE(core->_settings.UseAcrylic()); } // GH#603: Adjusting opacity shouldn't change whether or not we // requested acrylic. auto expectedUseAcrylic = expectedOpacity < 1.0f; VERIFY_IS_TRUE(core->_settings.UseAcrylic()); VERIFY_ARE_EQUAL(expectedUseAcrylic, core->UseAcrylic()); }; core->TransparencyChanged(opacityCallback); VERIFY_IS_FALSE(core->_initializedTerminal); // "Cascadia Mono" ends up with an actual size of 9x19 at 96DPI. So // let's just arbitrarily start with a 270x380px (30x20 chars) window core->Initialize(270, 380, 1.0); VERIFY_IS_TRUE(core->_initializedTerminal); Log::Comment(L"Increasing opacity till fully opaque"); expectedOpacity += 0.1f; // = 0.6; core->AdjustOpacity(0.1f); expectedOpacity += 0.1f; // = 0.7; core->AdjustOpacity(0.1f); expectedOpacity += 0.1f; // = 0.8; core->AdjustOpacity(0.1f); expectedOpacity += 0.1f; // = 0.9; core->AdjustOpacity(0.1f); expectedOpacity += 0.1f; // = 1.0; // cast to float because floating point numbers are mean VERIFY_ARE_EQUAL(1.0f, expectedOpacity); core->AdjustOpacity(0.1f); Log::Comment(L"Increasing opacity more doesn't actually change it to be >1.0"); expectedOpacity = 1.0f; core->AdjustOpacity(0.1f); Log::Comment(L"Decrease opacity"); expectedOpacity -= 0.25f; // = 0.75; core->AdjustOpacity(-0.25f); expectedOpacity -= 0.25f; // = 0.5; core->AdjustOpacity(-0.25f); expectedOpacity -= 0.25f; // = 0.25; core->AdjustOpacity(-0.25f); expectedOpacity -= 0.25f; // = 0.05; // cast to float because floating point numbers are mean VERIFY_ARE_EQUAL(0.0f, expectedOpacity); core->AdjustOpacity(-0.25f); Log::Comment(L"Decreasing opacity more doesn't actually change it to be < 0"); expectedOpacity = 0.0f; core->AdjustOpacity(-0.25f); } void ControlCoreTests::TestRuntimeOpacitySurvivesSettingsReload() { // GH#12424: a runtime opacity adjustment (Ctrl+Shift+scroll / adjustOpacity) // must survive a settings reload as a delta over the new settings opacity, // rather than snapping back to the configured value. auto [settings, conn] = _createSettingsAndConnection(); settings->UseAcrylic(true); settings->Opacity(0.5f); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); core->Initialize(270, 380, 1.0); VERIFY_IS_TRUE(core->_initializedTerminal); Log::Comment(L"Adjust opacity at runtime: 0.5 -> 0.7 (delta +0.2)"); core->AdjustOpacity(0.2f); VERIFY_ARE_EQUAL(0.7f, core->Opacity()); Log::Comment(L"Reload settings with a different opacity (0.4). A reload builds a " L"fresh settings object, so use a separate mock here."); auto reloaded = winrt::make_self(); reloaded->UseAcrylic(true); reloaded->Opacity(0.4f); core->UpdateSettings(*reloaded, *reloaded); Log::Comment(L"The runtime delta (+0.2) rides on top of the new settings opacity " L"(0.4) -> 0.6, instead of being stomped back to 0.4."); VERIFY_ARE_EQUAL(0.6f, core->Opacity()); // Acrylic follows the resolved (runtime-preserved) opacity, which is < 1.0. VERIFY_IS_TRUE(core->UseAcrylic()); Log::Comment(L"With no runtime adjustment, a reload follows the settings opacity."); auto core2 = createCore(*settings, *conn); core2->Initialize(270, 380, 1.0); VERIFY_ARE_EQUAL(0.5f, core2->Opacity()); auto reloaded2 = winrt::make_self(); reloaded2->UseAcrylic(true); reloaded2->Opacity(0.3f); core2->UpdateSettings(*reloaded2, *reloaded2); VERIFY_ARE_EQUAL(0.3f, core2->Opacity()); } void ControlCoreTests::TestReadOnlySurvivesSettingsReload() { // GH#12424: a runtime read-only toggle must not be reset by a settings reload. auto [settings, conn] = _createSettingsAndConnection(); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); core->Initialize(270, 380, 1.0); VERIFY_IS_FALSE(core->IsInReadOnlyMode()); core->SetReadOnlyMode(true); VERIFY_IS_TRUE(core->IsInReadOnlyMode()); auto reloaded = winrt::make_self(); core->UpdateSettings(*reloaded, *reloaded); VERIFY_IS_TRUE(core->IsInReadOnlyMode()); } void ControlCoreTests::TestFreeAfterClose() { { auto [settings, conn] = _createSettingsAndConnection(); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); Log::Comment(L"Close the Core, like a TermControl would"); core->Close(); } VERIFY_IS_TRUE(true, L"Make sure that the test didn't crash when the core when out of scope"); } void ControlCoreTests::TestFontInitializedInCtor() { // This is to catch a dumb programming mistake I made while working on // the core/control split. We want the font initialized in the ctor, // before we even get to Core::Initialize. auto [settings, conn] = _createSettingsAndConnection(); // Make sure to use something dumb like "Impact" as a font name here so // that you don't default to Cascadia* settings->FontFace(L"Impact"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); VERIFY_ARE_EQUAL(L"Impact", std::wstring_view{ core->_actualFont.GetFaceName() }); } void ControlCoreTests::TestClearScrollback() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " L"(leaving the cursor after 'Bar')"); for (auto i = 0; i < 40; ++i) { conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n")); } conn->WriteInput(winrt_wstring_to_array_view(L"Bar")); // We printed that 40 times, but the final \r\n bumped the view down one MORE row. Log::Comment(L"Check the buffer viewport before the clear"); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(21, core->ScrollOffset()); VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(41, core->BufferHeight()); Log::Comment(L"Clear the buffer"); core->ClearBuffer(Control::ClearBufferType::Scrollback); Log::Comment(L"Check the buffer after the clear"); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(0, core->ScrollOffset()); VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(20, core->BufferHeight()); // In this test, we can't actually check if we cleared the buffer // contents. ConPTY will handle the actual clearing of the buffer // contents. We can only ensure that the viewport moved when we did a // clear scrollback. } void ControlCoreTests::TestClearScreen() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " L"(leaving the cursor after 'Bar')"); for (auto i = 0; i < 40; ++i) { conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n")); } conn->WriteInput(winrt_wstring_to_array_view(L"Bar")); // We printed that 40 times, but the final \r\n bumped the view down one MORE row. Log::Comment(L"Check the buffer viewport before the clear"); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(21, core->ScrollOffset()); VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(41, core->BufferHeight()); Log::Comment(L"Clear the buffer"); core->ClearBuffer(Control::ClearBufferType::Screen); Log::Comment(L"Check the buffer after the clear"); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(21, core->ScrollOffset()); VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(41, core->BufferHeight()); // In this test, we can't actually check if we cleared the buffer // contents. ConPTY will handle the actual clearing of the buffer // contents. We can only ensure that the viewport moved when we did a // clear scrollback. } void ControlCoreTests::TestClearAll() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " L"(leaving the cursor after 'Bar')"); for (auto i = 0; i < 40; ++i) { conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n")); } conn->WriteInput(winrt_wstring_to_array_view(L"Bar")); // We printed that 40 times, but the final \r\n bumped the view down one MORE row. Log::Comment(L"Check the buffer viewport before the clear"); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(21, core->ScrollOffset()); VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(41, core->BufferHeight()); Log::Comment(L"Clear the buffer"); core->ClearBuffer(Control::ClearBufferType::All); Log::Comment(L"Check the buffer after the clear"); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(0, core->ScrollOffset()); VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(20, core->BufferHeight()); // In this test, we can't actually check if we cleared the buffer // contents. ConPTY will handle the actual clearing of the buffer // contents. We can only ensure that the viewport moved when we did a // clear scrollback. } void ControlCoreTests::TestReadEntireBuffer() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); Log::Comment(L"Check the buffer contents"); VERIFY_ARE_EQUAL(L"This is some text\r\nwith varying amounts\r\nof whitespace\r\n", core->ReadEntireBuffer()); } static void _writePrompt(const winrt::com_ptr& conn, const std::wstring_view& path) { conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;D\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;A\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]9;9;")); conn->WriteInput(winrt_wstring_to_array_view(path)); conn->WriteInput(winrt_wstring_to_array_view(L"\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"PWSH ")); conn->WriteInput(winrt_wstring_to_array_view(path)); conn->WriteInput(winrt_wstring_to_array_view(L"> ")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;B\x7")); } void ControlCoreTests::TestSelectCommandSimple() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); _writePrompt(conn, L"C:\\Windows"); conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); _writePrompt(conn, L"C:\\Windows"); Log::Comment(L"Check the buffer contents"); const auto& buffer = core->_terminal->GetTextBuffer(); const auto& cursor = buffer.GetCursor(); { const til::point expectedCursor{ 17, 4 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); } VERIFY_IS_FALSE(core->HasSelection()); core->SelectCommand(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 17, 0 }; const til::point expectedEnd{ 24, 0 }; VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } core->_terminal->ClearSelection(); conn->WriteInput(winrt_wstring_to_array_view(L"Boo-far")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); VERIFY_IS_FALSE(core->HasSelection()); { const til::point expectedCursor{ 24, 4 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); } VERIFY_IS_FALSE(core->HasSelection()); core->SelectCommand(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 17, 4 }; const til::point expectedEnd{ 24, 4 }; VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } core->SelectCommand(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 17, 0 }; const til::point expectedEnd{ 24, 0 }; VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } core->SelectCommand(false); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 17, 4 }; const til::point expectedEnd{ 24, 4 }; VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } } void ControlCoreTests::TestSelectOutputSimple() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); _writePrompt(conn, L"C:\\Windows"); conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); _writePrompt(conn, L"C:\\Windows"); Log::Comment(L"Check the buffer contents"); const auto& buffer = core->_terminal->GetTextBuffer(); const auto& cursor = buffer.GetCursor(); { const til::point expectedCursor{ 17, 4 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); } VERIFY_IS_FALSE(core->HasSelection()); core->SelectOutput(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 24, 0 }; // The character after the prompt const til::point expectedEnd{ 22, 3 }; // x = the end of the text + 1 (exclusive end) VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } } void ControlCoreTests::TestCommandContext() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); _writePrompt(conn, L"C:\\Windows"); conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); _writePrompt(conn, L"C:\\Windows"); Log::Comment(L"Check the command context"); const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; { auto historyContext{ core->CommandHistory() }; VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); VERIFY_ARE_EQUAL(L"", historyContext.CurrentCommandline()); } Log::Comment(L"Write 'Bar' to the command..."); conn->WriteInput(winrt_wstring_to_array_view(L"Bar")); { auto historyContext{ core->CommandHistory() }; // Bar shouldn't be in the history, it should be the current command VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); VERIFY_ARE_EQUAL(L"Bar", historyContext.CurrentCommandline()); } Log::Comment(L"then delete it"); conn->WriteInput(winrt_wstring_to_array_view(L"\b \b")); conn->WriteInput(winrt_wstring_to_array_view(L"\b \b")); conn->WriteInput(winrt_wstring_to_array_view(L"\b \b")); { auto historyContext{ core->CommandHistory() }; VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); // The current commandline is now empty VERIFY_ARE_EQUAL(L"", historyContext.CurrentCommandline()); } } void ControlCoreTests::TestCommandContextWithPwshGhostText() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); _writePrompt(conn, L"C:\\Windows"); conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); _writePrompt(conn, L"C:\\Windows"); Log::Comment(L"Check the command context"); const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; { auto historyContext{ core->CommandHistory() }; VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); VERIFY_ARE_EQUAL(L"", historyContext.CurrentCommandline()); } Log::Comment(L"Write 'BarBar' to the command..."); conn->WriteInput(winrt_wstring_to_array_view(L"BarBar")); { auto historyContext{ core->CommandHistory() }; // BarBar shouldn't be in the history, it should be the current command VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); VERIFY_ARE_EQUAL(L"BarBar", historyContext.CurrentCommandline()); } Log::Comment(L"then move the cursor to the left"); // This emulates the state the buffer is in when pwsh does its "ghost // text" thing. We don't want to include all that ghost text in the // current commandline. conn->WriteInput(winrt_wstring_to_array_view(L"\x1b[D")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b[D")); { auto historyContext{ core->CommandHistory() }; VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); // The current commandline is only the text to the left of the cursor auto curr{ historyContext.CurrentCommandline() }; VERIFY_ARE_EQUAL(4u, curr.size()); VERIFY_ARE_EQUAL(L"BarB", curr); } } void ControlCoreTests::TestSelectOutputScrolling() { auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); _writePrompt(conn, L"C:\\Windows"); // row 0 conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar")); // row 0 conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); // row 1 conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); // row 2 conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); // row 3 _writePrompt(conn, L"C:\\Windows"); // row 4 conn->WriteInput(winrt_wstring_to_array_view(L"gci")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); // enough to scroll for (auto i = 0; i < 30; i++) // row 5-34 { conn->WriteInput(winrt_wstring_to_array_view(L"-a--- 2/8/2024 9:47 README\r\n")); } _writePrompt(conn, L"C:\\Windows"); Log::Comment(L"Check the buffer contents"); const auto& buffer = core->_terminal->GetTextBuffer(); const auto& cursor = buffer.GetCursor(); { const til::point expectedCursor{ 17, 35 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); } VERIFY_IS_FALSE(core->HasSelection()); // The second mark is the first one we'll see core->SelectOutput(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 20, 4 }; // The character after the prompt const til::point expectedEnd{ 27, 34 }; // x = the end of the text + 1 (exclusive end) VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } core->SelectOutput(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 24, 0 }; // The character after the prompt const til::point expectedEnd{ 22, 3 }; // x = the end of the text + 1 (exclusive end) VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } } void ControlCoreTests::TestSelectOutputExactWrap() { // Just like the TestSelectOutputScrolling test, but these lines will // exactly wrap to the right edge of the buffer, to catch an edge case // present in `ControlCore::_selectSpan` auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); Log::Comment(L"Print some text"); _writePrompt(conn, L"C:\\Windows"); // row 0 conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar")); // row 0 conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n")); // row 1 conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n")); // row 2 conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n")); // row 3 _writePrompt(conn, L"C:\\Windows"); // row 4 conn->WriteInput(winrt_wstring_to_array_view(L"gci")); conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7")); conn->WriteInput(winrt_wstring_to_array_view(L"\r\n")); // enough to scroll for (auto i = 0; i < 30; i++) // row 5-35 { conn->WriteInput(winrt_wstring_to_array_view(L"-a--- 2/8/2024 9:47 README.md\r\n")); } _writePrompt(conn, L"C:\\Windows"); Log::Comment(L"Check the buffer contents"); const auto& buffer = core->_terminal->GetTextBuffer(); const auto& cursor = buffer.GetCursor(); { const til::point expectedCursor{ 17, 35 }; VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); } VERIFY_IS_FALSE(core->HasSelection()); // The second mark is the first one we'll see core->SelectOutput(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 20, 4 }; // The character after the prompt const til::point expectedEnd{ 30, 34 }; // x = the end of the text + 1 (exclusive end) VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } core->SelectOutput(true); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 24, 0 }; // The character after the prompt const til::point expectedEnd{ 22, 3 }; // x = the end of the text + 1 (exclusive end) VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } } void ControlCoreTests::TestSimpleClickSelection() { // Create a simple selection with the mouse, then click somewhere else, // and confirm the selection got updated. auto [settings, conn] = _createSettingsAndConnection(); Log::Comment(L"Create ControlCore object"); auto core = createCore(*settings, *conn); VERIFY_IS_NOT_NULL(core); _standardInit(core); // Here, we're using the UpdateSelectionMarkers as a stand-in to check // if the selection got updated with the renderer. Standing up a whole // dummy renderer for this test would be not very ergonomic. Instead, we // are relying on ControlCore::_updateSelectionUI both // TriggerSelection()'ing and also rasing this event bool expectedSelectionUpdate = false; bool gotSelectionUpdate = false; core->UpdateSelectionMarkers([&](auto&& /*sender*/, auto&& /*args*/) { VERIFY_IS_TRUE(expectedSelectionUpdate); expectedSelectionUpdate = false; gotSelectionUpdate = true; }); auto needToCopy = false; expectedSelectionUpdate = true; core->LeftClickOnTerminal(til::point{ 1, 1 }, 1, false, true, false, needToCopy); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 1, 1 }; const til::point expectedEnd{ 1, 1 }; VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } VERIFY_IS_TRUE(gotSelectionUpdate); expectedSelectionUpdate = true; core->LeftClickOnTerminal(til::point{ 1, 2 }, 1, false, true, false, needToCopy); VERIFY_IS_TRUE(core->HasSelection()); { const auto& start = core->_terminal->GetSelectionAnchor(); const auto& end = core->_terminal->GetSelectionEnd(); const til::point expectedStart{ 1, 1 }; const til::point expectedEnd{ 2, 2 }; VERIFY_ARE_EQUAL(expectedStart, start); VERIFY_ARE_EQUAL(expectedEnd, end); } VERIFY_IS_TRUE(gotSelectionUpdate); } }