mirror of
https://github.com/aphrody-labs/WSL.git
synced 2026-10-17 20:56:23 +00:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1180 lines
43 KiB
C++
1180 lines
43 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
WSLCCLITableDataUnitTests.cpp
|
|
|
|
Abstract:
|
|
|
|
Unit tests for the TableData model and renderer.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "windows/Common.h"
|
|
#include "WSLCCLITestHelpers.h"
|
|
|
|
#include "TableRenderer.h"
|
|
#include "VTSupport.h"
|
|
|
|
using namespace wsl::windows::wslc;
|
|
using namespace wsl::windows::wslc::cli;
|
|
using namespace wsl::windows::cli::table;
|
|
using namespace wsl::windows::common::vt;
|
|
using namespace WSLCTestHelpers;
|
|
using namespace WEX::Logging;
|
|
using namespace WEX::Common;
|
|
using namespace WEX::TestExecution;
|
|
|
|
namespace WSLCCLITableDataUnitTests {
|
|
|
|
class WSLCCLITableDataUnitTests
|
|
{
|
|
WSLC_TEST_CLASS(WSLCCLITableDataUnitTests)
|
|
|
|
TEST_CLASS_SETUP(TestClassSetup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(TestClassCleanup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_METHOD(TableData_EmptyTable_EmitsHeader)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_NoHeader_EmitsNothingWhenEmpty)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
|
|
}
|
|
|
|
TEST_METHOD(TableRenderer_Layout_ProducesLinesWithoutTerminal)
|
|
{
|
|
TableData table({L"NAME", L"STATUS"});
|
|
table.MinCellWidth = 0;
|
|
table.AddRow({L"my-container", L"running"});
|
|
|
|
const auto layout = TableRenderer::Layout(table, LayoutOptions{.ConsoleWidth = 120});
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), layout.Lines.size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"NAME STATUS"}, layout.Lines[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"my-container running"}, layout.Lines[1]);
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), layout.ColumnWidths.size());
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(12), layout.ColumnWidths[0]);
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(7), layout.ColumnWidths[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_DefaultMinCellWidth_ProducesExactSpacing)
|
|
{
|
|
TableData table{L"A", L"B", L"C"};
|
|
table.AddRow({L"x", L"y", L"z"});
|
|
|
|
const auto layout = TableRenderer::Layout(table, LayoutOptions{.ConsoleWidth = 120});
|
|
|
|
VERIFY_ARE_EQUAL(std::wstring{L"A B C"}, layout.Lines[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"x y z"}, layout.Lines[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableRenderer_Layout_ConsoleWidthConstrainsColumns)
|
|
{
|
|
TableData table({L"NAME", L"STATUS"});
|
|
table.MinCellWidth = 0;
|
|
table.ColumnConfig(0) = {.Overflow = ColumnOverflow::Shrink};
|
|
table.AddRow({L"a-very-long-container-name", L"running"});
|
|
|
|
const auto layout = TableRenderer::Layout(table, LayoutOptions{.ConsoleWidth = 20});
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(10), layout.ColumnWidths[0]);
|
|
VERIFY_IS_TRUE(layout.Lines[1].size() <= 20);
|
|
}
|
|
|
|
TEST_METHOD(TableRenderer_Layout_EmptyWithoutHeaderProducesNoLines)
|
|
{
|
|
TableData table({L"NAME", L"STATUS"});
|
|
table.ShowHeader = false;
|
|
|
|
const auto layout = TableRenderer::Layout(table, LayoutOptions{});
|
|
|
|
VERIFY_IS_TRUE(layout.Lines.empty());
|
|
VERIFY_IS_TRUE(layout.ColumnWidths.empty());
|
|
}
|
|
|
|
TEST_METHOD(TableData_SingleRow_EmitsHeaderPlusOneDataLine)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
|
|
cap.table.AddRow({L"my-container", L"running"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"my-container") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"running") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_MultipleRows_AllRowsEmittedAfterHeader)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
|
|
cap.table.AddRow({L"container-a", L"running"});
|
|
cap.table.AddRow({L"container-b", L"stopped"});
|
|
cap.table.AddRow({L"container-c", L"paused"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(4), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"container-a") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[2].find(L"container-b") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[3].find(L"container-c") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ColumnPadding_DefaultPaddingApplied)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
|
|
cap.table.AddRow({L"abc", L"ok"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
const auto& dataLine = lines[1];
|
|
VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos);
|
|
|
|
auto columnPadding = 3;
|
|
auto namePos = dataLine.find(L"abc");
|
|
auto statusPos = dataLine.find(L"ok");
|
|
VERIFY_IS_TRUE(statusPos >= namePos + wcslen(L"abc") + columnPadding);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ColumnPadding_CustomPaddingApplied)
|
|
{
|
|
constexpr size_t customPadding = 5;
|
|
TableCapture cap({L"A", L"B"});
|
|
cap.table.ColumnPadding = customPadding;
|
|
|
|
cap.table.AddRow({L"x", L"y"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
const auto& dataLine = lines[1];
|
|
auto posX = dataLine.find(L'x');
|
|
auto posY = dataLine.find(L'y');
|
|
VERIFY_IS_TRUE(posX != std::wstring::npos);
|
|
VERIFY_IS_TRUE(posY != std::wstring::npos);
|
|
VERIFY_IS_TRUE(posY >= posX + 1 + customPadding);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ColumnWidth_ExpandsToFitData)
|
|
{
|
|
TableCapture cap({L"ID", L"NAME"});
|
|
|
|
cap.table.AddRow({L"1", L"short"});
|
|
cap.table.AddRow({L"2", L"a-very-long-container-name"});
|
|
cap.Render();
|
|
|
|
VERIFY_IS_TRUE(cap.lines()[2].find(L"a-very-long-container-name") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ColumnWidth_AtLeastHeaderWidth)
|
|
{
|
|
TableCapture cap({L"CONTAINER_NAME", L"ST"});
|
|
|
|
cap.table.AddRow({L"abc", L"ok"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
const auto& dataLine = lines[1];
|
|
auto posOk = dataLine.find(L"ok");
|
|
VERIFY_IS_TRUE(posOk != std::wstring::npos);
|
|
VERIFY_IS_TRUE(posOk >= static_cast<size_t>(14 + c_defaultColumnPadding));
|
|
}
|
|
|
|
TEST_METHOD(TableData_MinCellWidth_DefaultIsAppliedToNonFinalColumns)
|
|
{
|
|
const TableData defaults{L"A", L"B"};
|
|
VERIFY_ARE_EQUAL(c_defaultMinCellWidth, defaults.MinCellWidth);
|
|
|
|
TableCapture cap({L"A", L"B", L"C"});
|
|
cap.table.MinCellWidth = c_defaultMinCellWidth;
|
|
|
|
cap.table.AddRow({L"x", L"y", L"z"});
|
|
cap.Render();
|
|
|
|
// Columns narrower than the minimum are widened so each cell, padding included, occupies
|
|
// the minimum width.
|
|
const auto dataLine = cap.lines()[1];
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), dataLine.find(L'x'));
|
|
VERIFY_ARE_EQUAL(c_defaultMinCellWidth, dataLine.find(L'y'));
|
|
VERIFY_ARE_EQUAL(c_defaultMinCellWidth * 2, dataLine.find(L'z'));
|
|
}
|
|
|
|
TEST_METHOD(TableData_MinCellWidth_FinalColumnIsExempt)
|
|
{
|
|
TableCapture cap({L"A", L"B"});
|
|
cap.table.MinCellWidth = c_defaultMinCellWidth;
|
|
|
|
cap.table.AddRow({L"x", L"y"});
|
|
cap.Render();
|
|
|
|
// The final column renders at its natural width, so no line carries trailing whitespace.
|
|
for (const auto& line : cap.lines())
|
|
{
|
|
VERIFY_ARE_EQUAL(c_defaultMinCellWidth + 1, line.size());
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(TableData_MinCellWidth_WiderColumnKeepsItsWidth)
|
|
{
|
|
TableCapture cap({L"CONTAINER_NAME", L"STATUS"});
|
|
cap.table.MinCellWidth = c_defaultMinCellWidth;
|
|
|
|
cap.table.AddRow({L"abc", L"ok"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(wcslen(L"CONTAINER_NAME") + c_defaultColumnPadding, cap.lines()[1].find(L"ok"));
|
|
}
|
|
|
|
TEST_METHOD(TableData_MaxWidth_LongValueIsTruncatedWithEllipsis)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 8;
|
|
configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"NAME", configs[0]}, {L"STATUS", configs[1]}});
|
|
|
|
cap.table.AddRow({L"a-very-long-name", L"running"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
const auto& dataLine = lines[1];
|
|
VERIFY_IS_TRUE(dataLine.find(L"\x2026") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(dataLine.find(L"a-very-long-name") == std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_MaxWidth_ShortValueNotTruncated)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 20;
|
|
configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"NAME", configs[0]}, {L"STATUS", configs[1]}});
|
|
|
|
cap.table.AddRow({L"short", L"running"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
const auto& dataLine = lines[1];
|
|
VERIFY_IS_TRUE(dataLine.find(L"short") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(dataLine.find(L"\x2026") == std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ConsoleWidthLimit_PreferredShrinkColumnIsShrunk)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = ColumnWidthConfig::NoLimit;
|
|
configs[0].Overflow = ColumnOverflow::Shrink;
|
|
configs[0].PreferredShrink = false;
|
|
configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
|
|
configs[1].Overflow = ColumnOverflow::Shrink;
|
|
configs[1].PreferredShrink = true;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"ID", configs[0]}, {L"DESCRIPTION", configs[1]}});
|
|
cap.consoleWidth = 20;
|
|
|
|
cap.table.AddRow({L"abc123", L"this-is-a-long-description-value"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
for (const auto& line : lines)
|
|
{
|
|
VERIFY_IS_TRUE(line.size() <= static_cast<size_t>(20));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(TableData_IsEmpty)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
VERIFY_IS_TRUE(cap.table.IsEmpty());
|
|
|
|
cap.table.AddRow({L"foo", L"bar"});
|
|
VERIFY_IS_FALSE(cap.table.IsEmpty());
|
|
}
|
|
|
|
TEST_METHOD(TableData_ColumnDefinition_NameAndConfigUsed)
|
|
{
|
|
std::vector<ColumnDefinition> defs{
|
|
ColumnDefinition{
|
|
L"MYID",
|
|
{
|
|
.MinWidth = ColumnWidthConfig::NoLimit,
|
|
.MaxWidth = 6,
|
|
.Overflow = ColumnOverflow::Shrink,
|
|
.PreferredShrink = false,
|
|
}},
|
|
ColumnDefinition{L"MYNAME", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = ColumnWidthConfig::NoLimit}},
|
|
};
|
|
|
|
TableCapture cap(std::move(defs));
|
|
|
|
cap.table.AddRow({L"id-value", L"name-value"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"MYID") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"MYNAME") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"\x2026") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ShowHeader_False_SuppressesHeaderWithDataRows)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"my-container", L"running"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ShowHeader_False_SuppressesHeaderWhenEmpty)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
|
|
}
|
|
|
|
TEST_METHOD(TableData_ShowHeader_True_IsDefaultAndEmitsHeader)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
|
|
cap.table.AddRow({L"my-container", L"running"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ShowHeader_False_MultipleDataRowsNoHeader)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"container-a", L"running"});
|
|
cap.table.AddRow({L"container-b", L"stopped"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ShowHeader)
|
|
{
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
|
|
cap.table.AddRow({L"my-container", L"running"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
|
|
}
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"my-container", L"running"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
|
|
}
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
|
|
}
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"container-a", L"running"});
|
|
cap.table.AddRow({L"container-b", L"stopped"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
|
|
VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(TableData_RowIndent_PrependedToEveryRow)
|
|
{
|
|
TableCapture cap({L"NAME", L"STATUS"});
|
|
cap.table.RowIndent = 2;
|
|
|
|
cap.table.AddRow({L"abc", L"ok"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L" NAME STATUS"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" abc ok"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_ShortValueProducesOneRow)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 10;
|
|
configs[1].MaxWidth = 20;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", L"short desc"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt short desc"}, cap.lines()[0]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_WrapsAtWordBoundary)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 10;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", L"hello world"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_ContinuationRowHasBlankLeadingColumns)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 10;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", L"hello world"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_MultipleWrapsProduceMultipleRows)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 4;
|
|
configs[1].MaxWidth = 10;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", L"one two three four"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt one two"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" three four"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_HardBreakWhenNoSpaceFound)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 4;
|
|
configs[1].MaxWidth = 6;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", L"abcdefghij"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt abcdef"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" ghij"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_NonWrappingColumnStillTruncates)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 5;
|
|
// ColumnOverflow::Truncate (default) — truncates
|
|
configs[1].MaxWidth = 20;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"toolongname", L"short desc"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"tool\u2026 short desc"}, cap.lines()[0]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_RowIndentAppliedToAllPhysicalRows)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 4;
|
|
configs[1].MaxWidth = 8;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
cap.table.RowIndent = 2;
|
|
|
|
cap.table.AddRow({L"opt", L"hello world"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L" opt hello"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_DisabledByDefault_LongTextTruncated)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 4;
|
|
configs[1].MaxWidth = 8;
|
|
// ColumnOverflow::Truncate (default) — truncates
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", L"a very long description"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt a very \u2026"}, cap.lines()[0]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_MultipleLogicalRowsEachWrapIndependently)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 10;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt-a", L"hello world"});
|
|
cap.table.AddRow({L"opt-b", L"short"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt-a hello"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt-b short"}, cap.lines()[2]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_TwoWrappingColumnsColBLonger)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 5;
|
|
configs[0].Overflow = ColumnOverflow::Wrap;
|
|
configs[1].MaxWidth = 5;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"ab cd ef", L"one two three"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"ab cd one"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"ef two"}, cap.lines()[1]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" three"}, cap.lines()[2]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_TwoWrappingColumnsColALonger)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 5;
|
|
configs[0].Overflow = ColumnOverflow::Wrap;
|
|
configs[1].MaxWidth = 5;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"one two three", L"ab cd ef"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_TwoWrappingColumnsWithEqualLengths)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 4;
|
|
configs[0].Overflow = ColumnOverflow::Wrap;
|
|
configs[1].MaxWidth = 4;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"aa bb", L"xx yy"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"aa xx"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"bb yy"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_NonWrappingColumnBetweenTwoWrappingColumns)
|
|
{
|
|
ColumnWidthConfig configs[3]{};
|
|
configs[0].MaxWidth = 4;
|
|
// configs[0].Overflow = ColumnOverflow::Truncate (default) — truncates
|
|
configs[1].MaxWidth = 5;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
configs[2].MaxWidth = 5;
|
|
configs[2].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}, {L"", configs[2]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"tag", L"aa bb", L"xx yy zz"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"tag aa bb xx yy"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" zz"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_WordWrap_FirstColumnLongerThanSecond)
|
|
{
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 5;
|
|
configs[0].Overflow = ColumnOverflow::Wrap;
|
|
configs[1].MaxWidth = 5;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"one two three", L"ab cd ef"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_ColumnConfig_WordWrapAfterConstruction)
|
|
{
|
|
TableCapture cap({L"", L""});
|
|
cap.table.ShowHeader = false;
|
|
cap.table.ColumnConfig(1) = ColumnWidthConfig{
|
|
.MaxWidth = 8,
|
|
.Overflow = ColumnOverflow::Wrap,
|
|
};
|
|
|
|
cap.table.AddRow({L"opt", L"hello world"});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
|
|
VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddRow_RejectsCellCountMismatch)
|
|
{
|
|
TableData table({L"NAME", L"STATUS"});
|
|
|
|
std::vector<Cell> tooFew{L"my-container"};
|
|
VERIFY_THROWS_SPECIFIC(table.AddRow(tooFew), wil::ResultException, [](const wil::ResultException& e) {
|
|
return e.GetErrorCode() == E_INVALIDARG;
|
|
});
|
|
|
|
std::vector<Cell> tooMany{L"my-container", L"running", L"extra"};
|
|
VERIFY_THROWS_SPECIFIC(table.AddRow(tooMany), wil::ResultException, [](const wil::ResultException& e) {
|
|
return e.GetErrorCode() == E_INVALIDARG;
|
|
});
|
|
|
|
VERIFY_IS_TRUE(table.IsEmpty());
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddRow_RejectsPartiallyBuiltRow)
|
|
{
|
|
TableData table({L"NAME", L"STATUS"});
|
|
|
|
Row row;
|
|
row.AddCell(L"my-container").AddCellIf(false, L"running");
|
|
|
|
VERIFY_THROWS_SPECIFIC(table.AddRow(std::move(row)), wil::ResultException, [](const wil::ResultException& e) {
|
|
return e.GetErrorCode() == E_INVALIDARG;
|
|
});
|
|
|
|
VERIFY_IS_TRUE(table.IsEmpty());
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddColumn_RejectsColumnAfterFirstRow)
|
|
{
|
|
TableData table;
|
|
table.AddColumn(L"NAME").AddColumn(L"STATUS");
|
|
|
|
std::vector<Cell> cells{L"my-container", L"running"};
|
|
table.AddRow(cells);
|
|
|
|
VERIFY_THROWS_SPECIFIC(table.AddColumn(L"SIZE"), wil::ResultException, [](const wil::ResultException& e) {
|
|
return e.GetErrorCode() == E_ILLEGAL_METHOD_CALL;
|
|
});
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), table.ColumnCount());
|
|
}
|
|
|
|
TEST_METHOD(Cell_VisibleWidth_PlainText)
|
|
{
|
|
Cell cell{L"hello"};
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(5), cell.VisibleWidth());
|
|
}
|
|
|
|
TEST_METHOD(Cell_VisibleWidth_StyledText)
|
|
{
|
|
Cell cell{L"hello", Format::Fg::BrightRed};
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(5), cell.VisibleWidth());
|
|
}
|
|
|
|
TEST_METHOD(Cell_Render_ColorEnabled)
|
|
{
|
|
Cell cell = Cell(L"hello", Format::Fg::BrightRed);
|
|
const auto result = cell.Render(true, true);
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
|
|
}
|
|
|
|
TEST_METHOD(Cell_Render_ColorDisabled)
|
|
{
|
|
Cell cell = Cell(L"hello", Format::Fg::BrightRed);
|
|
const auto result = cell.Render(false, false);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
|
|
}
|
|
|
|
TEST_METHOD(Cell_Render_VTEnabled_ColorDisabled_StripsColorSequences)
|
|
{
|
|
Cell cell = Cell(L"hello", Format::Fg::BrightRed);
|
|
// VT on but color off: color sequences should be stripped
|
|
const auto result = cell.Render(true, false);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
|
|
}
|
|
|
|
TEST_METHOD(Cell_RenderTruncated_TruncatesVisibleText)
|
|
{
|
|
Cell cell = Cell(L"hello world", Format::Fg::BrightRed);
|
|
const auto result = cell.RenderTruncated(5, true, true);
|
|
// Should contain truncated visible text + ellipsis + sequences
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
|
|
}
|
|
|
|
TEST_METHOD(TableData_Cell_ColumnWidthBasedOnVisibleChars)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 10;
|
|
configs[1].MaxWidth = 20;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}}, true);
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", Cell(L"hello", Format::Fg::BrightRed)});
|
|
cap.table.AddRow({L"end", Cell{L"world"}});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
// First cell is emphasized (has ESC), second is plain
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
|
|
VERIFY_ARE_EQUAL(std::wstring{L"end world"}, cap.lines()[1]);
|
|
}
|
|
|
|
TEST_METHOD(TableData_Cell_WrapsWithSequencesOnEachChunk)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 8;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}}, true);
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", Cell(L"hello world", Format::Fg::BrightRed)});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
// Both lines should have emphasis sequences
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
|
|
}
|
|
|
|
TEST_METHOD(Cell_DefaultCtor_EmptyCell)
|
|
{
|
|
Cell cell;
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
|
|
VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(true, true));
|
|
VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(false, false));
|
|
}
|
|
|
|
TEST_METHOD(Cell_SingleSequenceCtor_BuildsPrefixWithReset)
|
|
{
|
|
Cell cell(L"bold", Format::Bright);
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(4), cell.VisibleWidth());
|
|
VERIFY_IS_FALSE(cell.Prefix.Empty());
|
|
VERIFY_IS_FALSE(cell.Suffix.Empty());
|
|
|
|
// Color enabled: sequences emitted
|
|
const auto rendered = cell.Render(true, true);
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L"bold"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L'\x1b'));
|
|
|
|
// Color disabled: plain text only
|
|
VERIFY_ARE_EQUAL(std::wstring{L"bold"}, cell.Render(false, false));
|
|
}
|
|
|
|
TEST_METHOD(Cell_SequenceCtor_CopiesTemporarySequence)
|
|
{
|
|
const auto expected = std::wstring{Sgr({1, 31}).Get()};
|
|
|
|
Cell cell(L"styled", Sgr({1, 31}));
|
|
|
|
VERIFY_ARE_EQUAL(expected, cell.Prefix.Text);
|
|
VERIFY_IS_TRUE(cell.Prefix.IsColor);
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cell.Render(true, true).find(expected));
|
|
|
|
// The prefix and the trailing reset are both color sequences.
|
|
VERIFY_ARE_EQUAL(std::wstring{L"styled"}, cell.Render(true, false));
|
|
}
|
|
|
|
TEST_METHOD(Cell_VisibleWidth_StyledMultipleCharacters)
|
|
{
|
|
Cell cell{L"abc", Format::Bright};
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), cell.VisibleWidth());
|
|
}
|
|
|
|
TEST_METHOD(Cell_VisibleWidth_EmptyText)
|
|
{
|
|
Cell cell{L""};
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
|
|
}
|
|
|
|
TEST_METHOD(Cell_VisibleWidth_EmptyStyledText)
|
|
{
|
|
Cell cell{L"", Format::Bright};
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
|
|
}
|
|
|
|
TEST_METHOD(Cell_Render_PlainTextNoSequences)
|
|
{
|
|
Cell cell{L"plain text"};
|
|
VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(true, true));
|
|
VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(false, false));
|
|
}
|
|
|
|
TEST_METHOD(Cell_RenderTruncated_TextFitsNoTruncation)
|
|
{
|
|
Cell cell(L"hello", Format::Fg::BrightRed);
|
|
const auto result = cell.RenderTruncated(10, true, true);
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\u2026')); // no ellipsis
|
|
}
|
|
|
|
TEST_METHOD(Cell_RenderTruncated_ColorDisabled_StillTruncates)
|
|
{
|
|
Cell cell(L"hello world", Format::Fg::BrightRed);
|
|
const auto result = cell.RenderTruncated(5, false, false);
|
|
// Sequences stripped, but truncation still occurs
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
|
|
}
|
|
|
|
TEST_METHOD(Cell_RenderTruncated_PlainText)
|
|
{
|
|
Cell cell{L"abcdefghij"};
|
|
const auto result = cell.RenderTruncated(5, false, false);
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"fghij"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
|
|
}
|
|
|
|
TEST_METHOD(TableData_Cell_ColorDisabled_SequencesStripped)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 10;
|
|
configs[1].MaxWidth = 20;
|
|
|
|
// vtEnabled=false: sequences should be stripped from output
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}}, false);
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", Cell(L"hello", Format::Fg::BrightRed)});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
|
|
// No ESC bytes in output
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
|
|
// Visible text still present with correct padding
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
|
|
}
|
|
|
|
TEST_METHOD(TableData_Cell_WrapColorDisabled_SequencesStripped)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 8;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}}, false);
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt", Cell(L"hello world", Format::Fg::BrightRed)});
|
|
cap.Render();
|
|
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
|
|
// No ESC in either line
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
|
|
// Text still wraps correctly
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
|
|
}
|
|
|
|
TEST_METHOD(TableData_Cell_WrapMultipleLines_SequenceReapplied)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 5;
|
|
configs[1].Overflow = ColumnOverflow::Wrap;
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}}, true);
|
|
cap.table.ShowHeader = false;
|
|
|
|
// "aa bb cc dd" at width 5 breaks at spaces into multiple lines.
|
|
cap.table.AddRow({L"cmd", Cell(L"aa bb cc dd", Format::Fg::BrightCyan)});
|
|
cap.Render();
|
|
|
|
// Should produce multiple wrap lines, each with sequences
|
|
auto lines = cap.lines();
|
|
VERIFY_IS_GREATER_THAN(lines.size(), static_cast<size_t>(1));
|
|
for (const auto& line : lines)
|
|
{
|
|
// Every line that has content should have the escape sequence reapplied
|
|
if (!line.empty())
|
|
{
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\x1b'));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(TableData_Cell_HyperlinkTruncated_BothSequencesEmitted)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
|
|
// Simulate a hyperlink cell: OSC 8 open + visible text + OSC 8 close.
|
|
// The open/close are ConstructedSequences since they contain a URL.
|
|
const ConstructedSequence hyperlinkOpen{L"\x1b]8;;https://example.com\x1b\\"};
|
|
const ConstructedSequence hyperlinkClose{L"\x1b]8;;\x1b\\"};
|
|
|
|
ColumnWidthConfig configs[2]{};
|
|
configs[0].MaxWidth = 6;
|
|
configs[1].MaxWidth = 8; // Force truncation of "click here now" (14 chars)
|
|
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", configs[0]}, {L"", configs[1]}}, true);
|
|
cap.table.ShowHeader = false;
|
|
|
|
// The cell wraps visible text with hyperlink open/close sequences.
|
|
cap.table.AddRow({L"link", Cell(L"click here now", hyperlinkOpen, hyperlinkClose)});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(1), lines.size());
|
|
const auto& line = lines[0];
|
|
|
|
// The hyperlink open sequence must be present (starts the clickable region)
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L"\x1b]8;;https://example.com\x1b\\"));
|
|
// The hyperlink close sequence must also be present (terminates the clickable region)
|
|
// Find the close AFTER the open
|
|
auto openEnd = line.find(L"\x1b]8;;https://example.com\x1b\\");
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, openEnd);
|
|
auto closePos = line.find(L"\x1b]8;;\x1b\\", openEnd + 1);
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, closePos);
|
|
// Truncation occurred — full text should NOT be present
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, line.find(L"click here now"));
|
|
// Ellipsis should be present
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\u2026'));
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddSpanningRow_BlankLineEmittedBetweenRows)
|
|
{
|
|
TableCapture cap({L"", L""});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"row-a", L"val-a"});
|
|
cap.table.AddSpanningRow();
|
|
cap.table.AddRow({L"row-b", L"val-b"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
// 3 lines: data, blank, data
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
|
|
VERIFY_IS_TRUE(lines[0].find(L"row-a") != std::wstring::npos);
|
|
VERIFY_IS_TRUE(lines[1].empty());
|
|
VERIFY_IS_TRUE(lines[2].find(L"row-b") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddSpanningRow_SectionHeaderEmittedBetweenRows)
|
|
{
|
|
TableCapture cap({L"", L""});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"opt-a", L"desc-a"});
|
|
cap.table.AddSpanningRow(Cell{L"Global Options:"});
|
|
cap.table.AddRow({L"opt-b", L"desc-b"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
|
|
VERIFY_IS_TRUE(lines[0].find(L"opt-a") != std::wstring::npos);
|
|
VERIFY_ARE_EQUAL(std::wstring{L"Global Options:"}, lines[1]);
|
|
VERIFY_IS_TRUE(lines[2].find(L"opt-b") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddSpanningRow_DoesNotAffectColumnWidths)
|
|
{
|
|
// The break text is longer than any data cell; column widths should
|
|
// be driven only by data rows, not breaks.
|
|
TableCapture cap({L"", L""});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddRow({L"ab", L"cd"});
|
|
cap.table.AddSpanningRow(Cell{L"This is a very long section header that should not widen columns"});
|
|
cap.table.AddRow({L"ef", L"gh"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
|
|
// Both data rows should have the same width (driven by "ab"/"ef" column)
|
|
VERIFY_ARE_EQUAL(lines[0].size(), lines[2].size());
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddSpanningRow_SharedColumnWidthsAcrossSections)
|
|
{
|
|
// Data rows in different sections share column widths because they
|
|
// are sized together within a single table instance.
|
|
TableCapture cap({L"", L""});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddSpanningRow(Cell{L"Section A:"});
|
|
cap.table.AddRow({L"short", L"x"});
|
|
cap.table.AddSpanningRow(Cell{L"Section B:"});
|
|
cap.table.AddRow({L"much-longer-name", L"y"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
// 4 lines: header, data, header, data
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(4), lines.size());
|
|
|
|
// "short" row should be padded to match "much-longer-name" column width.
|
|
// Both data rows have the same total width.
|
|
VERIFY_ARE_EQUAL(lines[1].size(), lines[3].size());
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddSpanningRow_CellRendersSequences)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
TableCapture cap(std::vector<ColumnDefinition>{{L"", {}}, {L"", {}}}, true);
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddSpanningRow(Cell{L"Options:", Format::Bright});
|
|
cap.table.AddRow({L"name", L"desc"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), lines.size());
|
|
// The section header should contain VT sequences (Bright + Default reset)
|
|
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
|
|
VERIFY_IS_TRUE(lines[0].find(L"Options:") != std::wstring::npos);
|
|
}
|
|
|
|
TEST_METHOD(TableData_AddSpanningRow_CellStrippedWhenVTDisabled)
|
|
{
|
|
using namespace wsl::windows::common::vt;
|
|
TableCapture cap({L"", L""});
|
|
cap.table.ShowHeader = false;
|
|
|
|
cap.table.AddSpanningRow(Cell{L"Options:", Format::Bright});
|
|
cap.table.AddRow({L"name", L"desc"});
|
|
cap.Render();
|
|
|
|
auto lines = cap.lines();
|
|
VERIFY_ARE_EQUAL(static_cast<size_t>(2), lines.size());
|
|
// VT disabled: no escape sequences, just the text
|
|
VERIFY_ARE_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
|
|
VERIFY_ARE_EQUAL(std::wstring{L"Options:"}, lines[0]);
|
|
}
|
|
};
|
|
|
|
} // namespace WSLCCLITableDataUnitTests
|