// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "WexTestClass.h" #include "../inc/consoletaeftemplates.hpp" #include "CommonState.hpp" #include "globals.h" #include "../buffer/out/textBuffer.hpp" #include "../buffer/out/CharRow.hpp" #include "input.h" #include "_stream.h" #include "../interactivity/inc/ServiceLocator.hpp" #include "../renderer/inc/DummyRenderTarget.hpp" using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::VirtualTerminal; using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; class TextBufferTests { DummyRenderTarget _renderTarget; CommonState* m_state; TEST_CLASS(TextBufferTests); TEST_CLASS_SETUP(ClassSetup) { m_state = new CommonState(); m_state->PrepareGlobalFont(); m_state->PrepareGlobalScreenBuffer(); return true; } TEST_CLASS_CLEANUP(ClassCleanup) { m_state->CleanupGlobalScreenBuffer(); m_state->CleanupGlobalFont(); delete m_state; return true; } TEST_METHOD_SETUP(MethodSetup) { m_state->PrepareNewTextBufferInfo(); return true; } TEST_METHOD_CLEANUP(MethodCleanup) { m_state->CleanupNewTextBufferInfo(); return true; } TEST_METHOD(TestBufferCreate); TextBuffer& GetTbi(); SHORT GetBufferWidth(); SHORT GetBufferHeight(); TEST_METHOD(TestBufferRowByOffset); TEST_METHOD(TestWrapFlag); TEST_METHOD(TestWrapThroughWriteLine); TEST_METHOD(TestDoubleBytePadFlag); void DoBoundaryTest(PCWCHAR const pwszInputString, short const cLength, short const cMax, short const cLeft, short const cRight); TEST_METHOD(TestBoundaryMeasuresRegularString); TEST_METHOD(TestBoundaryMeasuresFloatingString); TEST_METHOD(TestCopyProperties); TEST_METHOD(TestInsertCharacter); TEST_METHOD(TestIncrementCursor); TEST_METHOD(TestNewlineCursor); void TestLastNonSpace(short const cursorPosY); TEST_METHOD(TestGetLastNonSpaceCharacter); TEST_METHOD(TestSetWrapOnCurrentRow); TEST_METHOD(TestIncrementCircularBuffer); TEST_METHOD(TestMixedRgbAndLegacyForeground); TEST_METHOD(TestMixedRgbAndLegacyBackground); TEST_METHOD(TestMixedRgbAndLegacyUnderline); TEST_METHOD(TestMixedRgbAndLegacyBrightness); TEST_METHOD(TestRgbEraseLine); TEST_METHOD(TestUnBold); TEST_METHOD(TestUnBoldRgb); TEST_METHOD(TestComplexUnBold); TEST_METHOD(CopyAttrs); TEST_METHOD(EmptySgrTest); TEST_METHOD(TestReverseReset); TEST_METHOD(CopyLastAttr); TEST_METHOD(TestRgbThenBold); TEST_METHOD(TestResetClearsBoldness); TEST_METHOD(TestBackspaceRightSideVt); TEST_METHOD(TestBackspaceStrings); TEST_METHOD(TestBackspaceStringsAPI); TEST_METHOD(TestRepeatCharacter); TEST_METHOD(ResizeTraditional); TEST_METHOD(ResizeTraditionalRotationPreservesHighUnicode); TEST_METHOD(ScrollBufferRotationPreservesHighUnicode); TEST_METHOD(ResizeTraditionalHighUnicodeRowRemoval); TEST_METHOD(ResizeTraditionalHighUnicodeColumnRemoval); TEST_METHOD(TestBurrito); void WriteLinesToBuffer(const std::vector& text, TextBuffer& buffer); TEST_METHOD(GetWordBoundaries); TEST_METHOD(MoveByWord); TEST_METHOD(GetGlyphBoundaries); TEST_METHOD(GetTextRects); TEST_METHOD(GetText); TEST_METHOD(HyperlinkTrim); TEST_METHOD(NoHyperlinkTrim); }; void TextBufferTests::TestBufferCreate() { VERIFY_SUCCEEDED(m_state->GetTextBufferInfoInitResult()); } TextBuffer& TextBufferTests::GetTbi() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return gci.GetActiveOutputBuffer().GetTextBuffer(); } SHORT TextBufferTests::GetBufferWidth() { return GetTbi().GetSize().Width(); } SHORT TextBufferTests::GetBufferHeight() { return GetTbi().GetSize().Height(); } void TextBufferTests::TestBufferRowByOffset() { TextBuffer& textBuffer = GetTbi(); SHORT csBufferHeight = GetBufferHeight(); VERIFY_IS_TRUE(csBufferHeight > 20); short sId = csBufferHeight / 2 - 5; const ROW& row = textBuffer.GetRowByOffset(sId); VERIFY_ARE_EQUAL(row.GetId(), sId); } void TextBufferTests::TestWrapFlag() { TextBuffer& textBuffer = GetTbi(); ROW& Row = textBuffer._GetFirstRow(); // no wrap by default VERIFY_IS_FALSE(Row.WasWrapForced()); // try set wrap and check Row.SetWrapForced(true); VERIFY_IS_TRUE(Row.WasWrapForced()); // try unset wrap and check Row.SetWrapForced(false); VERIFY_IS_FALSE(Row.WasWrapForced()); } void TextBufferTests::TestWrapThroughWriteLine() { TextBuffer& textBuffer = GetTbi(); auto VerifyWrap = [&](bool expected) { ROW& Row = textBuffer._GetFirstRow(); if (expected) { VERIFY_IS_TRUE(Row.WasWrapForced()); } else { VERIFY_IS_FALSE(Row.WasWrapForced()); } }; // Construct string for testing const auto width = textBuffer.GetSize().Width(); std::wstring chars = L""; for (auto i = 0; i < width; i++) { chars.append(L"a"); } const auto lineOfText = std::move(chars); Log::Comment(L"Case 1 : Implicit wrap (false)"); { TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(lineOfText, expectedAttr); textBuffer.WriteLine(it, { 0, 0 }); VerifyWrap(false); } Log::Comment(L"Case 2 : wrap = true"); { TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(lineOfText, expectedAttr); textBuffer.WriteLine(it, { 0, 0 }, true); VerifyWrap(true); } Log::Comment(L"Case 3: wrap = nullopt (remain as TRUE)"); { TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(lineOfText, expectedAttr); textBuffer.WriteLine(it, { 0, 0 }, std::nullopt); VerifyWrap(true); } Log::Comment(L"Case 4: wrap = false"); { TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(lineOfText, expectedAttr); textBuffer.WriteLine(it, { 0, 0 }, false); VerifyWrap(false); } Log::Comment(L"Case 5: wrap = nullopt (remain as false)"); { TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(lineOfText, expectedAttr); textBuffer.WriteLine(it, { 0, 0 }, std::nullopt); VerifyWrap(false); } } void TextBufferTests::TestDoubleBytePadFlag() { TextBuffer& textBuffer = GetTbi(); ROW& Row = textBuffer._GetFirstRow(); // no padding by default VERIFY_IS_FALSE(Row.WasDoubleBytePadded()); // try set and check Row.SetDoubleBytePadded(true); VERIFY_IS_TRUE(Row.WasDoubleBytePadded()); // try unset and check Row.SetDoubleBytePadded(false); VERIFY_IS_FALSE(Row.WasDoubleBytePadded()); } void TextBufferTests::DoBoundaryTest(PCWCHAR const pwszInputString, short const cLength, short const cMax, short const cLeft, short const cRight) { TextBuffer& textBuffer = GetTbi(); CharRow& charRow = textBuffer._GetFirstRow().GetCharRow(); // copy string into buffer for (size_t i = 0; i < static_cast(cLength); ++i) { charRow.GlyphAt(i) = { &pwszInputString[i], 1 }; } // space pad the rest of the string if (cLength < cMax) { for (short cStart = cLength; cStart < cMax; cStart++) { charRow.ClearGlyph(cStart); } } // left edge should be 0 since there are no leading spaces VERIFY_ARE_EQUAL(charRow.MeasureLeft(), static_cast(cLeft)); // right edge should be one past the index of the last character or the string length VERIFY_ARE_EQUAL(charRow.MeasureRight(), static_cast(cRight)); } void TextBufferTests::TestBoundaryMeasuresRegularString() { SHORT csBufferWidth = GetBufferWidth(); // length 44, left 0, right 44 const auto pwszLazyDog = L"The quick brown fox jumps over the lazy dog."; DoBoundaryTest(pwszLazyDog, 44, csBufferWidth, 0, 44); } void TextBufferTests::TestBoundaryMeasuresFloatingString() { SHORT csBufferWidth = GetBufferWidth(); // length 5 spaces + 4 chars + 5 spaces = 14, left 5, right 9 const auto pwszOffsets = L" C:\\> "; DoBoundaryTest(pwszOffsets, 14, csBufferWidth, 5, 9); } void TextBufferTests::TestCopyProperties() { TextBuffer& otherTbi = GetTbi(); std::unique_ptr testTextBuffer = std::make_unique(otherTbi.GetSize().Dimensions(), otherTbi._currentAttributes, 12, otherTbi._renderTarget); VERIFY_IS_NOT_NULL(testTextBuffer.get()); // set initial mapping values testTextBuffer->GetCursor().SetHasMoved(false); otherTbi.GetCursor().SetHasMoved(true); testTextBuffer->GetCursor().SetIsVisible(false); otherTbi.GetCursor().SetIsVisible(true); testTextBuffer->GetCursor().SetIsOn(false); otherTbi.GetCursor().SetIsOn(true); testTextBuffer->GetCursor().SetIsDouble(false); otherTbi.GetCursor().SetIsDouble(true); testTextBuffer->GetCursor().SetDelay(false); otherTbi.GetCursor().SetDelay(true); // run copy testTextBuffer->CopyProperties(otherTbi); // test that new now contains values from other VERIFY_IS_TRUE(testTextBuffer->GetCursor().HasMoved()); VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsVisible()); VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsDouble()); VERIFY_IS_TRUE(testTextBuffer->GetCursor().GetDelay()); } void TextBufferTests::TestInsertCharacter() { TextBuffer& textBuffer = GetTbi(); // get starting cursor position COORD const coordCursorBefore = textBuffer.GetCursor().GetPosition(); // Get current row from the buffer ROW& Row = textBuffer.GetRowByOffset(coordCursorBefore.Y); // create some sample test data const auto wch = L'Z'; const std::wstring_view wchTest(&wch, 1); DbcsAttribute dbcsAttribute; dbcsAttribute.SetTrailing(); WORD const wAttrTest = BACKGROUND_INTENSITY | FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE; TextAttribute TestAttributes = TextAttribute(wAttrTest); CharRow& charRow = Row.GetCharRow(); charRow.DbcsAttrAt(coordCursorBefore.X).SetLeading(); // ensure that the buffer didn't start with these fields VERIFY_ARE_NOT_EQUAL(charRow.GlyphAt(coordCursorBefore.X), wchTest); VERIFY_ARE_NOT_EQUAL(charRow.DbcsAttrAt(coordCursorBefore.X), dbcsAttribute); auto attr = Row.GetAttrRow().GetAttrByColumn(coordCursorBefore.X); VERIFY_ARE_NOT_EQUAL(attr, TestAttributes); // now apply the new data to the buffer textBuffer.InsertCharacter(wchTest, dbcsAttribute, TestAttributes); // ensure that the buffer position where the cursor WAS contains the test items VERIFY_ARE_EQUAL(charRow.GlyphAt(coordCursorBefore.X), wchTest); VERIFY_ARE_EQUAL(charRow.DbcsAttrAt(coordCursorBefore.X), dbcsAttribute); attr = Row.GetAttrRow().GetAttrByColumn(coordCursorBefore.X); VERIFY_ARE_EQUAL(attr, TestAttributes); // ensure that the cursor moved to a new position (X or Y or both have changed) VERIFY_IS_TRUE((coordCursorBefore.X != textBuffer.GetCursor().GetPosition().X) || (coordCursorBefore.Y != textBuffer.GetCursor().GetPosition().Y)); // the proper advancement of the cursor (e.g. which position it goes to) is validated in other tests } void TextBufferTests::TestIncrementCursor() { TextBuffer& textBuffer = GetTbi(); // only checking X increments here // Y increments are covered in the NewlineCursor test short const sBufferWidth = textBuffer.GetSize().Width(); short const sBufferHeight = textBuffer.GetSize().Height(); VERIFY_IS_TRUE(sBufferWidth > 1 && sBufferHeight > 1); Log::Comment(L"Test normal case of moving once to the right within a single line"); textBuffer.GetCursor().SetXPosition(0); textBuffer.GetCursor().SetYPosition(0); COORD coordCursorBefore = textBuffer.GetCursor().GetPosition(); textBuffer.IncrementCursor(); VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 1); // X should advance by 1 VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y); // Y shouldn't have moved Log::Comment(L"Test line wrap case where cursor is on the right edge of the line"); textBuffer.GetCursor().SetXPosition(sBufferWidth - 1); textBuffer.GetCursor().SetYPosition(0); coordCursorBefore = textBuffer.GetCursor().GetPosition(); textBuffer.IncrementCursor(); VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // position should be reset to the left edge when passing right edge VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y - 1, coordCursorBefore.Y); // the cursor should be moved one row down from where it used to be } void TextBufferTests::TestNewlineCursor() { TextBuffer& textBuffer = GetTbi(); const short sBufferHeight = textBuffer.GetSize().Height(); const short sBufferWidth = textBuffer.GetSize().Width(); // width and height are sufficiently large for upcoming math VERIFY_IS_TRUE(sBufferWidth > 4 && sBufferHeight > 4); Log::Comment(L"Verify standard row increment from somewhere in the buffer"); // set cursor X position to non zero, any position in buffer textBuffer.GetCursor().SetXPosition(3); // set cursor Y position to not-the-final row in the buffer textBuffer.GetCursor().SetYPosition(3); COORD coordCursorBefore = textBuffer.GetCursor().GetPosition(); // perform operation textBuffer.NewlineCursor(); // verify VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // move to left edge of buffer VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y + 1); // move down one row Log::Comment(L"Verify increment when already on last row of buffer"); // X position still doesn't matter textBuffer.GetCursor().SetXPosition(3); // Y position needs to be on the last row of the buffer textBuffer.GetCursor().SetYPosition(sBufferHeight - 1); coordCursorBefore = textBuffer.GetCursor().GetPosition(); // perform operation textBuffer.NewlineCursor(); // verify VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // move to left edge VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y); // cursor Y position should not have moved. stays on same logical final line of buffer // This is okay because the backing circular buffer changes, not the logical screen position (final visible line of the buffer) } void TextBufferTests::TestLastNonSpace(short const cursorPosY) { TextBuffer& textBuffer = GetTbi(); textBuffer.GetCursor().SetYPosition(cursorPosY); COORD coordLastNonSpace = textBuffer.GetLastNonSpaceCharacter(); // We expect the last non space character to be the last printable character in the row. // The .Right property on a row is 1 past the last printable character in the row. // If there is one character in the row, the last character would be 0. // If there are no characters in the row, the last character would be -1 and we need to seek backwards to find the previous row with a character. // start expected position from cursor COORD coordExpected = textBuffer.GetCursor().GetPosition(); // Try to get the X position from the current cursor position. coordExpected.X = static_cast(textBuffer.GetRowByOffset(coordExpected.Y).GetCharRow().MeasureRight()) - 1; // If we went negative, this row was empty and we need to continue seeking upward... // - As long as X is negative (empty rows) // - As long as we have space before the top of the buffer (Y isn't the 0th/top row). while (coordExpected.X < 0 && coordExpected.Y > 0) { coordExpected.Y--; coordExpected.X = static_cast(textBuffer.GetRowByOffset(coordExpected.Y).GetCharRow().MeasureRight()) - 1; } VERIFY_ARE_EQUAL(coordLastNonSpace.X, coordExpected.X); VERIFY_ARE_EQUAL(coordLastNonSpace.Y, coordExpected.Y); } void TextBufferTests::TestGetLastNonSpaceCharacter() { m_state->FillTextBuffer(); // fill buffer with some text, it should be 4 rows. See CommonState for details Log::Comment(L"Test with cursor inside last row of text"); TestLastNonSpace(3); Log::Comment(L"Test with cursor one beyond last row of text"); TestLastNonSpace(4); Log::Comment(L"Test with cursor way beyond last row of text"); TestLastNonSpace(14); } void TextBufferTests::TestSetWrapOnCurrentRow() { TextBuffer& textBuffer = GetTbi(); short sCurrentRow = textBuffer.GetCursor().GetPosition().Y; ROW& Row = textBuffer.GetRowByOffset(sCurrentRow); Log::Comment(L"Testing off to on"); // turn wrap status off first Row.SetWrapForced(false); // trigger wrap textBuffer._SetWrapOnCurrentRow(); // ensure this row was flipped VERIFY_IS_TRUE(Row.WasWrapForced()); Log::Comment(L"Testing on stays on"); // make sure wrap status is on Row.SetWrapForced(true); // trigger wrap textBuffer._SetWrapOnCurrentRow(); // ensure row is still on VERIFY_IS_TRUE(Row.WasWrapForced()); } void TextBufferTests::TestIncrementCircularBuffer() { TextBuffer& textBuffer = GetTbi(); short const sBufferHeight = textBuffer.GetSize().Height(); VERIFY_IS_TRUE(sBufferHeight > 4); // buffer should be sufficiently large Log::Comment(L"Test 1 = FirstRow of circular buffer is not the final row of the buffer"); Log::Comment(L"Test 2 = FirstRow of circular buffer IS THE FINAL ROW of the buffer (and therefore circles)"); short rgRowsToTest[] = { 2, sBufferHeight - 1 }; for (UINT iTestIndex = 0; iTestIndex < ARRAYSIZE(rgRowsToTest); iTestIndex++) { const short iRowToTestIndex = rgRowsToTest[iTestIndex]; short iNextRowIndex = iRowToTestIndex + 1; // if we're at or crossing the height, loop back to 0 (circular buffer) if (iNextRowIndex >= sBufferHeight) { iNextRowIndex = 0; } textBuffer._firstRow = iRowToTestIndex; // fill first row with some stuff ROW& FirstRow = textBuffer._GetFirstRow(); CharRow& charRow = FirstRow.GetCharRow(); const auto stuff = L'A'; charRow.GlyphAt(0) = { &stuff, 1 }; // ensure it does say that it contains text VERIFY_IS_TRUE(FirstRow.GetCharRow().ContainsText()); // try increment textBuffer.IncrementCircularBuffer(); // validate that first row has moved VERIFY_ARE_EQUAL(textBuffer._firstRow, iNextRowIndex); // first row has incremented VERIFY_ARE_NOT_EQUAL(textBuffer._GetFirstRow(), FirstRow); // the old first row is no longer the first // ensure old first row has been emptied VERIFY_IS_FALSE(FirstRow.GetCharRow().ContainsText()); } } void TextBufferTests::TestMixedRgbAndLegacyForeground() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); // Case 1 - // Write '\E[m\E[38;2;64;128;255mX\E[49mX\E[m' // Make sure that the second X has RGB attributes (FG and BG) // FG = rgb(64;128;255), BG = rgb(default) Log::Comment(L"Case 1 \"\\E[m\\E[38;2;64;128;255mX\\E[49mX\\E[m\""); const auto sequence = L"\x1b[m\x1b[38;2;64;128;255mX\x1b[49mX\x1b[m"; stateMachine.ProcessString(sequence); const short x = cursor.GetPosition().X; const short y = cursor.GetPosition().Y; const ROW& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); const auto fgColor = RGB(64, 128, 255); const auto bgColor = gci.LookupAttributeColors(si.GetAttributes()).second; VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(fgColor, bgColor)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(fgColor, bgColor)); const auto reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestMixedRgbAndLegacyBackground() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); // Case 2 - // \E[m\E[48;2;64;128;255mX\E[39mX\E[m // Make sure that the second X has RGB attributes (FG and BG) // FG = rgb(default), BG = rgb(64;128;255) Log::Comment(L"Case 2 \"\\E[m\\E[48;2;64;128;255mX\\E[39mX\\E[m\""); const auto sequence = L"\x1b[m\x1b[48;2;64;128;255mX\x1b[39mX\x1b[m"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); const auto bgColor = RGB(64, 128, 255); const auto fgColor = gci.LookupAttributeColors(si.GetAttributes()).first; VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(fgColor, bgColor)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(fgColor, bgColor)); const auto reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestMixedRgbAndLegacyUnderline() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); // Case 3 - // '\E[m\E[48;2;64;128;255mX\E[4mX\E[m' // Make sure that the second X has RGB attributes AND underline Log::Comment(L"Case 3 \"\\E[m\\E[48;2;64;128;255mX\\E[4mX\\E[m\""); const auto sequence = L"\x1b[m\x1b[48;2;64;128;255mX\x1b[4mX\x1b[m"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); const auto bgColor = RGB(64, 128, 255); const auto fgColor = gci.LookupAttributeColors(si.GetAttributes()).first; VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(fgColor, bgColor)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(fgColor, bgColor)); VERIFY_ARE_EQUAL(attrA.IsUnderlined(), false); VERIFY_ARE_EQUAL(attrB.IsUnderlined(), true); const auto reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestMixedRgbAndLegacyBrightness() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); // Case 4 - // '\E[m\E[32mX\E[1mX' // Make sure that the second X is a BRIGHT green, not white. Log::Comment(L"Case 4 ;\"\\E[m\\E[32mX\\E[1mX\""); const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); VERIFY_ARE_NOT_EQUAL(dark_green, bright_green); const auto sequence = L"\x1b[m\x1b[32mX\x1b[1mX"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA).first, dark_green); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB).first, bright_green); const auto reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestRgbEraseLine() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); // Case 1 - // Write '\E[m\E[48;2;64;128;255X\E[48;2;128;128;255\E[KX' // Make sure that all the characters after the first have the rgb attrs // BG = rgb(128;128;255) { std::wstring sequence = L"\x1b[m\x1b[48;2;64;128;255m"; stateMachine.ProcessString(sequence); sequence = L"X"; stateMachine.ProcessString(sequence); sequence = L"\x1b[48;2;128;128;255m"; stateMachine.ProcessString(sequence); sequence = L"\x1b[K"; stateMachine.ProcessString(sequence); sequence = L"X"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_ARE_EQUAL(x, 2); VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const auto len = tbi.GetSize().Width(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attr0 = attrs[0]; VERIFY_ARE_EQUAL(attr0.IsLegacy(), false); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr0).second, RGB(64, 128, 255)); for (auto i = 1; i < len; i++) { const auto attr = attrs[i]; LOG_ATTR(attr); VERIFY_ARE_EQUAL(attr.IsLegacy(), false); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr).second, RGB(128, 128, 255)); } std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } } void TextBufferTests::TestUnBold() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); // Case 1 - // Write '\E[1;32mX\E[22mX' // The first X should be bright green. // The second x should be dark green. std::wstring sequence = L"\x1b[1;32mX\x1b[22mX"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_ARE_EQUAL(x, 2); VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA).first, bright_green); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB).first, dark_green); std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestUnBoldRgb() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); // Case 2 - // Write '\E[1;32m\E[48;2;1;2;3mX\E[22mX' // The first X should be bright green, and not legacy. // The second X should be dark green, and not legacy. // BG = rgb(1;2;3) std::wstring sequence = L"\x1b[1;32m\x1b[48;2;1;2;3mX\x1b[22mX"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_ARE_EQUAL(x, 2); VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA).first, bright_green); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB).first, dark_green); std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestComplexUnBold() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); // Case 3 - // Write '\E[1;32m\E[48;2;1;2;3mA\E[22mB\E[38;2;32;32;32mC\E[1mD\E[38;2;64;64;64mE\E[22mF' // The A should be bright green, and not legacy. // The B should be dark green, and not legacy. // The C should be rgb(32, 32, 32), and not legacy. // The D should be unchanged from the third. // The E should be rgb(64, 64, 64), and not legacy. // The F should be rgb(64, 64, 64), and not legacy. // BG = rgb(1;2;3) std::wstring sequence = L"\x1b[1;32m\x1b[48;2;1;2;3mA\x1b[22mB\x1b[38;2;32;32;32mC\x1b[1mD\x1b[38;2;64;64;64mE\x1b[22mF"; Log::Comment(NoThrowString().Format(sequence.c_str())); stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_ARE_EQUAL(x, 6); VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 6]; const auto attrB = attrs[x - 5]; const auto attrC = attrs[x - 4]; const auto attrD = attrs[x - 3]; const auto attrE = attrs[x - 2]; const auto attrF = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); Log::Comment(NoThrowString().Format( L"attrA=%s", VerifyOutputTraits::ToString(attrA).GetBuffer())); LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); LOG_ATTR(attrD); LOG_ATTR(attrE); LOG_ATTR(attrF); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); VERIFY_ARE_EQUAL(attrC.IsLegacy(), false); VERIFY_ARE_EQUAL(attrD.IsLegacy(), false); VERIFY_ARE_EQUAL(attrE.IsLegacy(), false); VERIFY_ARE_EQUAL(attrF.IsLegacy(), false); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(bright_green, RGB(1, 2, 3))); VERIFY_IS_TRUE(attrA.IsBold()); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(dark_green, RGB(1, 2, 3))); VERIFY_IS_FALSE(attrB.IsBold()); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrC), std::make_pair(RGB(32, 32, 32), RGB(1, 2, 3))); VERIFY_IS_FALSE(attrC.IsBold()); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrD), gci.LookupAttributeColors(attrC)); VERIFY_IS_TRUE(attrD.IsBold()); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrE), std::make_pair(RGB(64, 64, 64), RGB(1, 2, 3))); VERIFY_IS_TRUE(attrE.IsBold()); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrF), std::make_pair(RGB(64, 64, 64), RGB(1, 2, 3))); VERIFY_IS_FALSE(attrF.IsBold()); std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::CopyAttrs() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); // Write '\E[32mX\E[33mX\n\E[34mX\E[35mX\E[H\E[M' // The first two X's should get deleted. // The third X should be blue // The fourth X should be magenta std::wstring sequence = L"\x1b[32mX\x1b[33mX\n\x1b[34mX\x1b[35mX\x1b[H\x1b[M"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto dark_blue = gci.GetColorTableEntry(TextColor::DARK_BLUE); const auto dark_magenta = gci.GetColorTableEntry(TextColor::DARK_MAGENTA); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_ARE_EQUAL(x, 0); VERIFY_ARE_EQUAL(y, 0); const auto& row = tbi.GetRowByOffset(0); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[0]; const auto attrB = attrs[1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA).first, dark_blue); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB).first, dark_magenta); } void TextBufferTests::EmptySgrTest() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); const auto [defaultFg, defaultBg] = gci.LookupAttributeColors(si.GetAttributes()); // Case 1 - // Write '\x1b[0mX\x1b[31mX\x1b[31;m' // The first X should be default colors. // The second X should be (darkRed,default). // The third X should be default colors. std::wstring sequence = L"\x1b[0mX\x1b[31mX\x1b[31;mX"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const COLORREF darkRed = gci.GetColorTableEntry(TextColor::DARK_RED); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_IS_TRUE(x >= 3); const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 3]; const auto attrB = attrs[x - 2]; const auto attrC = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(defaultFg, defaultBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(darkRed, defaultBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrC), std::make_pair(defaultFg, defaultBg)); stateMachine.ProcessString(reset); } void TextBufferTests::TestReverseReset() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); const auto [defaultFg, defaultBg] = gci.LookupAttributeColors(si.GetAttributes()); // Case 1 - // Write '\E[42m\E[38;2;128;5;255mX\E[7mX\E[27mX' // The first X should be (fg,bg) = (rgb(128;5;255), dark_green) // The second X should be (fg,bg) = (dark_green, rgb(128;5;255)) // The third X should be (fg,bg) = (rgb(128;5;255), dark_green) std::wstring sequence = L"\x1b[42m\x1b[38;2;128;5;255mX\x1b[7mX\x1b[27mX"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const COLORREF rgbColor = RGB(128, 5, 255); Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); VERIFY_IS_TRUE(x >= 3); const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 3]; const auto attrB = attrs[x - 2]; const auto attrC = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(rgbColor, dark_green)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(dark_green, rgbColor)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrC), std::make_pair(rgbColor, dark_green)); stateMachine.ProcessString(reset); } void TextBufferTests::CopyLastAttr() { DisableVerifyExceptions disable; CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); std::wstring reset = L"\x1b[0m"; stateMachine.ProcessString(reset); const auto [defaultFg, defaultBg] = gci.LookupAttributeColors(si.GetAttributes()); const COLORREF solFg = RGB(101, 123, 131); const COLORREF solBg = RGB(0, 43, 54); const COLORREF solCyan = RGB(42, 161, 152); std::wstring solFgSeq = L"\x1b[38;2;101;123;131m"; std::wstring solBgSeq = L"\x1b[48;2;0;43;54m"; std::wstring solCyanSeq = L"\x1b[38;2;42;161;152m"; // Make sure that the color table has certain values we expect const COLORREF defaultBrightBlack = RGB(118, 118, 118); const COLORREF defaultBrightYellow = RGB(249, 241, 165); const COLORREF defaultBrightCyan = RGB(97, 214, 214); gci.SetColorTableEntry(TextColor::BRIGHT_BLACK, defaultBrightBlack); gci.SetColorTableEntry(TextColor::BRIGHT_YELLOW, defaultBrightYellow); gci.SetColorTableEntry(TextColor::BRIGHT_CYAN, defaultBrightCyan); // Write (solFg, solBG) X \n // (solFg, solBG) X (solCyan, solBG) X \n // (solFg, solBG) X (solCyan, solBG) X (solFg, solBG) X // then go home, and insert a line. // Row 1 stateMachine.ProcessString(solFgSeq); stateMachine.ProcessString(solBgSeq); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\n"); // Row 2 // Remember that the colors from before persist here too, so we don't need // to emit both the FG and BG if they haven't changed. stateMachine.ProcessString(L"X"); stateMachine.ProcessString(solCyanSeq); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(L"\n"); // Row 3 stateMachine.ProcessString(solFgSeq); stateMachine.ProcessString(solBgSeq); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(solCyanSeq); stateMachine.ProcessString(L"X"); stateMachine.ProcessString(solFgSeq); stateMachine.ProcessString(L"X"); std::wstring insertLineAtHome = L"\x1b[H\x1b[L"; stateMachine.ProcessString(insertLineAtHome); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); const ROW& row1 = tbi.GetRowByOffset(y + 1); const ROW& row2 = tbi.GetRowByOffset(y + 2); const ROW& row3 = tbi.GetRowByOffset(y + 3); const std::vector attrs1{ row1.GetAttrRow().begin(), row1.GetAttrRow().end() }; const std::vector attrs2{ row2.GetAttrRow().begin(), row2.GetAttrRow().end() }; const std::vector attrs3{ row3.GetAttrRow().begin(), row3.GetAttrRow().end() }; const auto attr1A = attrs1[0]; const auto attr2A = attrs2[0]; const auto attr2B = attrs2[1]; const auto attr3A = attrs3[0]; const auto attr3B = attrs3[1]; const auto attr3C = attrs3[2]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); LOG_ATTR(attr1A); LOG_ATTR(attr2A); LOG_ATTR(attr2A); LOG_ATTR(attr3A); LOG_ATTR(attr3B); LOG_ATTR(attr3C); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr1A), std::make_pair(solFg, solBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr2A), std::make_pair(solFg, solBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr2B), std::make_pair(solCyan, solBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr3A), std::make_pair(solFg, solBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr3B), std::make_pair(solCyan, solBg)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attr3C), std::make_pair(solFg, solBg)); stateMachine.ProcessString(reset); } void TextBufferTests::TestRgbThenBold() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); // See MSFT:16398982 Log::Comment(NoThrowString().Format( L"Test that a bold following a RGB color doesn't remove the RGB color")); Log::Comment(L"\"\\x1b[38;2;40;40;40m\\x1b[48;2;168;153;132mX\\x1b[1mX\\x1b[m\""); const auto foreground = RGB(40, 40, 40); const auto background = RGB(168, 153, 132); const wchar_t* const sequence = L"\x1b[38;2;40;40;40m\x1b[48;2;168;153;132mX\x1b[1mX\x1b[m"; stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x - 2]; const auto attrB = attrs[x - 1]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); Log::Comment(NoThrowString().Format( L"attrA should be RGB, and attrB should be the same as attrA, NOT bolded")); LOG_ATTR(attrA); LOG_ATTR(attrB); VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA), std::make_pair(foreground, background)); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB), std::make_pair(foreground, background)); const auto reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestResetClearsBoldness() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); Log::Comment(NoThrowString().Format( L"Test that resetting bold attributes clears the boldness.")); const auto x0 = cursor.GetPosition().X; // Test assumes that the background/foreground were default attribute when it starts up, // so set that here. TextAttribute defaultAttribute; si.SetAttributes(defaultAttribute); const auto [defaultFg, defaultBg] = gci.LookupAttributeColors(si.GetAttributes()); const auto dark_green = gci.GetColorTableEntry(TextColor::DARK_GREEN); const auto bright_green = gci.GetColorTableEntry(TextColor::BRIGHT_GREEN); const auto sequence = L"\x1b[32mA\x1b[1mB\x1b[0mC\x1b[32mD"; Log::Comment(NoThrowString().Format(sequence)); stateMachine.ProcessString(sequence); const auto x = cursor.GetPosition().X; const auto y = cursor.GetPosition().Y; const auto& row = tbi.GetRowByOffset(y); const auto attrRow = &row.GetAttrRow(); const std::vector attrs{ attrRow->begin(), attrRow->end() }; const auto attrA = attrs[x0]; const auto attrB = attrs[x0 + 1]; const auto attrC = attrs[x0 + 2]; const auto attrD = attrs[x0 + 3]; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x, y)); Log::Comment(NoThrowString().Format( L"attrA should be RGB, and attrB should be the same as attrA, NOT bolded")); LOG_ATTR(attrA); LOG_ATTR(attrB); LOG_ATTR(attrC); LOG_ATTR(attrD); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrA).first, dark_green); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrB).first, bright_green); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrC).first, defaultFg); VERIFY_ARE_EQUAL(gci.LookupAttributeColors(attrD).first, dark_green); VERIFY_IS_FALSE(attrA.IsBold()); VERIFY_IS_TRUE(attrB.IsBold()); VERIFY_IS_FALSE(attrC.IsBold()); VERIFY_IS_FALSE(attrD.IsBold()); const auto reset = L"\x1b[0m"; stateMachine.ProcessString(reset); } void TextBufferTests::TestBackspaceRightSideVt() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); Log::Comment(L"verify that backspace has the same behavior as a vt CUB sequence once " L"we've traversed to the right side of the current row"); const wchar_t* const sequence = L"\033[1000Cx\by\n"; Log::Comment(NoThrowString().Format(sequence)); const auto preCursorPosition = cursor.GetPosition(); stateMachine.ProcessString(sequence); const auto postCursorPosition = cursor.GetPosition(); // make sure newline was handled correctly VERIFY_ARE_EQUAL(0, postCursorPosition.X); VERIFY_ARE_EQUAL(preCursorPosition.Y, postCursorPosition.Y - 1); // make sure "yx" was written to the end of the line the cursor started on const auto& row = tbi.GetRowByOffset(preCursorPosition.Y); const auto rowText = row.GetText(); auto it = rowText.crbegin(); VERIFY_ARE_EQUAL(*it, L'x'); ++it; VERIFY_ARE_EQUAL(*it, L'y'); } void TextBufferTests::TestBackspaceStrings() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); const Cursor& cursor = tbi.GetCursor(); const auto x0 = cursor.GetPosition().X; const auto y0 = cursor.GetPosition().Y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x0, y0)); std::wstring seq = L"a\b \b"; stateMachine.ProcessString(seq); const auto x1 = cursor.GetPosition().X; const auto y1 = cursor.GetPosition().Y; VERIFY_ARE_EQUAL(x1, x0); VERIFY_ARE_EQUAL(y1, y0); seq = L"a"; stateMachine.ProcessString(seq); seq = L"\b"; stateMachine.ProcessString(seq); seq = L" "; stateMachine.ProcessString(seq); seq = L"\b"; stateMachine.ProcessString(seq); const auto x2 = cursor.GetPosition().X; const auto y2 = cursor.GetPosition().Y; VERIFY_ARE_EQUAL(x2, x0); VERIFY_ARE_EQUAL(y2, y0); } void TextBufferTests::TestBackspaceStringsAPI() { // Pretty much the same as the above test, but explicitly DOESN'T use the // state machine. CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); const TextBuffer& tbi = si.GetTextBuffer(); const Cursor& cursor = tbi.GetCursor(); gci.SetVirtTermLevel(0); WI_ClearFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto x0 = cursor.GetPosition().X; const auto y0 = cursor.GetPosition().Y; Log::Comment(NoThrowString().Format( L"cursor={X:%d,Y:%d}", x0, y0)); // We're going to write an "a" to the buffer in various ways, then try // backspacing it with "\b \b". // Regardless of how we write those sequences of characters, the end result // should be the same. std::unique_ptr waiter; size_t aCb = 2; VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); size_t seqCb = 6; Log::Comment(NoThrowString().Format( L"Using WriteCharsLegacy, write \\b \\b as a single string.")); { const auto str = L"\b \b"; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); Log::Comment(NoThrowString().Format( L"Using DoWriteConsole, write \\b \\b as a single string.")); VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(str, &seqCb, si, false, waiter)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); } seqCb = 2; Log::Comment(NoThrowString().Format( L"Using DoWriteConsole, write \\b \\b as separate strings.")); VERIFY_SUCCEEDED(DoWriteConsole(L"a", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L" ", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); Log::Comment(NoThrowString().Format( L"Using WriteCharsLegacy, write \\b \\b as separate strings.")); { const auto str = L"a"; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); } { const auto str = L"\b"; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); } { const auto str = L" "; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); } { const auto str = L"\b"; VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); } VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); } void TextBufferTests::TestRepeatCharacter() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); TextBuffer& tbi = si.GetTextBuffer(); StateMachine& stateMachine = si.GetStateMachine(); Cursor& cursor = tbi.GetCursor(); WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); cursor.SetXPosition(0); cursor.SetYPosition(0); Log::Comment( L"Test 0: Simply repeat a single character."); std::wstring sequence = L"X"; stateMachine.ProcessString(sequence); sequence = L"\x1b[b"; stateMachine.ProcessString(sequence); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); { const auto& row0 = tbi.GetRowByOffset(0); const auto row0Text = row0.GetText(); VERIFY_ARE_EQUAL(L'X', row0Text[0]); VERIFY_ARE_EQUAL(L'X', row0Text[1]); VERIFY_ARE_EQUAL(L' ', row0Text[2]); } Log::Comment( L"Test 1: Try repeating characters after another VT action. It should do nothing."); stateMachine.ProcessString(L"\n"); stateMachine.ProcessString(L"A"); stateMachine.ProcessString(L"B"); stateMachine.ProcessString(L"\x1b[A"); stateMachine.ProcessString(L"\x1b[b"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); { const auto& row0 = tbi.GetRowByOffset(0); const auto& row1 = tbi.GetRowByOffset(1); const auto row0Text = row0.GetText(); const auto row1Text = row1.GetText(); VERIFY_ARE_EQUAL(L'X', row0Text[0]); VERIFY_ARE_EQUAL(L'X', row0Text[1]); VERIFY_ARE_EQUAL(L' ', row0Text[2]); VERIFY_ARE_EQUAL(L'A', row1Text[0]); VERIFY_ARE_EQUAL(L'B', row1Text[1]); VERIFY_ARE_EQUAL(L' ', row1Text[2]); } Log::Comment( L"Test 2: Repeat a character lots of times"); stateMachine.ProcessString(L"\x1b[3;H"); stateMachine.ProcessString(L"C"); stateMachine.ProcessString(L"\x1b[5b"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 6); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 2); { const auto& row2 = tbi.GetRowByOffset(2); const auto row2Text = row2.GetText(); VERIFY_ARE_EQUAL(L'C', row2Text[0]); VERIFY_ARE_EQUAL(L'C', row2Text[1]); VERIFY_ARE_EQUAL(L'C', row2Text[2]); VERIFY_ARE_EQUAL(L'C', row2Text[3]); VERIFY_ARE_EQUAL(L'C', row2Text[4]); VERIFY_ARE_EQUAL(L'C', row2Text[5]); VERIFY_ARE_EQUAL(L' ', row2Text[6]); } Log::Comment( L"Test 3: try repeating a non-graphical character. It should do nothing."); stateMachine.ProcessString(L"\r\n"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 3); stateMachine.ProcessString(L"D\n"); stateMachine.ProcessString(L"\x1b[b"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 4); Log::Comment( L"Test 4: try repeating multiple times. It should do nothing."); stateMachine.ProcessString(L"\r\n"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); stateMachine.ProcessString(L"E"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 1); stateMachine.ProcessString(L"\x1b[b"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); stateMachine.ProcessString(L"\x1b[b"); VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); { const auto& row5 = tbi.GetRowByOffset(5); const auto row5Text = row5.GetText(); VERIFY_ARE_EQUAL(L'E', row5Text[0]); VERIFY_ARE_EQUAL(L'E', row5Text[1]); VERIFY_ARE_EQUAL(L' ', row5Text[2]); } } void TextBufferTests::ResizeTraditional() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:shrinkX", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:shrinkY", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool shrinkX; VERIFY_SUCCEEDED(TestData::TryGetValue(L"shrinkX", shrinkX), L"Shrink X = true, Grow X = false"); bool shrinkY; VERIFY_SUCCEEDED(TestData::TryGetValue(L"shrinkY", shrinkY), L"Shrink Y = true, Grow Y = false"); const COORD smallSize = { 5, 5 }; const TextAttribute defaultAttr(0); TextBuffer buffer(smallSize, defaultAttr, 12, _renderTarget); Log::Comment(L"Fill buffer with some data and do assorted resize operations."); wchar_t expectedChar = L'A'; const std::wstring_view expectedView(&expectedChar, 1); TextAttribute expectedAttr(FOREGROUND_RED); OutputCellIterator it(expectedChar, expectedAttr); const auto finalIt = buffer.Write(it); VERIFY_ARE_EQUAL(smallSize.X * smallSize.Y, finalIt.GetCellDistance(it), L"Verify we said we filled every cell."); const Viewport writtenView = Viewport::FromDimensions({ 0, 0 }, smallSize); Log::Comment(L"Ensure every cell has our test pattern value."); { TextBufferCellIterator viewIt(buffer, { 0, 0 }); while (viewIt) { VERIFY_ARE_EQUAL(expectedView, viewIt->Chars()); VERIFY_ARE_EQUAL(expectedAttr, viewIt->TextAttr()); viewIt++; } } Log::Comment(L"Resize to X and Y."); COORD newSize = smallSize; if (shrinkX) { newSize.X -= 2; } else { newSize.X += 2; } if (shrinkY) { newSize.Y -= 2; } else { newSize.Y += 2; } // When we grow, we extend the last color. Therefore, this region covers the area colored the same as the letters but filled with a blank. const auto widthAdjustedView = Viewport::FromDimensions(writtenView.Origin(), { newSize.X, smallSize.Y }); // When we resize, we expect the attributes to be unchanged, but the new cells // to be filled with spaces wchar_t expectedSpace = UNICODE_SPACE; std::wstring_view expectedSpaceView(&expectedSpace, 1); VERIFY_SUCCEEDED(buffer.ResizeTraditional(newSize)); Log::Comment(L"Verify every cell in the X dimension is still the same as when filled and the new Y row is just empty default cells."); { TextBufferCellIterator viewIt(buffer, { 0, 0 }); while (viewIt) { Log::Comment(NoThrowString().Format(L"Checking cell (Y=%d, X=%d)", viewIt._pos.Y, viewIt._pos.X)); if (writtenView.IsInBounds(viewIt._pos)) { Log::Comment(L"This position is inside our original write area. It should have the original character and color."); // If the position is in bounds with what we originally wrote, it should have that character and color. VERIFY_ARE_EQUAL(expectedView, viewIt->Chars()); VERIFY_ARE_EQUAL(expectedAttr, viewIt->TextAttr()); } else if (widthAdjustedView.IsInBounds(viewIt._pos)) { Log::Comment(L"This position is right of our original write area. It should have extended the color rightward and filled with a space."); // If we missed the original fill, but we're still in region defined by the adjusted width, then // the color was extended outward but without the character value. VERIFY_ARE_EQUAL(expectedSpaceView, viewIt->Chars()); VERIFY_ARE_EQUAL(expectedAttr, viewIt->TextAttr()); } else { Log::Comment(L"This position is below our original write area. It should have filled blank lines (space lines) with the default fill color."); // Otherwise, we use the default. VERIFY_ARE_EQUAL(expectedSpaceView, viewIt->Chars()); VERIFY_ARE_EQUAL(defaultAttr, viewIt->TextAttr()); } viewIt++; } } } // This tests that when buffer storage rows are rotated around during a resize traditional operation, // that the Unicode Storage-held high unicode items like emoji rotate properly with it. void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() { // Set up a text buffer for us const COORD bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Get a position inside the buffer const COORD pos{ 2, 1 }; auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); // Fill it up with a sequence that will have to hit the high unicode storage. // This is the negative squared latin capital letter B emoji: 🅱 // It's encoded in UTF-16, as needed by the buffer. const auto bButton = L"\xD83C\xDD71"; position = bButton; // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); const auto readBackText = *readBack; VERIFY_ARE_EQUAL(String(bButton), String(readBackText.data(), gsl::narrow(readBackText.size()))); // Make it the first row in the buffer so it will rotate around when we resize and cause renumbering const SHORT delta = _buffer->GetFirstRowIndex() - pos.Y; const COORD newPos{ pos.X, pos.Y + delta }; _buffer->_SetFirstRowIndex(pos.Y); // Perform resize to rotate the rows around VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(bufferSize)); // Retrieve the text at the old and new positions. const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); const auto shouldBeEmojiText = *_buffer->GetTextDataAt(newPos); VERIFY_ARE_EQUAL(String(L" "), String(shouldBeEmptyText.data(), gsl::narrow(shouldBeEmptyText.size()))); VERIFY_ARE_EQUAL(String(bButton), String(shouldBeEmojiText.data(), gsl::narrow(shouldBeEmojiText.size()))); } // This tests that when buffer storage rows are rotated around during a scroll buffer operation, // that the Unicode Storage-held high unicode items like emoji rotate properly with it. void TextBufferTests::ScrollBufferRotationPreservesHighUnicode() { // Set up a text buffer for us const COORD bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Get a position inside the buffer const COORD pos{ 2, 1 }; auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); // Fill it up with a sequence that will have to hit the high unicode storage. // This is the fire emoji: 🔥 // It's encoded in UTF-16, as needed by the buffer. const auto fire = L"\xD83D\xDD25"; position = fire; // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); const auto readBackText = *readBack; VERIFY_ARE_EQUAL(String(fire), String(readBackText.data(), gsl::narrow(readBackText.size()))); // Prepare a delta and the new position we expect the symbol to be moved into. const SHORT delta = 5; const COORD newPos{ pos.X, pos.Y + delta }; // Scroll the row with our data by delta. _buffer->ScrollRows(pos.Y, 1, delta); // Retrieve the text at the old and new positions. const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); const auto shouldBeFireText = *_buffer->GetTextDataAt(newPos); VERIFY_ARE_EQUAL(String(L" "), String(shouldBeEmptyText.data(), gsl::narrow(shouldBeEmptyText.size()))); VERIFY_ARE_EQUAL(String(fire), String(shouldBeFireText.data(), gsl::narrow(shouldBeFireText.size()))); } // This tests that rows removed from the buffer while resizing traditionally will also drop the high unicode // characters from the Unicode Storage buffer void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() { // Set up a text buffer for us const COORD bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Get a position inside the buffer in the bottom row const COORD pos{ 0, bufferSize.Y - 1 }; auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); // Fill it up with a sequence that will have to hit the high unicode storage. // This is the eggplant emoji: 🍆 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF46"; position = emoji; // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); const auto readBackText = *readBack; VERIFY_ARE_EQUAL(String(emoji), String(readBackText.data(), gsl::narrow(readBackText.size()))); VERIFY_ARE_EQUAL(1u, _buffer->GetUnicodeStorage()._map.size(), L"There should be one item in the map."); // Perform resize to trim off the row of the buffer that included the emoji COORD trimmedBufferSize{ bufferSize.X, bufferSize.Y - 1 }; VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); VERIFY_IS_TRUE(_buffer->GetUnicodeStorage()._map.empty(), L"The map should now be empty."); } // This tests that columns removed from the buffer while resizing traditionally will also drop the high unicode // characters from the Unicode Storage buffer void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() { // Set up a text buffer for us const COORD bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Get a position inside the buffer in the last column const COORD pos{ bufferSize.X - 1, 0 }; auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); // Fill it up with a sequence that will have to hit the high unicode storage. // This is the peach emoji: 🍑 // It's encoded in UTF-16, as needed by the buffer. const auto emoji = L"\xD83C\xDF51"; position = emoji; // Read back the text at that position and ensure that it matches what we wrote. const auto readBack = _buffer->GetTextDataAt(pos); const auto readBackText = *readBack; VERIFY_ARE_EQUAL(String(emoji), String(readBackText.data(), gsl::narrow(readBackText.size()))); VERIFY_ARE_EQUAL(1u, _buffer->GetUnicodeStorage()._map.size(), L"There should be one item in the map."); // Perform resize to trim off the column of the buffer that included the emoji COORD trimmedBufferSize{ bufferSize.X - 1, bufferSize.Y }; VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); VERIFY_IS_TRUE(_buffer->GetUnicodeStorage()._map.empty(), L"The map should now be empty."); } void TextBufferTests::TestBurrito() { COORD bufferSize{ 80, 9001 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // This is the burrito emoji: 🌯 // It's encoded in UTF-16, as needed by the buffer. const auto burrito = L"\xD83C\xDF2F"; OutputCellIterator burriter{ burrito }; auto afterFIter = _buffer->Write({ L"F" }); _buffer->IncrementCursor(); auto afterBurritoIter = _buffer->Write(burriter); _buffer->IncrementCursor(); _buffer->IncrementCursor(); VERIFY_IS_FALSE(afterBurritoIter); } void TextBufferTests::WriteLinesToBuffer(const std::vector& text, TextBuffer& buffer) { const auto bufferSize = buffer.GetSize(); for (size_t row = 0; row < text.size(); ++row) { auto line = text[row]; if (!line.empty()) { // TODO GH#780: writing up to (but not past) the end of the line // should NOT set the wrap flag std::optional wrap = true; if (line.size() == static_cast(bufferSize.RightExclusive())) { wrap = std::nullopt; } OutputCellIterator iter{ line }; buffer.Write(iter, { 0, gsl::narrow(row) }, wrap); } } } void TextBufferTests::GetWordBoundaries() { COORD bufferSize{ 80, 9001 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Setup: Write lines of text to the buffer const std::vector text = { L"word other", L" more words" }; WriteLinesToBuffer(text, *_buffer); // Test Data: // - COORD - starting position // - COORD - expected result (accessibilityMode = false) // - COORD - expected result (accessibilityMode = true) struct ExpectedResult { COORD accessibilityModeDisabled; COORD accessibilityModeEnabled; }; struct Test { COORD startPos; ExpectedResult expected; }; // Set testData for GetWordStart tests // clang-format off std::vector testData = { // tests for first line of text { { 0, 0 }, {{ 0, 0 }, { 0, 0 }} }, { { 1, 0 }, {{ 0, 0 }, { 0, 0 }} }, { { 3, 0 }, {{ 0, 0 }, { 0, 0 }} }, { { 4, 0 }, {{ 4, 0 }, { 0, 0 }} }, { { 5, 0 }, {{ 5, 0 }, { 5, 0 }} }, { { 6, 0 }, {{ 5, 0 }, { 5, 0 }} }, { { 20, 0 }, {{ 10, 0 }, { 5, 0 }} }, { { 79, 0 }, {{ 10, 0 }, { 5, 0 }} }, // tests for second line of text { { 0, 1 }, {{ 0, 1 }, { 5, 0 }} }, { { 1, 1 }, {{ 0, 1 }, { 5, 0 }} }, { { 2, 1 }, {{ 2, 1 }, { 2, 1 }} }, { { 3, 1 }, {{ 2, 1 }, { 2, 1 }} }, { { 5, 1 }, {{ 2, 1 }, { 2, 1 }} }, { { 6, 1 }, {{ 6, 1 }, { 2, 1 }} }, { { 7, 1 }, {{ 6, 1 }, { 2, 1 }} }, { { 9, 1 }, {{ 9, 1 }, { 9, 1 }} }, { { 10, 1 }, {{ 9, 1 }, { 9, 1 }} }, { { 20, 1 }, {{14, 1 }, { 9, 1 }} }, { { 79, 1 }, {{14, 1 }, { 9, 1 }} }, }; // clang-format on BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:accessibilityMode", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool accessibilityMode; VERIFY_SUCCEEDED(TestData::TryGetValue(L"accessibilityMode", accessibilityMode), L"Get accessibility mode variant"); const std::wstring_view delimiters = L" "; for (const auto& test : testData) { Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.X, test.startPos.Y)); const auto result = _buffer->GetWordStart(test.startPos, delimiters, accessibilityMode); const auto expected = accessibilityMode ? test.expected.accessibilityModeEnabled : test.expected.accessibilityModeDisabled; VERIFY_ARE_EQUAL(expected, result); } // Update testData for GetWordEnd tests // clang-format off testData = { // tests for first line of text { { 0, 0 }, { { 3, 0 }, { 5, 0 } } }, { { 1, 0 }, { { 3, 0 }, { 5, 0 } } }, { { 3, 0 }, { { 3, 0 }, { 5, 0 } } }, { { 4, 0 }, { { 4, 0 }, { 5, 0 } } }, { { 5, 0 }, { { 9, 0 }, { 2, 1 } } }, { { 6, 0 }, { { 9, 0 }, { 2, 1 } } }, { { 20, 0 }, { { 79, 0 }, { 2, 1 } } }, { { 79, 0 }, { { 79, 0 }, { 2, 1 } } }, // tests for second line of text { { 0, 1 }, { { 1, 1 }, { 2, 1 } } }, { { 1, 1 }, { { 1, 1 }, { 2, 1 } } }, { { 2, 1 }, { { 5, 1 }, { 9, 1 } } }, { { 3, 1 }, { { 5, 1 }, { 9, 1 } } }, { { 5, 1 }, { { 5, 1 }, { 9, 1 } } }, { { 6, 1 }, { { 8, 1 }, { 9, 1 } } }, { { 7, 1 }, { { 8, 1 }, { 9, 1 } } }, { { 9, 1 }, { { 13, 1 }, { 0, 9001 } } }, { { 10, 1 }, { { 13, 1 }, { 0, 9001 } } }, { { 20, 1 }, { { 79, 1 }, { 0, 9001 } } }, { { 79, 1 }, { { 79, 1 }, { 0, 9001 } } }, }; // clang-format on for (const auto& test : testData) { Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.X, test.startPos.Y)); COORD result = _buffer->GetWordEnd(test.startPos, delimiters, accessibilityMode); const auto expected = accessibilityMode ? test.expected.accessibilityModeEnabled : test.expected.accessibilityModeDisabled; VERIFY_ARE_EQUAL(expected, result); } } void TextBufferTests::MoveByWord() { COORD bufferSize{ 80, 9001 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Setup: Write lines of text to the buffer const std::vector text = { L"word other", L" more words" }; WriteLinesToBuffer(text, *_buffer); // Test Data: // - COORD - starting position // - COORD - expected result (moving forwards) // - COORD - expected result (moving backwards) struct ExpectedResult { COORD moveForwards; COORD moveBackwards; }; struct Test { COORD startPos; ExpectedResult expected; }; // Set testData for GetWordStart tests // clang-format off std::vector testData = { // tests for first line of text { { 0, 0 }, {{ 5, 0 }, { 0, 0 }} }, { { 1, 0 }, {{ 5, 0 }, { 1, 0 }} }, { { 3, 0 }, {{ 5, 0 }, { 3, 0 }} }, { { 4, 0 }, {{ 5, 0 }, { 4, 0 }} }, { { 5, 0 }, {{ 2, 1 }, { 0, 0 }} }, { { 6, 0 }, {{ 2, 1 }, { 0, 0 }} }, { { 20, 0 }, {{ 2, 1 }, { 0, 0 }} }, { { 79, 0 }, {{ 2, 1 }, { 0, 0 }} }, // tests for second line of text { { 0, 1 }, {{ 2, 1 }, { 0, 0 }} }, { { 1, 1 }, {{ 2, 1 }, { 0, 0 }} }, { { 2, 1 }, {{ 9, 1 }, { 5, 0 }} }, { { 3, 1 }, {{ 9, 1 }, { 5, 0 }} }, { { 5, 1 }, {{ 9, 1 }, { 5, 0 }} }, { { 6, 1 }, {{ 9, 1 }, { 5, 0 }} }, { { 7, 1 }, {{ 9, 1 }, { 5, 0 }} }, { { 9, 1 }, {{ 9, 1 }, { 2, 1 }} }, { { 10, 1 }, {{10, 1 }, { 2, 1 }} }, { { 20, 1 }, {{20, 1 }, { 2, 1 }} }, { { 79, 1 }, {{79, 1 }, { 2, 1 }} }, }; // clang-format on BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:movingForwards", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool movingForwards; VERIFY_SUCCEEDED(TestData::TryGetValue(L"movingForwards", movingForwards), L"Get movingForwards variant"); const std::wstring_view delimiters = L" "; const COORD lastCharPos = _buffer->GetLastNonSpaceCharacter(); for (const auto& test : testData) { Log::Comment(NoThrowString().Format(L"COORD (%hd, %hd)", test.startPos.X, test.startPos.Y)); auto pos{ test.startPos }; const auto result = movingForwards ? _buffer->MoveToNextWord(pos, delimiters, lastCharPos) : _buffer->MoveToPreviousWord(pos, delimiters); const auto expected = movingForwards ? test.expected.moveForwards : test.expected.moveBackwards; VERIFY_ARE_EQUAL(expected, pos); // if we moved, result is true and pos != startPos. // otherwise, result is false and pos == startPos. VERIFY_ARE_EQUAL(result, pos != test.startPos); } } void TextBufferTests::GetGlyphBoundaries() { struct ExpectedResult { std::wstring name; til::point start; til::point wideGlyphEnd; til::point normalEnd; }; // clang-format off const std::vector expected = { { L"Buffer Start", { 0, 0 }, { 2, 0 }, { 1, 0 } }, { L"Line Start", { 0, 1 }, { 2, 1 }, { 1, 1 } }, { L"General Case", { 1, 1 }, { 3, 1 }, { 2, 1 } }, { L"Line End", { 9, 1 }, { 0, 2 }, { 0, 2 } }, { L"Buffer End", { 9, 9 }, { 0, 10 }, { 0, 10 } }, }; // clang-format on BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:wideGlyph", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool wideGlyph; VERIFY_SUCCEEDED(TestData::TryGetValue(L"wideGlyph", wideGlyph), L"Get wide glyph variant"); COORD bufferSize{ 10, 10 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // This is the burrito emoji: 🌯 // It's encoded in UTF-16, as needed by the buffer. const auto burrito = L"\xD83C\xDF2F"; const wchar_t* const output = wideGlyph ? burrito : L"X"; const OutputCellIterator iter{ output }; for (const auto& test : expected) { Log::Comment(test.name.c_str()); auto target = test.start; _buffer->Write(iter, target); auto start = _buffer->GetGlyphStart(target); auto end = _buffer->GetGlyphEnd(target, true); VERIFY_ARE_EQUAL(test.start, start); VERIFY_ARE_EQUAL(wideGlyph ? test.wideGlyphEnd : test.normalEnd, end); } } void TextBufferTests::GetTextRects() { // GetTextRects() is used to... // - Represent selection rects // - Represent UiaTextRanges for accessibility // This is the burrito emoji: 🌯 // It's encoded in UTF-16, as needed by the buffer. const auto burrito = std::wstring(L"\xD83C\xDF2F"); COORD bufferSize{ 20, 50 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Setup: Write lines of text to the buffer const std::vector text = { L"0123456789", L" " + burrito + L"3456" + burrito, L" " + burrito + L"45" + burrito, burrito + L"234567" + burrito, L"0123456789" }; WriteLinesToBuffer(text, *_buffer); // - - - Text Buffer Contents - - - // |0123456789 // | 🌯3456🌯 // | 🌯45🌯 // |🌯234567🌯 // |0123456789 // - - - - - - - - - - - - - - - - BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:blockSelection", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool blockSelection; VERIFY_SUCCEEDED(TestData::TryGetValue(L"blockSelection", blockSelection), L"Get 'blockSelection' variant"); std::vector expected{}; if (blockSelection) { expected.push_back({ 1, 0, 7, 0 }); expected.push_back({ 1, 1, 8, 1 }); // expand right expected.push_back({ 1, 2, 7, 2 }); expected.push_back({ 0, 3, 7, 3 }); // expand left expected.push_back({ 1, 4, 7, 4 }); } else { expected.push_back({ 1, 0, 19, 0 }); expected.push_back({ 0, 1, 19, 1 }); expected.push_back({ 0, 2, 19, 2 }); expected.push_back({ 0, 3, 19, 3 }); expected.push_back({ 0, 4, 7, 4 }); } COORD start{ 1, 0 }; COORD end{ 7, 4 }; const auto result = _buffer->GetTextRects(start, end, blockSelection, false); VERIFY_ARE_EQUAL(expected.size(), result.size()); for (size_t i = 0; i < expected.size(); ++i) { VERIFY_ARE_EQUAL(expected.at(i), result.at(i)); } } void TextBufferTests::GetText() { // GetText() is used by... // - Copying text to the clipboard regularly // - Copying text to the clipboard, with shift held (collapse to one line) // - Extracting text from a UiaTextRange BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:wrappedText", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:blockSelection", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:includeCRLF", L"{false, true}") TEST_METHOD_PROPERTY(L"Data:trimTrailingWhitespace", L"{false, true}") END_TEST_METHOD_PROPERTIES(); bool wrappedText; bool blockSelection; bool includeCRLF; bool trimTrailingWhitespace; VERIFY_SUCCEEDED(TestData::TryGetValue(L"wrappedText", wrappedText), L"Get 'wrappedText' variant"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"blockSelection", blockSelection), L"Get 'blockSelection' variant"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"includeCRLF", includeCRLF), L"Get 'includeCRLF' variant"); VERIFY_SUCCEEDED(TestData::TryGetValue(L"trimTrailingWhitespace", trimTrailingWhitespace), L"Get 'trimTrailingWhitespace' variant"); if (!wrappedText) { COORD bufferSize{ 10, 20 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Setup: Write lines of text to the buffer const std::vector bufferText = { L"12345", L" 345", L"123 ", L" 3 " }; WriteLinesToBuffer(bufferText, *_buffer); // simulate a selection from origin to {4,4} const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 4 }, blockSelection, false); std::wstring result = L""; const auto textData = _buffer->GetText(includeCRLF, trimTrailingWhitespace, textRects).text; for (auto& text : textData) { result += text; } std::wstring expectedText = L""; if (includeCRLF) { if (trimTrailingWhitespace) { Log::Comment(L"Standard Copy to Clipboard"); expectedText += L"12345\r\n"; expectedText += L" 345\r\n"; expectedText += L"123\r\n"; expectedText += L" 3\r\n"; } else { Log::Comment(L"UI Automation"); if (blockSelection) { expectedText += L"12345\r\n"; expectedText += L" 345\r\n"; expectedText += L"123 \r\n"; expectedText += L" 3 \r\n"; expectedText += L" "; } else { expectedText += L"12345 \r\n"; expectedText += L" 345 \r\n"; expectedText += L"123 \r\n"; expectedText += L" 3 \r\n"; expectedText += L" "; } } } else { if (trimTrailingWhitespace) { Log::Comment(L"UNDEFINED"); expectedText += L"12345"; expectedText += L" 345"; expectedText += L"123"; expectedText += L" 3"; } else { Log::Comment(L"Shift+Copy to Clipboard"); if (blockSelection) { expectedText += L"12345"; expectedText += L" 345"; expectedText += L"123 "; expectedText += L" 3 "; expectedText += L" "; } else { expectedText += L"12345 "; expectedText += L" 345 "; expectedText += L"123 "; expectedText += L" 3 "; expectedText += L" "; } } } // Verify expected output and actual output are the same VERIFY_ARE_EQUAL(expectedText, result); } else { // Case 2: Wrapped Text COORD bufferSize{ 5, 20 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); // Setup: Write lines of text to the buffer const std::vector bufferText = { L"1234567", L"", L" 345", L"123 ", L"" }; WriteLinesToBuffer(bufferText, *_buffer); // buffer should look like this: // ______ // |12345| <-- wrapped // |67 | // | 345| // |123 | <-- wrapped // | | // |_____| // simulate a selection from origin to {4,5} const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 5 }, blockSelection, false); std::wstring result = L""; const auto formatWrappedRows = blockSelection; const auto textData = _buffer->GetText(includeCRLF, trimTrailingWhitespace, textRects, nullptr, formatWrappedRows).text; for (auto& text : textData) { result += text; } std::wstring expectedText = L""; if (formatWrappedRows) { if (includeCRLF) { if (trimTrailingWhitespace) { Log::Comment(L"UNDEFINED"); expectedText += L"12345\r\n"; expectedText += L"67\r\n"; expectedText += L" 345\r\n"; expectedText += L"123\r\n"; expectedText += L"\r\n"; } else { Log::Comment(L"Copy block selection to Clipboard"); expectedText += L"12345\r\n"; expectedText += L"67 \r\n"; expectedText += L" 345\r\n"; expectedText += L"123 \r\n"; expectedText += L" \r\n"; expectedText += L" "; } } else { if (trimTrailingWhitespace) { Log::Comment(L"UNDEFINED"); expectedText += L"12345"; expectedText += L"67"; expectedText += L" 345"; expectedText += L"123"; } else { Log::Comment(L"UNDEFINED"); expectedText += L"12345"; expectedText += L"67 "; expectedText += L" 345"; expectedText += L"123 "; expectedText += L" "; expectedText += L" "; } } } else { if (includeCRLF) { if (trimTrailingWhitespace) { Log::Comment(L"Standard Copy to Clipboard"); expectedText += L"12345"; expectedText += L"67\r\n"; expectedText += L" 345\r\n"; expectedText += L"123 \r\n"; } else { Log::Comment(L"UI Automation"); expectedText += L"12345"; expectedText += L"67 \r\n"; expectedText += L" 345\r\n"; expectedText += L"123 "; expectedText += L" \r\n"; expectedText += L" "; } } else { if (trimTrailingWhitespace) { Log::Comment(L"UNDEFINED"); expectedText += L"12345"; expectedText += L"67"; expectedText += L" 345"; expectedText += L"123 "; } else { Log::Comment(L"Shift+Copy to Clipboard"); expectedText += L"12345"; expectedText += L"67 "; expectedText += L" 345"; expectedText += L"123 "; expectedText += L" "; expectedText += L" "; } } } // Verify expected output and actual output are the same VERIFY_ARE_EQUAL(expectedText, result); } } // This tests that when we increment the circular buffer, obsolete hyperlink references // are removed from the hyperlink map void TextBufferTests::HyperlinkTrim() { // Set up a text buffer for us const COORD bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); const auto url = L"test.url"; const auto otherUrl = L"other.url"; const auto customId = L"CustomId"; const auto otherCustomId = L"OtherCustomId"; // Set a hyperlink id in the first row and add a hyperlink to our map const COORD pos{ 70, 0 }; const auto id = _buffer->GetHyperlinkId(url, customId); TextAttribute newAttr{ 0x7f }; newAttr.SetHyperlinkId(id); _buffer->GetRowByOffset(pos.Y).GetAttrRow().SetAttrToEnd(pos.X, newAttr); _buffer->AddHyperlinkToMap(url, id); // Set a different hyperlink id somewhere else in the buffer const COORD otherPos{ 70, 5 }; const auto otherId = _buffer->GetHyperlinkId(otherUrl, otherCustomId); newAttr.SetHyperlinkId(otherId); _buffer->GetRowByOffset(otherPos.Y).GetAttrRow().SetAttrToEnd(otherPos.X, newAttr); _buffer->AddHyperlinkToMap(otherUrl, otherId); // Increment the circular buffer _buffer->IncrementCircularBuffer(); const auto finalCustomId = fmt::format(L"{}%{}", customId, std::hash{}(url)); const auto finalOtherCustomId = fmt::format(L"{}%{}", otherCustomId, std::hash{}(otherUrl)); // The hyperlink reference that was only in the first row should be deleted from the map VERIFY_ARE_EQUAL(_buffer->_hyperlinkMap.find(id), _buffer->_hyperlinkMap.end()); // Since there was a custom id, that should be deleted as well VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap.find(finalCustomId), _buffer->_hyperlinkCustomIdMap.end()); // The other hyperlink reference should not be deleted VERIFY_ARE_EQUAL(_buffer->_hyperlinkMap[otherId], otherUrl); VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap[finalOtherCustomId], otherId); } // This tests that when we increment the circular buffer, non-obsolete hyperlink references // do not get removed from the hyperlink map void TextBufferTests::NoHyperlinkTrim() { // Set up a text buffer for us const COORD bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); const auto url = L"test.url"; const auto customId = L"CustomId"; // Set a hyperlink id in the first row and add a hyperlink to our map const COORD pos{ 70, 0 }; const auto id = _buffer->GetHyperlinkId(url, customId); TextAttribute newAttr{ 0x7f }; newAttr.SetHyperlinkId(id); _buffer->GetRowByOffset(pos.Y).GetAttrRow().SetAttrToEnd(pos.X, newAttr); _buffer->AddHyperlinkToMap(url, id); // Set the same hyperlink id somewhere else in the buffer const COORD otherPos{ 70, 5 }; _buffer->GetRowByOffset(otherPos.Y).GetAttrRow().SetAttrToEnd(otherPos.X, newAttr); // Increment the circular buffer _buffer->IncrementCircularBuffer(); const auto finalCustomId = fmt::format(L"{}%{}", customId, std::hash{}(url)); // The hyperlink reference should not be deleted from the map since it is still present in the buffer VERIFY_ARE_EQUAL(_buffer->GetHyperlinkUriFromId(id), url); VERIFY_ARE_EQUAL(_buffer->_hyperlinkCustomIdMap[finalCustomId], id); }