terminal/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
Leonard Hecker 168d28b036
Reduce usage of Json::Value throughout Terminal.Settings.Model (#11184)
This commit reduces the code surface that interacts with raw JSON data,
reducing code complexity and improving maintainability.
Files that needed to be changed drastically were additionally
cleaned up to remove any code cruft that has accrued over time.

In order to facility this the following changes were made:
* Move JSON handling from `CascadiaSettings` into `SettingsLoader`
  This allows us to use STL containers for data model instances.
  For instance profiles are now added to a hashmap for O(1) lookup.
* JSON parsing within `SettingsLoader` doesn't differentiate between user,
  inbox and fragment JSON data, reducing code complexity and size.
  It also centralizes common concerns, like profile deduplication and
  ensuring that all profiles are assigned a GUID.
* Direct JSON modification, like the insertion of dynamic profiles into
  settings.json were removed. This vastly reduces code complexity,
  but unfortunately removes support for comments in JSON on first start.
* `ColorScheme`s cannot be layered. As such its `LayerJson` API was replaced
  with `FromJson`, allowing us to remove JSON-based color scheme validation.
* `Profile`s used to test their wish to layer using `ShouldBeLayered`, which
  was replaced with a GUID-based hashmap lookup on previously parsed profiles.

Further changes were made as improvements upon the previous changes:
* Compact the JSON files embedded binary, saving 28kB
* Prevent double-initialization of the color table in `ColorScheme`
* Making `til::color` getters `constexpr`, allow better optimizations

The result is a reduction of:
* 48kB binary size for the Settings.Model.dll
* 5-10% startup duration
* 26% code for the `CascadiaSettings` class
* 1% overall code in this project

Furthermore this results in the following breaking changes:
* The long deprecated "globals" settings object will not be detected and no
  warning will be created during load.
* The initial creation of a new settings.json will not produce helpful comments.

Both cases are caused by the removal of manual JSON handling and the
move to representing the settings file with model objects instead

## PR Checklist
* [x] Closes #5276
* [x] Closes #7421
* [x] I work here
* [x] Tests added/passed

## Validation Steps Performed

* Out-of-box-experience is identical to before ✔️
  (Except for the settings.json file lacking comments.)
* Existing user settings load correctly ✔️
* New WSL instances are added to user settings ✔️
* New fragments are added to user settings ✔️
* All profiles are assigned GUIDs ✔️
2021-09-22 16:27:31 +00:00

799 lines
36 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "../TerminalControl/EventArgs.h"
#include "../TerminalControl/ControlInteractivity.h"
#include "MockControlSettings.h"
#include "MockConnection.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;
using namespace ::Microsoft::Terminal::Core;
using namespace ::Microsoft::Console::VirtualTerminal;
namespace ControlUnitTests
{
class ControlInteractivityTests
{
BEGIN_TEST_CLASS(ControlInteractivityTests)
TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout
END_TEST_CLASS()
TEST_METHOD(TestAdjustAcrylic);
TEST_METHOD(TestScrollWithMouse);
TEST_METHOD(CreateSubsequentSelectionWithDragging);
TEST_METHOD(ScrollWithSelection);
TEST_METHOD(TestScrollWithTrackpad);
TEST_METHOD(TestQuickDragOnSelect);
TEST_METHOD(TestDragSelectOutsideBounds);
TEST_METHOD(PointerClickOutsideActiveRegion);
TEST_METHOD(IncrementCircularBufferWithSelection);
TEST_CLASS_SETUP(ClassSetup)
{
winrt::init_apartment(winrt::apartment_type::single_threaded);
return true;
}
TEST_CLASS_CLEANUP(ClassCleanup)
{
winrt::uninit_apartment();
return true;
}
std::tuple<winrt::com_ptr<MockControlSettings>,
winrt::com_ptr<MockConnection>>
_createSettingsAndConnection()
{
Log::Comment(L"Create settings object");
auto settings = winrt::make_self<MockControlSettings>();
VERIFY_IS_NOT_NULL(settings);
Log::Comment(L"Create connection object");
auto conn = winrt::make_self<MockConnection>();
VERIFY_IS_NOT_NULL(conn);
return { settings, conn };
}
std::tuple<winrt::com_ptr<Control::implementation::ControlCore>,
winrt::com_ptr<Control::implementation::ControlInteractivity>>
_createCoreAndInteractivity(Control::IControlSettings settings,
TerminalConnection::ITerminalConnection conn)
{
Log::Comment(L"Create ControlInteractivity object");
auto interactivity = winrt::make_self<Control::implementation::ControlInteractivity>(settings, conn);
VERIFY_IS_NOT_NULL(interactivity);
auto core = interactivity->_core;
core->_inUnitTests = true;
VERIFY_IS_NOT_NULL(core);
return { core, interactivity };
}
void _standardInit(winrt::com_ptr<Control::implementation::ControlCore> core,
winrt::com_ptr<Control::implementation::ControlInteractivity> interactivity)
{
// "Consolas" ends up with an actual size of 9x21 at 96DPI. So
// let's just arbitrarily start with a 270x420px (30x20 chars) window
core->Initialize(270, 420, 1.0);
VERIFY_IS_TRUE(core->_initializedTerminal);
VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height());
interactivity->Initialize();
}
};
void ControlInteractivityTests::TestAdjustAcrylic()
{
Log::Comment(L"Test that scrolling the mouse wheel with Ctrl+Shift changes opacity");
Log::Comment(L"(This test won't log as it goes, because it does some 200 verifications.)");
WEX::TestExecution::SetVerifyOutput verifyOutputScope{ WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures };
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:useAcrylic", L"{true, false}")
END_TEST_METHOD_PROPERTIES()
bool useAcrylic;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"useAcrylic", useAcrylic), L"whether or not we should enable acrylic");
auto [settings, conn] = _createSettingsAndConnection();
settings->UseAcrylic(useAcrylic);
settings->Opacity(0.5f);
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
// A callback to make sure that we're raising TransparencyChanged events
double expectedOpacity = 0.5;
auto opacityCallback = [&](auto&&, Control::TransparencyChangedEventArgs args) mutable {
VERIFY_ARE_EQUAL(expectedOpacity, args.Opacity());
VERIFY_ARE_EQUAL(expectedOpacity, settings->Opacity());
VERIFY_ARE_EQUAL(expectedOpacity, core->_settings.Opacity());
VERIFY_ARE_EQUAL(useAcrylic, settings->UseAcrylic());
VERIFY_ARE_EQUAL(useAcrylic, core->_settings.UseAcrylic());
};
core->TransparencyChanged(opacityCallback);
const auto modifiers = ControlKeyStates(ControlKeyStates::RightCtrlPressed | ControlKeyStates::ShiftPressed);
const Control::MouseButtonState buttonState{};
Log::Comment(L"Scroll in the positive direction, increasing opacity");
// Scroll more than enough times to get to 1.0 from .5.
for (int i = 0; i < 55; i++)
{
// each mouse wheel only adjusts opacity by .01
expectedOpacity += 0.01;
if (expectedOpacity >= 1.0)
{
expectedOpacity = 1.0;
}
// The mouse location and buttons don't matter here.
interactivity->MouseWheel(modifiers,
30,
til::point{ 0, 0 },
buttonState);
}
Log::Comment(L"Scroll in the negative direction, decreasing opacity");
// Scroll more than enough times to get to 0.0 from 1.0
for (int i = 0; i < 105; i++)
{
// each mouse wheel only adjusts opacity by .01
expectedOpacity -= 0.01;
if (expectedOpacity <= 0.0)
{
expectedOpacity = 0.0;
}
// The mouse location and buttons don't matter here.
interactivity->MouseWheel(modifiers,
-30,
til::point{ 0, 0 },
buttonState);
}
}
void ControlInteractivityTests::TestScrollWithMouse()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{};
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For the sake of this test, scroll one line at a time
interactivity->_rowsToScroll = 1;
int expectedTop = 0;
int expectedViewHeight = 20;
int expectedBufferHeight = 20;
auto scrollChangedHandler = [&](auto&&, const Control::ScrollPositionChangedArgs& args) mutable {
VERIFY_ARE_EQUAL(expectedTop, args.ViewTop());
VERIFY_ARE_EQUAL(expectedViewHeight, args.ViewHeight());
VERIFY_ARE_EQUAL(expectedBufferHeight, args.BufferSize());
};
core->ScrollPositionChanged(scrollChangedHandler);
interactivity->ScrollPositionChanged(scrollChangedHandler);
for (int i = 0; i < 40; ++i)
{
Log::Comment(NoThrowString().Format(L"Writing line #%d", i));
// The \r\n in the 19th loop will cause the view to start moving
if (i >= 19)
{
expectedTop++;
expectedBufferHeight++;
}
conn->WriteInput(L"Foo\r\n");
}
// We printed that 40 times, but the final \r\n bumped the view down one MORE row.
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"Scroll up a line");
const Control::MouseButtonState buttonState{};
const auto modifiers = ControlKeyStates();
expectedBufferHeight = 41;
expectedTop = 20;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
Log::Comment(L"Scroll up 19 more times, to the top");
for (int i = 0; i < 20; ++i)
{
expectedTop--;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
}
Log::Comment(L"Scrolling up more should do nothing");
expectedTop = 0;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
Log::Comment(L"Scroll down 21 more times, to the bottom");
for (int i = 0; i < 21; ++i)
{
Log::Comment(NoThrowString().Format(L"---scroll down #%d---", i));
expectedTop++;
interactivity->MouseWheel(modifiers,
-WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
Log::Comment(NoThrowString().Format(L"internal scrollbar pos:%f", interactivity->_internalScrollbarPosition));
}
Log::Comment(L"Scrolling down more should do nothing");
expectedTop = 21;
interactivity->MouseWheel(modifiers,
-WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
interactivity->MouseWheel(modifiers,
-WHEEL_DELTA,
til::point{ 0, 0 },
buttonState);
}
void ControlInteractivityTests::CreateSubsequentSelectionWithDragging()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
// This is a test for GH#9725
WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{};
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For this test, don't use any modifiers
const auto modifiers = ControlKeyStates();
const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown };
const Control::MouseButtonState noMouseDown{};
const til::size fontSize{ 9, 21 };
Log::Comment(L"Click on the terminal");
const til::point terminalPosition0{ 0, 0 };
const til::point cursorPosition0 = terminalPosition0 * fontSize;
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition0);
Log::Comment(L"Verify that there's not yet a selection");
VERIFY_IS_FALSE(core->HasSelection());
Log::Comment(L"Drag the mouse just a little");
// move not quite a whole cell, but enough to start a selection
const til::point terminalPosition1{ 0, 0 };
const til::point cursorPosition1{ 6, 0 };
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition1,
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Drag the mouse down a whole row");
const til::point terminalPosition2{ 1, 1 };
const til::point cursorPosition2 = terminalPosition2 * fontSize;
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition2,
true);
Log::Comment(L"Verify that there's now two selections (one on each row)");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Release the mouse");
interactivity->PointerReleased(noMouseDown,
WM_LBUTTONUP, //pointerUpdateKind
modifiers,
cursorPosition2);
Log::Comment(L"Verify that there's still two selections");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"click outside the current selection");
const til::point terminalPosition3{ 2, 2 };
const til::point cursorPosition3 = terminalPosition3 * fontSize;
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition3);
Log::Comment(L"Verify that there's now no selection");
VERIFY_IS_FALSE(core->HasSelection());
VERIFY_ARE_EQUAL(0u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Drag the mouse");
const til::point terminalPosition4{ 3, 2 };
const til::point cursorPosition4 = terminalPosition4 * fontSize;
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition4,
true);
Log::Comment(L"Verify that there's now one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
}
void ControlInteractivityTests::ScrollWithSelection()
{
// This is a test for GH#9955.a
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For the sake of this test, scroll one line at a time
interactivity->_rowsToScroll = 1;
Log::Comment(L"Add some test to the terminal so we can scroll");
for (int i = 0; i < 40; ++i)
{
conn->WriteInput(L"Foo\r\n");
}
// We printed that 40 times, but the final \r\n bumped the view down one MORE row.
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());
// For this test, don't use any modifiers
const auto modifiers = ControlKeyStates();
const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown };
const til::size fontSize{ 9, 21 };
Log::Comment(L"Click on the terminal");
const til::point terminalPosition0{ 5, 5 };
const til::point cursorPosition0{ terminalPosition0 * fontSize };
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition0);
Log::Comment(L"Verify that there's not yet a selection");
VERIFY_IS_FALSE(core->HasSelection());
VERIFY_IS_TRUE(interactivity->_singleClickTouchdownPos.has_value());
VERIFY_ARE_EQUAL(cursorPosition0, interactivity->_singleClickTouchdownPos.value());
Log::Comment(L"Drag the mouse just a little");
// move not quite a whole cell, but enough to start a selection
const til::point cursorPosition1{ cursorPosition0 + til::point{ 6, 0 } };
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition1,
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify the location of the selection");
// The viewport is on row 21, so the selection will be on:
// {(5, 5)+(0, 21)} to {(5, 5)+(0, 21)}
COORD expectedAnchor{ 5, 26 };
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionEnd());
Log::Comment(L"Scroll up a line, with the left mouse button selected");
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
cursorPosition1,
leftMouseDown);
Log::Comment(L"Verify the location of the selection");
// The viewport is now on row 20, so the selection will be on:
// {(5, 5)+(0, 20)} to {(5, 5)+(0, 21)}
COORD newExpectedAnchor{ 5, 25 };
// Remember, the anchor is always before the end in the buffer. So yes,
// se started the selection on 5,26, but now that's the end.
VERIFY_ARE_EQUAL(newExpectedAnchor, core->_terminal->GetSelectionAnchor());
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionEnd());
}
void ControlInteractivityTests::TestScrollWithTrackpad()
{
WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{};
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For the sake of this test, scroll one line at a time
interactivity->_rowsToScroll = 1;
for (int i = 0; i < 40; ++i)
{
conn->WriteInput(L"Foo\r\n");
}
// We printed that 40 times, but the final \r\n bumped the view down one MORE row.
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"Scroll up a line");
const auto modifiers = ControlKeyStates();
// Deltas that I saw while scrolling with the surface laptop trackpad
// were on the range [-22, 7], though I'm sure they could be greater in
// magnitude.
//
// WHEEL_DELTA is 120, so we'll use 24 for now as the delta, just so the tests don't take forever.
const int delta = WHEEL_DELTA / 5;
const til::point mousePos{ 0, 0 };
Control::MouseButtonState state{};
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
Log::Comment(L"Scroll up 4 more times. Once we're at 3/5 scrolls, "
L"we'll round the internal scrollbar position to scrolling to the next row.");
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 3/5
VERIFY_ARE_EQUAL(20, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 4/5
VERIFY_ARE_EQUAL(20, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 5/5
VERIFY_ARE_EQUAL(20, core->ScrollOffset());
Log::Comment(L"Jump to line 5, so we can scroll down from there.");
interactivity->UpdateScrollbar(5);
VERIFY_ARE_EQUAL(5, core->ScrollOffset());
Log::Comment(L"Scroll down 5 times, at which point we should accumulate a whole row of delta.");
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(5, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(5, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 3/5
VERIFY_ARE_EQUAL(6, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 4/5
VERIFY_ARE_EQUAL(6, core->ScrollOffset());
interactivity->MouseWheel(modifiers, -delta, mousePos, state); // 5/5
VERIFY_ARE_EQUAL(6, core->ScrollOffset());
Log::Comment(L"Jump to the bottom.");
interactivity->UpdateScrollbar(21);
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
Log::Comment(L"Scroll a bit, then emit a line of text. We should reset our internal scroll position.");
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
conn->WriteInput(L"Foo\r\n");
VERIFY_ARE_EQUAL(22, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 1/5
VERIFY_ARE_EQUAL(22, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 2/5
VERIFY_ARE_EQUAL(22, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 3/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 4/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
interactivity->MouseWheel(modifiers, delta, mousePos, state); // 5/5
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
}
void ControlInteractivityTests::TestQuickDragOnSelect()
{
// This is a test for GH#9955.c
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For this test, don't use any modifiers
const auto modifiers = ControlKeyStates();
const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown };
const til::size fontSize{ 9, 21 };
Log::Comment(L"Click on the terminal");
const til::point cursorPosition0{ 6, 0 };
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition0);
Log::Comment(L"Verify that there's not yet a selection");
VERIFY_IS_FALSE(core->HasSelection());
VERIFY_IS_TRUE(interactivity->_singleClickTouchdownPos.has_value());
VERIFY_ARE_EQUAL(cursorPosition0, interactivity->_singleClickTouchdownPos.value());
Log::Comment(L"Drag the mouse a lot. This simulates dragging the mouse real fast.");
const til::point cursorPosition1{ 6 + fontSize.width<int>() * 2, 0 };
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition1,
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify that it started on the first cell we clicked on, not the one we dragged to");
COORD expectedAnchor{ 0, 0 };
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
}
void ControlInteractivityTests::TestDragSelectOutsideBounds()
{
// This is a test for GH#4603
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For this test, don't use any modifiers
const auto modifiers = ControlKeyStates();
const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown };
const Control::MouseButtonState noMouseDown{};
const til::size fontSize{ 9, 21 };
Log::Comment(L"Click on the terminal");
const til::point cursorPosition0{ 6, 0 };
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition0);
Log::Comment(L"Verify that there's not yet a selection");
VERIFY_IS_FALSE(core->HasSelection());
VERIFY_IS_TRUE(interactivity->_singleClickTouchdownPos.has_value());
VERIFY_ARE_EQUAL(cursorPosition0, interactivity->_singleClickTouchdownPos.value());
Log::Comment(L"Drag the mouse a lot. This simulates dragging the mouse real fast.");
const til::point cursorPosition1{ 6 + fontSize.width<int>() * 2, 0 };
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition1,
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify that it started on the first cell we clicked on, not the one we dragged to");
COORD expectedAnchor{ 0, 0 };
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
COORD expectedEnd{ 2, 0 };
VERIFY_ARE_EQUAL(expectedEnd, core->_terminal->GetSelectionEnd());
interactivity->PointerReleased(noMouseDown,
WM_LBUTTONUP,
modifiers,
cursorPosition1);
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
VERIFY_ARE_EQUAL(expectedEnd, core->_terminal->GetSelectionEnd());
Log::Comment(L"Simulate dragging the mouse into the control, without first clicking into the control");
const til::point cursorPosition2{ fontSize.width<int>() * 10, 0 };
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition2,
false);
Log::Comment(L"The selection should be unchanged.");
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
VERIFY_ARE_EQUAL(expectedEnd, core->_terminal->GetSelectionEnd());
}
void ControlInteractivityTests::PointerClickOutsideActiveRegion()
{
// This is a test for GH#10642
WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{};
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
// For this test, don't use any modifiers
const auto modifiers = ControlKeyStates();
const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown };
const Control::MouseButtonState noMouseDown{};
const til::size fontSize{ 9, 21 };
interactivity->_rowsToScroll = 1;
int expectedTop = 0;
int expectedViewHeight = 20;
int expectedBufferHeight = 20;
auto scrollChangedHandler = [&](auto&&, const Control::ScrollPositionChangedArgs& args) mutable {
VERIFY_ARE_EQUAL(expectedTop, args.ViewTop());
VERIFY_ARE_EQUAL(expectedViewHeight, args.ViewHeight());
VERIFY_ARE_EQUAL(expectedBufferHeight, args.BufferSize());
};
core->ScrollPositionChanged(scrollChangedHandler);
interactivity->ScrollPositionChanged(scrollChangedHandler);
for (int i = 0; i < 40; ++i)
{
Log::Comment(NoThrowString().Format(L"Writing line #%d", i));
// The \r\n in the 19th loop will cause the view to start moving
if (i >= 19)
{
expectedTop++;
expectedBufferHeight++;
}
conn->WriteInput(L"Foo\r\n");
}
// We printed that 40 times, but the final \r\n bumped the view down one MORE row.
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());
expectedBufferHeight = 41;
expectedTop = 21;
Log::Comment(L"Scroll up 10 times");
for (int i = 0; i < 11; ++i)
{
expectedTop--;
interactivity->MouseWheel(modifiers,
WHEEL_DELTA,
til::point{ 0, 0 },
noMouseDown);
}
// Enable VT mouse event tracking
conn->WriteInput(L"\x1b[?1003;1006h");
// Mouse clicks in the inactive region (i.e. the top 10 rows in this case) should not register
Log::Comment(L"Click on the terminal");
const til::point terminalPosition0{ 4, 4 };
const til::point cursorPosition0 = terminalPosition0 * fontSize;
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition0);
Log::Comment(L"Verify that there's not yet a selection");
VERIFY_IS_FALSE(core->HasSelection());
Log::Comment(L"Drag the mouse");
// move the mouse as if to make a selection
const til::point terminalPosition1{ 10, 4 };
const til::point cursorPosition1 = terminalPosition1 * fontSize;
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition1,
true);
Log::Comment(L"Verify that there's still no selection");
VERIFY_IS_FALSE(core->HasSelection());
}
void ControlInteractivityTests::IncrementCircularBufferWithSelection()
{
// This is a test for GH#10749
WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{};
auto [settings, conn] = _createSettingsAndConnection();
auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn);
_standardInit(core, interactivity);
Log::Comment(L"Fill up the history buffer");
// Output lines equal to history size + viewport height to make sure we're
// at the point where outputting more lines causes circular incrementing
for (int i = 0; i < settings->HistorySize() + core->ViewHeight(); ++i)
{
conn->WriteInput(L"Foo\r\n");
}
// For this test, don't use any modifiers
const auto modifiers = ControlKeyStates();
const Control::MouseButtonState leftMouseDown{ Control::MouseButtonState::IsLeftButtonDown };
const til::size fontSize{ 9, 21 };
Log::Comment(L"Click on the terminal");
const til::point terminalPosition0{ 5, 5 };
const til::point cursorPosition0{ terminalPosition0 * fontSize };
interactivity->PointerPressed(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
0, // timestamp
modifiers,
cursorPosition0);
Log::Comment(L"Verify that there's not yet a selection");
VERIFY_IS_FALSE(core->HasSelection());
VERIFY_IS_TRUE(interactivity->_singleClickTouchdownPos.has_value());
VERIFY_ARE_EQUAL(cursorPosition0, interactivity->_singleClickTouchdownPos.value());
Log::Comment(L"Drag the mouse just a little");
// move not quite a whole cell, but enough to start a selection
const til::point cursorPosition1{ cursorPosition0 + til::point{ 6, 0 } };
interactivity->PointerMoved(leftMouseDown,
WM_LBUTTONDOWN, //pointerUpdateKind
modifiers,
true, // focused,
cursorPosition1,
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify the location of the selection");
// The viewport is on row (historySize + 5), so the selection will be on:
// {(5, (historySize+5))+(0, 21)} to {(5, (historySize+5))+(0, 21)}
COORD expectedAnchor{ 5, gsl::narrow_cast<SHORT>(settings->HistorySize()) + 5 };
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionEnd());
Log::Comment(L"Output a line of text");
conn->WriteInput(L"Foo\r\n");
Log::Comment(L"Verify the location of the selection");
// The selection should now be 1 row lower
expectedAnchor.Y -= 1;
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionAnchor());
VERIFY_ARE_EQUAL(expectedAnchor, core->_terminal->GetSelectionEnd());
// Output enough text for the selection to get pushed off the buffer
for (int i = 0; i < settings->HistorySize() + core->ViewHeight(); ++i)
{
conn->WriteInput(L"Foo\r\n");
}
// Verify that the selection got reset
VERIFY_IS_FALSE(core->HasSelection());
}
}