master
cpp 1,018 lines 39.4 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLITableOutputUnitTests.cpp
8
9 Abstract:
10
11 Unit tests for the TableOutput class.
12
13 --*/
14
15 #include "precomp.h"
16 #include "windows/Common.h"
17 #include "WSLCCLITestHelpers.h"
18
19 #include "TableOutput.h"
20 #include "VTSupport.h"
21
22 using namespace wsl::windows::wslc;
23 using namespace wsl::windows::common::vt;
24 using namespace WSLCTestHelpers;
25 using namespace WEX::Logging;
26 using namespace WEX::Common;
27 using namespace WEX::TestExecution;
28
29 namespace WSLCCLITableOutputUnitTests {
30
31 class WSLCCLITableOutputUnitTests
32 {
33 WSLC_TEST_CLASS(WSLCCLITableOutputUnitTests)
34
35 TEST_CLASS_SETUP(TestClassSetup)
36 {
37 return true;
38 }
39
40 TEST_CLASS_CLEANUP(TestClassCleanup)
41 {
42 return true;
43 }
44
45 TEST_METHOD(TableOutput_AlwaysShowHeader_EmitsHeaderWhenEmpty)
46 {
47 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
48 cap.table.SetAlwaysShowHeader(true);
49 cap.table.Complete();
50
51 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
52 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
53 VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
54 }
55
56 TEST_METHOD(TableOutput_NoHeader_EmitsNothingWhenEmpty)
57 {
58 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
59 cap.table.SetAlwaysShowHeader(false);
60 cap.table.Complete();
61
62 VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
63 }
64
65 TEST_METHOD(TableOutput_SingleRow_EmitsHeaderPlusOneDataLine)
66 {
67 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
68
69 cap.table.WriteRow({L"my-container", L"running"});
70 cap.table.Complete();
71
72 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
73 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
74 VERIFY_IS_TRUE(cap.lines()[1].find(L"my-container") != std::wstring::npos);
75 VERIFY_IS_TRUE(cap.lines()[1].find(L"running") != std::wstring::npos);
76 }
77
78 TEST_METHOD(TableOutput_MultipleRows_AllRowsEmittedAfterHeader)
79 {
80 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
81
82 cap.table.WriteRow({L"container-a", L"running"});
83 cap.table.WriteRow({L"container-b", L"stopped"});
84 cap.table.WriteRow({L"container-c", L"paused"});
85 cap.table.Complete();
86
87 VERIFY_ARE_EQUAL(static_cast<size_t>(4), cap.lines().size());
88 VERIFY_IS_TRUE(cap.lines()[1].find(L"container-a") != std::wstring::npos);
89 VERIFY_IS_TRUE(cap.lines()[2].find(L"container-b") != std::wstring::npos);
90 VERIFY_IS_TRUE(cap.lines()[3].find(L"container-c") != std::wstring::npos);
91 }
92
93 TEST_METHOD(TableOutput_ColumnPadding_DefaultPaddingApplied)
94 {
95 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/3);
96
97 cap.table.WriteRow({L"abc", L"ok"});
98 cap.table.Complete();
99
100 auto lines = cap.lines();
101 const auto& dataLine = lines[1];
102 VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos);
103 VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos);
104
105 auto columnPadding = 3;
106 auto namePos = dataLine.find(L"abc");
107 auto statusPos = dataLine.find(L"ok");
108 VERIFY_IS_TRUE(statusPos >= namePos + wcslen(L"abc") + columnPadding);
109 }
110
111 TEST_METHOD(TableOutput_ColumnPadding_CustomPaddingApplied)
112 {
113 constexpr size_t customPadding = 5;
114 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"A", L"B"}, /*sizingBuffer=*/50, customPadding);
115
116 cap.table.WriteRow({L"x", L"y"});
117 cap.table.Complete();
118
119 auto lines = cap.lines();
120 const auto& dataLine = lines[1];
121 auto posX = dataLine.find(L'x');
122 auto posY = dataLine.find(L'y');
123 VERIFY_IS_TRUE(posX != std::wstring::npos);
124 VERIFY_IS_TRUE(posY != std::wstring::npos);
125 VERIFY_IS_TRUE(posY >= posX + 1 + customPadding);
126 }
127
128 TEST_METHOD(TableOutput_ColumnWidth_ExpandsToFitData)
129 {
130 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"NAME"});
131
132 cap.table.WriteRow({L"1", L"short"});
133 cap.table.WriteRow({L"2", L"a-very-long-container-name"});
134 cap.table.Complete();
135
136 VERIFY_IS_TRUE(cap.lines()[2].find(L"a-very-long-container-name") != std::wstring::npos);
137 }
138
139 TEST_METHOD(TableOutput_ColumnWidth_AtLeastHeaderWidth)
140 {
141 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"CONTAINER_NAME", L"ST"});
142
143 cap.table.WriteRow({L"abc", L"ok"});
144 cap.table.Complete();
145
146 auto lines = cap.lines();
147 const auto& dataLine = lines[1];
148 auto posOk = dataLine.find(L"ok");
149 VERIFY_IS_TRUE(posOk != std::wstring::npos);
150 VERIFY_IS_TRUE(posOk >= static_cast<size_t>(14 + TableOutput<2>::DefaultColumnPadding));
151 }
152
153 TEST_METHOD(TableOutput_MaxWidth_LongValueIsTruncatedWithEllipsis)
154 {
155 TableOutput<2>::column_config_t configs{};
156 configs[0].MaxWidth = 8;
157 configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
158
159 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
160
161 cap.table.WriteRow({L"a-very-long-name", L"running"});
162 cap.table.Complete();
163
164 auto lines = cap.lines();
165 const auto& dataLine = lines[1];
166 VERIFY_IS_TRUE(dataLine.find(L"\x2026") != std::wstring::npos);
167 VERIFY_IS_TRUE(dataLine.find(L"a-very-long-name") == std::wstring::npos);
168 }
169
170 TEST_METHOD(TableOutput_MaxWidth_ShortValueNotTruncated)
171 {
172 TableOutput<2>::column_config_t configs{};
173 configs[0].MaxWidth = 20;
174 configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
175
176 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, std::move(configs));
177
178 cap.table.WriteRow({L"short", L"running"});
179 cap.table.Complete();
180
181 auto lines = cap.lines();
182 const auto& dataLine = lines[1];
183 VERIFY_IS_TRUE(dataLine.find(L"short") != std::wstring::npos);
184 VERIFY_IS_TRUE(dataLine.find(L"\x2026") == std::wstring::npos);
185 }
186
187 TEST_METHOD(TableOutput_ConsoleWidthLimit_PreferredShrinkColumnIsShrunk)
188 {
189 TableOutput<2>::column_config_t configs{};
190 configs[0].MaxWidth = ColumnWidthConfig::NoLimit;
191 configs[0].Overflow = ColumnOverflow::Shrink;
192 configs[0].PreferredShrink = false;
193 configs[1].MaxWidth = ColumnWidthConfig::NoLimit;
194 configs[1].Overflow = ColumnOverflow::Shrink;
195 configs[1].PreferredShrink = true;
196
197 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"ID", L"DESCRIPTION"}, std::move(configs));
198 cap.table.SetConsoleWidthOverride(20);
199
200 cap.table.WriteRow({L"abc123", L"this-is-a-long-description-value"});
201 cap.table.Complete();
202
203 auto lines = cap.lines();
204 for (const auto& line : lines)
205 {
206 VERIFY_IS_TRUE(line.size() <= static_cast<size_t>(20));
207 }
208 }
209
210 TEST_METHOD(TableOutput_IsEmpty)
211 {
212 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
213 VERIFY_IS_TRUE(cap.table.IsEmpty());
214
215 cap.table.WriteRow({L"foo", L"bar"});
216 VERIFY_IS_FALSE(cap.table.IsEmpty());
217 }
218
219 TEST_METHOD(TableOutput_ColumnDefinition_NameAndConfigUsed)
220 {
221 TableOutput<2>::column_def_t defs{{
222 ColumnDefinition{L"MYID", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = 6, .Overflow = ColumnOverflow::Shrink, .PreferredShrink = false}},
223 ColumnDefinition{L"MYNAME", {.MinWidth = ColumnWidthConfig::NoLimit, .MaxWidth = ColumnWidthConfig::NoLimit}},
224 }};
225
226 TableOutputCapture<2> cap(std::move(defs));
227
228 cap.table.WriteRow({L"id-value", L"name-value"});
229 cap.table.Complete();
230
231 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
232 VERIFY_IS_TRUE(cap.lines()[0].find(L"MYID") != std::wstring::npos);
233 VERIFY_IS_TRUE(cap.lines()[0].find(L"MYNAME") != std::wstring::npos);
234 VERIFY_IS_TRUE(cap.lines()[1].find(L"\x2026") != std::wstring::npos);
235 }
236
237 TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderWithDataRows)
238 {
239 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
240 cap.table.SetShowHeader(false);
241
242 cap.table.WriteRow({L"my-container", L"running"});
243 cap.table.Complete();
244
245 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
246 VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
247 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
248 }
249
250 TEST_METHOD(TableOutput_ShowHeader_False_SuppressesHeaderEvenWhenAlwaysShowHeaderTrue)
251 {
252 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
253 cap.table.SetAlwaysShowHeader(true);
254 cap.table.SetShowHeader(false);
255 cap.table.Complete();
256
257 VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
258 }
259
260 TEST_METHOD(TableOutput_ShowHeader_True_IsDefaultAndEmitsHeader)
261 {
262 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
263
264 cap.table.WriteRow({L"my-container", L"running"});
265 cap.table.Complete();
266
267 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
268 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
269 VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
270 }
271
272 TEST_METHOD(TableOutput_ShowHeader_False_MultipleDataRowsNoHeader)
273 {
274 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
275 cap.table.SetShowHeader(false);
276
277 cap.table.WriteRow({L"container-a", L"running"});
278 cap.table.WriteRow({L"container-b", L"stopped"});
279 cap.table.Complete();
280
281 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
282 VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
283 VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
284 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
285 VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
286 }
287
288 TEST_METHOD(TableOutput_ShowHeader)
289 {
290 {
291 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
292
293 cap.table.WriteRow({L"my-container", L"running"});
294 cap.table.Complete();
295
296 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
297 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") != std::wstring::npos);
298 VERIFY_IS_TRUE(cap.lines()[0].find(L"STATUS") != std::wstring::npos);
299 }
300 {
301 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
302 cap.table.SetShowHeader(false);
303
304 cap.table.WriteRow({L"my-container", L"running"});
305 cap.table.Complete();
306
307 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
308 VERIFY_IS_TRUE(cap.lines()[0].find(L"my-container") != std::wstring::npos);
309 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
310 }
311 {
312 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
313 cap.table.SetAlwaysShowHeader(true);
314 cap.table.SetShowHeader(false);
315 cap.table.Complete();
316
317 VERIFY_ARE_EQUAL(static_cast<size_t>(0), cap.lines().size());
318 }
319 {
320 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
321 cap.table.SetShowHeader(false);
322
323 cap.table.WriteRow({L"container-a", L"running"});
324 cap.table.WriteRow({L"container-b", L"stopped"});
325 cap.table.Complete();
326
327 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
328 VERIFY_IS_TRUE(cap.lines()[0].find(L"container-a") != std::wstring::npos);
329 VERIFY_IS_TRUE(cap.lines()[1].find(L"container-b") != std::wstring::npos);
330 VERIFY_IS_TRUE(cap.lines()[0].find(L"NAME") == std::wstring::npos);
331 VERIFY_IS_TRUE(cap.lines()[1].find(L"NAME") == std::wstring::npos);
332 }
333 }
334
335 TEST_METHOD(TableOutput_RowIndent_PrependedToEveryRow)
336 {
337 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"});
338 cap.table.SetRowIndent(2);
339
340 cap.table.WriteRow({L"abc", L"ok"});
341 cap.table.Complete();
342
343 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
344 VERIFY_ARE_EQUAL(std::wstring{L" NAME STATUS"}, cap.lines()[0]);
345 VERIFY_ARE_EQUAL(std::wstring{L" abc ok"}, cap.lines()[1]);
346 }
347
348 TEST_METHOD(TableOutput_WordWrap_ShortValueProducesOneRow)
349 {
350 TableOutput<2>::column_config_t configs{};
351 configs[0].MaxWidth = 10;
352 configs[1].MaxWidth = 20;
353 configs[1].Overflow = ColumnOverflow::Wrap;
354
355 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
356 cap.table.SetShowHeader(false);
357
358 cap.table.WriteRow({L"opt", L"short desc"});
359 cap.table.Complete();
360
361 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
362 VERIFY_ARE_EQUAL(std::wstring{L"opt short desc"}, cap.lines()[0]);
363 }
364
365 TEST_METHOD(TableOutput_WordWrap_WrapsAtWordBoundary)
366 {
367 TableOutput<2>::column_config_t configs{};
368 configs[0].MaxWidth = 6;
369 configs[1].MaxWidth = 10;
370 configs[1].Overflow = ColumnOverflow::Wrap;
371
372 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
373 cap.table.SetShowHeader(false);
374
375 cap.table.WriteRow({L"opt", L"hello world"});
376 cap.table.Complete();
377
378 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
379 VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
380 VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
381 }
382
383 TEST_METHOD(TableOutput_WordWrap_ContinuationRowHasBlankLeadingColumns)
384 {
385 TableOutput<2>::column_config_t configs{};
386 configs[0].MaxWidth = 6;
387 configs[1].MaxWidth = 10;
388 configs[1].Overflow = ColumnOverflow::Wrap;
389
390 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
391 cap.table.SetShowHeader(false);
392
393 cap.table.WriteRow({L"opt", L"hello world"});
394 cap.table.Complete();
395
396 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
397 VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
398 }
399
400 TEST_METHOD(TableOutput_WordWrap_MultipleWrapsProduceMultipleRows)
401 {
402 TableOutput<2>::column_config_t configs{};
403 configs[0].MaxWidth = 4;
404 configs[1].MaxWidth = 10;
405 configs[1].Overflow = ColumnOverflow::Wrap;
406
407 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
408 cap.table.SetShowHeader(false);
409
410 cap.table.WriteRow({L"opt", L"one two three four"});
411 cap.table.Complete();
412
413 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
414 VERIFY_ARE_EQUAL(std::wstring{L"opt one two"}, cap.lines()[0]);
415 VERIFY_ARE_EQUAL(std::wstring{L" three four"}, cap.lines()[1]);
416 }
417
418 TEST_METHOD(TableOutput_WordWrap_HardBreakWhenNoSpaceFound)
419 {
420 TableOutput<2>::column_config_t configs{};
421 configs[0].MaxWidth = 4;
422 configs[1].MaxWidth = 6;
423 configs[1].Overflow = ColumnOverflow::Wrap;
424
425 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
426 cap.table.SetShowHeader(false);
427
428 cap.table.WriteRow({L"opt", L"abcdefghij"});
429 cap.table.Complete();
430
431 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
432 VERIFY_ARE_EQUAL(std::wstring{L"opt abcdef"}, cap.lines()[0]);
433 VERIFY_ARE_EQUAL(std::wstring{L" ghij"}, cap.lines()[1]);
434 }
435
436 TEST_METHOD(TableOutput_WordWrap_NonWrappingColumnStillTruncates)
437 {
438 TableOutput<2>::column_config_t configs{};
439 configs[0].MaxWidth = 5;
440 // ColumnOverflow::Truncate (default) — truncates
441 configs[1].MaxWidth = 20;
442 configs[1].Overflow = ColumnOverflow::Wrap;
443
444 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
445 cap.table.SetShowHeader(false);
446
447 cap.table.WriteRow({L"toolongname", L"short desc"});
448 cap.table.Complete();
449
450 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
451 VERIFY_ARE_EQUAL(std::wstring{L"tool\u2026 short desc"}, cap.lines()[0]);
452 }
453
454 TEST_METHOD(TableOutput_WordWrap_RowIndentAppliedToAllPhysicalRows)
455 {
456 TableOutput<2>::column_config_t configs{};
457 configs[0].MaxWidth = 4;
458 configs[1].MaxWidth = 8;
459 configs[1].Overflow = ColumnOverflow::Wrap;
460
461 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
462 cap.table.SetShowHeader(false);
463 cap.table.SetRowIndent(2);
464
465 cap.table.WriteRow({L"opt", L"hello world"});
466 cap.table.Complete();
467
468 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
469 VERIFY_ARE_EQUAL(std::wstring{L" opt hello"}, cap.lines()[0]);
470 VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
471 }
472
473 TEST_METHOD(TableOutput_WordWrap_DisabledByDefault_LongTextTruncated)
474 {
475 TableOutput<2>::column_config_t configs{};
476 configs[0].MaxWidth = 4;
477 configs[1].MaxWidth = 8;
478 // ColumnOverflow::Truncate (default) — truncates
479
480 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
481 cap.table.SetShowHeader(false);
482
483 cap.table.WriteRow({L"opt", L"a very long description"});
484 cap.table.Complete();
485
486 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
487 VERIFY_ARE_EQUAL(std::wstring{L"opt a very \u2026"}, cap.lines()[0]);
488 }
489
490 TEST_METHOD(TableOutput_WordWrap_MultipleLogicalRowsEachWrapIndependently)
491 {
492 TableOutput<2>::column_config_t configs{};
493 configs[0].MaxWidth = 6;
494 configs[1].MaxWidth = 10;
495 configs[1].Overflow = ColumnOverflow::Wrap;
496
497 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
498 cap.table.SetShowHeader(false);
499
500 cap.table.WriteRow({L"opt-a", L"hello world"});
501 cap.table.WriteRow({L"opt-b", L"short"});
502 cap.table.Complete();
503
504 VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
505 VERIFY_ARE_EQUAL(std::wstring{L"opt-a hello"}, cap.lines()[0]);
506 VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
507 VERIFY_ARE_EQUAL(std::wstring{L"opt-b short"}, cap.lines()[2]);
508 }
509
510 TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsColBLonger)
511 {
512 TableOutput<2>::column_config_t configs{};
513 configs[0].MaxWidth = 5;
514 configs[0].Overflow = ColumnOverflow::Wrap;
515 configs[1].MaxWidth = 5;
516 configs[1].Overflow = ColumnOverflow::Wrap;
517
518 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
519 cap.table.SetShowHeader(false);
520
521 cap.table.WriteRow({L"ab cd ef", L"one two three"});
522 cap.table.Complete();
523
524 VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
525 VERIFY_ARE_EQUAL(std::wstring{L"ab cd one"}, cap.lines()[0]);
526 VERIFY_ARE_EQUAL(std::wstring{L"ef two"}, cap.lines()[1]);
527 VERIFY_ARE_EQUAL(std::wstring{L" three"}, cap.lines()[2]);
528 }
529
530 TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsColALonger)
531 {
532 TableOutput<2>::column_config_t configs{};
533 configs[0].MaxWidth = 5;
534 configs[0].Overflow = ColumnOverflow::Wrap;
535 configs[1].MaxWidth = 5;
536 configs[1].Overflow = ColumnOverflow::Wrap;
537
538 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
539 cap.table.SetShowHeader(false);
540
541 cap.table.WriteRow({L"one two three", L"ab cd ef"});
542 cap.table.Complete();
543
544 VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
545 VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
546 VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
547 VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
548 }
549
550 TEST_METHOD(TableOutput_WordWrap_TwoWrappingColumnsWithEqualLengths)
551 {
552 TableOutput<2>::column_config_t configs{};
553 configs[0].MaxWidth = 4;
554 configs[0].Overflow = ColumnOverflow::Wrap;
555 configs[1].MaxWidth = 4;
556 configs[1].Overflow = ColumnOverflow::Wrap;
557
558 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
559 cap.table.SetShowHeader(false);
560
561 cap.table.WriteRow({L"aa bb", L"xx yy"});
562 cap.table.Complete();
563
564 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
565 VERIFY_ARE_EQUAL(std::wstring{L"aa xx"}, cap.lines()[0]);
566 VERIFY_ARE_EQUAL(std::wstring{L"bb yy"}, cap.lines()[1]);
567 }
568
569 TEST_METHOD(TableOutput_WordWrap_NonWrappingColumnBetweenTwoWrappingColumns)
570 {
571 TableOutput<3>::column_config_t configs{};
572 configs[0].MaxWidth = 4;
573 // configs[0].Overflow = ColumnOverflow::Truncate (default) — truncates
574 configs[1].MaxWidth = 5;
575 configs[1].Overflow = ColumnOverflow::Wrap;
576 configs[2].MaxWidth = 5;
577 configs[2].Overflow = ColumnOverflow::Wrap;
578
579 TableOutputCapture<3> cap(TableOutput<3>::header_t{L"", L"", L""}, std::move(configs));
580 cap.table.SetShowHeader(false);
581
582 cap.table.WriteRow({L"tag", L"aa bb", L"xx yy zz"});
583 cap.table.Complete();
584
585 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
586 VERIFY_ARE_EQUAL(std::wstring{L"tag aa bb xx yy"}, cap.lines()[0]);
587 VERIFY_ARE_EQUAL(std::wstring{L" zz"}, cap.lines()[1]);
588 }
589
590 TEST_METHOD(TableOutput_WordWrap_FirstColumnLongerThanSecond)
591 {
592 TableOutput<2>::column_config_t configs{};
593 configs[0].MaxWidth = 5;
594 configs[0].Overflow = ColumnOverflow::Wrap;
595 configs[1].MaxWidth = 5;
596 configs[1].Overflow = ColumnOverflow::Wrap;
597
598 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs));
599 cap.table.SetShowHeader(false);
600
601 cap.table.WriteRow({L"one two three", L"ab cd ef"});
602 cap.table.Complete();
603
604 VERIFY_ARE_EQUAL(static_cast<size_t>(3), cap.lines().size());
605 VERIFY_ARE_EQUAL(std::wstring{L"one ab cd"}, cap.lines()[0]);
606 VERIFY_ARE_EQUAL(std::wstring{L"two ef"}, cap.lines()[1]);
607 VERIFY_ARE_EQUAL(std::wstring{L"three "}, cap.lines()[2]);
608 }
609
610 TEST_METHOD(TableOutput_SetColumnConfig_WordWrapAfterConstruction)
611 {
612 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
613 cap.table.SetShowHeader(false);
614 cap.table.SetColumnConfig(
615 1,
616 ColumnWidthConfig{
617 .MaxWidth = 8,
618 .Overflow = ColumnOverflow::Wrap,
619 });
620
621 cap.table.WriteRow({L"opt", L"hello world"});
622 cap.table.Complete();
623
624 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
625 VERIFY_ARE_EQUAL(std::wstring{L"opt hello"}, cap.lines()[0]);
626 VERIFY_ARE_EQUAL(std::wstring{L" world"}, cap.lines()[1]);
627 }
628
629 TEST_METHOD(FormattedCell_VisibleWidth_PlainText)
630 {
631 FormattedCell cell{L"hello"};
632 VERIFY_ARE_EQUAL(static_cast<size_t>(5), cell.VisibleWidth());
633 }
634
635 TEST_METHOD(FormattedCell_VisibleWidth_ExcludesPlaceholders)
636 {
637 FormattedCell cell{L"{}hello{}", {&Format::Fg::BrightRed, &Format::Default}};
638 VERIFY_ARE_EQUAL(static_cast<size_t>(5), cell.VisibleWidth());
639 }
640
641 TEST_METHOD(FormattedCell_Render_ColorEnabled)
642 {
643 FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
644 const auto result = cell.Render(true, true);
645 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
646 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
647 }
648
649 TEST_METHOD(FormattedCell_Render_ColorDisabled)
650 {
651 FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
652 const auto result = cell.Render(false, false);
653 VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
654 }
655
656 TEST_METHOD(FormattedCell_Render_VTEnabled_ColorDisabled_StripsColorSequences)
657 {
658 FormattedCell cell = FormattedCell(L"hello", Format::Fg::BrightRed);
659 // VT on but color off: color sequences should be stripped
660 const auto result = cell.Render(true, false);
661 VERIFY_ARE_EQUAL(std::wstring{L"hello"}, result);
662 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
663 }
664
665 TEST_METHOD(FormattedCell_RenderTruncated_TruncatesVisibleText)
666 {
667 FormattedCell cell = FormattedCell(L"hello world", Format::Fg::BrightRed);
668 const auto result = cell.RenderTruncated(5, true, true);
669 // Should contain truncated visible text + ellipsis + sequences
670 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\x1b'));
671 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
672 }
673
674 TEST_METHOD(TableOutput_FormattedCell_ColumnWidthBasedOnVisibleChars)
675 {
676 using namespace wsl::windows::common::vt;
677
678 TableOutput<2>::column_config_t configs{};
679 configs[0].MaxWidth = 10;
680 configs[1].MaxWidth = 20;
681
682 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
683 cap.table.SetShowHeader(false);
684
685 cap.table.WriteRow({L"opt", FormattedCell(L"hello", Format::Fg::BrightRed)});
686 cap.table.WriteRow({L"end", FormattedCell{L"world"}});
687 cap.table.Complete();
688
689 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
690 // First cell is emphasized (has ESC), second is plain
691 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
692 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
693 VERIFY_ARE_EQUAL(std::wstring{L"end world"}, cap.lines()[1]);
694 }
695
696 TEST_METHOD(TableOutput_FormattedCell_WrapsWithSequencesOnEachChunk)
697 {
698 using namespace wsl::windows::common::vt;
699
700 TableOutput<2>::column_config_t configs{};
701 configs[0].MaxWidth = 6;
702 configs[1].MaxWidth = 8;
703 configs[1].Overflow = ColumnOverflow::Wrap;
704
705 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
706 cap.table.SetShowHeader(false);
707
708 cap.table.WriteRow({L"opt", FormattedCell(L"hello world", Format::Fg::BrightRed)});
709 cap.table.Complete();
710
711 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
712 // Both lines should have emphasis sequences
713 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
714 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
715 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
716 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
717 }
718
719 TEST_METHOD(FormattedCell_DefaultCtor_EmptyCell)
720 {
721 FormattedCell cell;
722 VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
723 VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(true, true));
724 VERIFY_ARE_EQUAL(std::wstring{L""}, cell.Render(false, false));
725 }
726
727 TEST_METHOD(FormattedCell_SingleSequenceCtor_BuildsFormatWithReset)
728 {
729 FormattedCell cell(L"bold", Format::Bright);
730 VERIFY_ARE_EQUAL(static_cast<size_t>(4), cell.VisibleWidth());
731 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cell.sequences.size());
732
733 // Color enabled: sequences emitted
734 const auto rendered = cell.Render(true, true);
735 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L"bold"));
736 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, rendered.find(L'\x1b'));
737
738 // Color disabled: plain text only
739 VERIFY_ARE_EQUAL(std::wstring{L"bold"}, cell.Render(false, false));
740 }
741
742 TEST_METHOD(FormattedCell_VisibleWidth_MultiplePlaceholders)
743 {
744 // "{}a{}bc{}" = 3 visible chars (a, b, c), 3 placeholders
745 FormattedCell cell{L"{}a{}bc{}", {&Format::Bright, &Format::Fg::BrightRed, &Format::Default}};
746 VERIFY_ARE_EQUAL(static_cast<size_t>(3), cell.VisibleWidth());
747 }
748
749 TEST_METHOD(FormattedCell_VisibleWidth_EmptyFormat)
750 {
751 FormattedCell cell{L""};
752 VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
753 }
754
755 TEST_METHOD(FormattedCell_VisibleWidth_OnlyPlaceholders)
756 {
757 FormattedCell cell{L"{}{}", {&Format::Bright, &Format::Default}};
758 VERIFY_ARE_EQUAL(static_cast<size_t>(0), cell.VisibleWidth());
759 }
760
761 TEST_METHOD(FormattedCell_Render_PlainTextNoSequences)
762 {
763 FormattedCell cell{L"plain text"};
764 VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(true, true));
765 VERIFY_ARE_EQUAL(std::wstring{L"plain text"}, cell.Render(false, false));
766 }
767
768 TEST_METHOD(FormattedCell_RenderTruncated_TextFitsNoTruncation)
769 {
770 FormattedCell cell(L"hello", Format::Fg::BrightRed);
771 const auto result = cell.RenderTruncated(10, true, true);
772 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"hello"));
773 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\u2026')); // no ellipsis
774 }
775
776 TEST_METHOD(FormattedCell_RenderTruncated_ColorDisabled_StillTruncates)
777 {
778 FormattedCell cell(L"hello world", Format::Fg::BrightRed);
779 const auto result = cell.RenderTruncated(5, false, false);
780 // Sequences stripped, but truncation still occurs
781 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L'\x1b'));
782 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"world"));
783 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
784 }
785
786 TEST_METHOD(FormattedCell_RenderTruncated_PlainText)
787 {
788 FormattedCell cell{L"abcdefghij"};
789 const auto result = cell.RenderTruncated(5, false, false);
790 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(L"fghij"));
791 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L'\u2026'));
792 }
793
794 TEST_METHOD(TableOutput_FormattedCell_ColorDisabled_SequencesStripped)
795 {
796 using namespace wsl::windows::common::vt;
797
798 TableOutput<2>::column_config_t configs{};
799 configs[0].MaxWidth = 10;
800 configs[1].MaxWidth = 20;
801
802 // vtEnabled=false: sequences should be stripped from output
803 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), false);
804 cap.table.SetShowHeader(false);
805
806 cap.table.WriteRow({L"opt", FormattedCell(L"hello", Format::Fg::BrightRed)});
807 cap.table.Complete();
808
809 VERIFY_ARE_EQUAL(static_cast<size_t>(1), cap.lines().size());
810 // No ESC bytes in output
811 VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
812 // Visible text still present with correct padding
813 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
814 }
815
816 TEST_METHOD(TableOutput_FormattedCell_WrapColorDisabled_SequencesStripped)
817 {
818 using namespace wsl::windows::common::vt;
819
820 TableOutput<2>::column_config_t configs{};
821 configs[0].MaxWidth = 6;
822 configs[1].MaxWidth = 8;
823 configs[1].Overflow = ColumnOverflow::Wrap;
824
825 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), false);
826 cap.table.SetShowHeader(false);
827
828 cap.table.WriteRow({L"opt", FormattedCell(L"hello world", Format::Fg::BrightRed)});
829 cap.table.Complete();
830
831 VERIFY_ARE_EQUAL(static_cast<size_t>(2), cap.lines().size());
832 // No ESC in either line
833 VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[0].find(L'\x1b'));
834 VERIFY_ARE_EQUAL(std::wstring::npos, cap.lines()[1].find(L'\x1b'));
835 // Text still wraps correctly
836 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[0].find(L"hello"));
837 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, cap.lines()[1].find(L"world"));
838 }
839
840 TEST_METHOD(TableOutput_FormattedCell_WrapMultipleLines_SequenceReapplied)
841 {
842 using namespace wsl::windows::common::vt;
843
844 TableOutput<2>::column_config_t configs{};
845 configs[0].MaxWidth = 6;
846 configs[1].MaxWidth = 5;
847 configs[1].Overflow = ColumnOverflow::Wrap;
848
849 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
850 cap.table.SetShowHeader(false);
851
852 // Text that wraps into 3 lines: "aa bb cc" at width 5 -> "aa bb" / "cc"
853 // Actually at width 5: "aa" "bb" "cc" (word-break at spaces)
854 cap.table.WriteRow({L"cmd", FormattedCell(L"aa bb cc dd", Format::Fg::BrightCyan)});
855 cap.table.Complete();
856
857 // Should produce multiple wrap lines, each with sequences
858 auto lines = cap.lines();
859 VERIFY_IS_GREATER_THAN(lines.size(), static_cast<size_t>(1));
860 for (const auto& line : lines)
861 {
862 // Every line that has content should have the escape sequence reapplied
863 if (!line.empty())
864 {
865 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\x1b'));
866 }
867 }
868 }
869
870 TEST_METHOD(TableOutput_FormattedCell_HyperlinkTruncated_BothSequencesEmitted)
871 {
872 using namespace wsl::windows::common::vt;
873
874 // Simulate a hyperlink cell: OSC 8 open + visible text + OSC 8 close.
875 // The open/close are ConstructedSequences since they contain a URL.
876 const ConstructedSequence hyperlinkOpen{L"\x1b]8;;https://example.com\x1b\\"};
877 const ConstructedSequence hyperlinkClose{L"\x1b]8;;\x1b\\"};
878
879 TableOutput<2>::column_config_t configs{};
880 configs[0].MaxWidth = 6;
881 configs[1].MaxWidth = 8; // Force truncation of "click here now" (14 chars)
882
883 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, std::move(configs), true);
884 cap.table.SetShowHeader(false);
885
886 // FormattedCell: {}visible text{} with hyperlink open/close as sequences
887 cap.table.WriteRow({L"link", FormattedCell(L"{}click here now{}", {&hyperlinkOpen, &hyperlinkClose})});
888 cap.table.Complete();
889
890 auto lines = cap.lines();
891 VERIFY_ARE_EQUAL(static_cast<size_t>(1), lines.size());
892 const auto& line = lines[0];
893
894 // The hyperlink open sequence must be present (starts the clickable region)
895 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L"\x1b]8;;https://example.com\x1b\\"));
896 // The hyperlink close sequence must also be present (terminates the clickable region)
897 // Find the close AFTER the open
898 auto openEnd = line.find(L"\x1b]8;;https://example.com\x1b\\");
899 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, openEnd);
900 auto closePos = line.find(L"\x1b]8;;\x1b\\", openEnd + 1);
901 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, closePos);
902 // Truncation occurred — full text should NOT be present
903 VERIFY_ARE_EQUAL(std::wstring::npos, line.find(L"click here now"));
904 // Ellipsis should be present
905 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(L'\u2026'));
906 }
907
908 TEST_METHOD(TableOutput_WriteLine_BlankLineEmittedBetweenRows)
909 {
910 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
911 cap.table.SetShowHeader(false);
912
913 cap.table.WriteRow({L"row-a", L"val-a"});
914 cap.table.WriteLine();
915 cap.table.WriteRow({L"row-b", L"val-b"});
916 cap.table.Complete();
917
918 auto lines = cap.lines();
919 // 3 lines: data, blank, data
920 VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
921 VERIFY_IS_TRUE(lines[0].find(L"row-a") != std::wstring::npos);
922 VERIFY_IS_TRUE(lines[1].empty());
923 VERIFY_IS_TRUE(lines[2].find(L"row-b") != std::wstring::npos);
924 }
925
926 TEST_METHOD(TableOutput_WriteLine_SectionHeaderEmittedBetweenRows)
927 {
928 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
929 cap.table.SetShowHeader(false);
930
931 cap.table.WriteRow({L"opt-a", L"desc-a"});
932 cap.table.WriteLine(FormattedCell{L"Global Options:"});
933 cap.table.WriteRow({L"opt-b", L"desc-b"});
934 cap.table.Complete();
935
936 auto lines = cap.lines();
937 VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
938 VERIFY_IS_TRUE(lines[0].find(L"opt-a") != std::wstring::npos);
939 VERIFY_ARE_EQUAL(std::wstring{L"Global Options:"}, lines[1]);
940 VERIFY_IS_TRUE(lines[2].find(L"opt-b") != std::wstring::npos);
941 }
942
943 TEST_METHOD(TableOutput_WriteLine_DoesNotAffectColumnWidths)
944 {
945 // The break text is longer than any data cell; column widths should
946 // be driven only by data rows, not breaks.
947 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
948 cap.table.SetShowHeader(false);
949
950 cap.table.WriteRow({L"ab", L"cd"});
951 cap.table.WriteLine(FormattedCell{L"This is a very long section header that should not widen columns"});
952 cap.table.WriteRow({L"ef", L"gh"});
953 cap.table.Complete();
954
955 auto lines = cap.lines();
956 VERIFY_ARE_EQUAL(static_cast<size_t>(3), lines.size());
957 // Both data rows should have the same width (driven by "ab"/"ef" column)
958 VERIFY_ARE_EQUAL(lines[0].size(), lines[2].size());
959 }
960
961 TEST_METHOD(TableOutput_WriteLine_SharedColumnWidthsAcrossSections)
962 {
963 // Data rows in different sections share column widths because they
964 // are sized together within a single table instance.
965 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
966 cap.table.SetShowHeader(false);
967
968 cap.table.WriteLine(FormattedCell{L"Section A:"});
969 cap.table.WriteRow({L"short", L"x"});
970 cap.table.WriteLine(FormattedCell{L"Section B:"});
971 cap.table.WriteRow({L"much-longer-name", L"y"});
972 cap.table.Complete();
973
974 auto lines = cap.lines();
975 // 4 lines: header, data, header, data
976 VERIFY_ARE_EQUAL(static_cast<size_t>(4), lines.size());
977
978 // "short" row should be padded to match "much-longer-name" column width.
979 // Both data rows have the same total width.
980 VERIFY_ARE_EQUAL(lines[1].size(), lines[3].size());
981 }
982
983 TEST_METHOD(TableOutput_WriteLine_FormattedCellRendersSequences)
984 {
985 using namespace wsl::windows::common::vt;
986 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""}, TableOutput<2>::column_config_t{}, true);
987 cap.table.SetShowHeader(false);
988
989 cap.table.WriteLine(FormattedCell{L"Options:", Format::Bright});
990 cap.table.WriteRow({L"name", L"desc"});
991 cap.table.Complete();
992
993 auto lines = cap.lines();
994 VERIFY_ARE_EQUAL(static_cast<size_t>(2), lines.size());
995 // The section header should contain VT sequences (Bright + Default reset)
996 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
997 VERIFY_IS_TRUE(lines[0].find(L"Options:") != std::wstring::npos);
998 }
999
1000 TEST_METHOD(TableOutput_WriteLine_FormattedCellStrippedWhenVTDisabled)
1001 {
1002 using namespace wsl::windows::common::vt;
1003 TableOutputCapture<2> cap(TableOutput<2>::header_t{L"", L""});
1004 cap.table.SetShowHeader(false);
1005
1006 cap.table.WriteLine(FormattedCell{L"Options:", Format::Bright});
1007 cap.table.WriteRow({L"name", L"desc"});
1008 cap.table.Complete();
1009
1010 auto lines = cap.lines();
1011 VERIFY_ARE_EQUAL(static_cast<size_t>(2), lines.size());
1012 // VT disabled: no escape sequences, just the text
1013 VERIFY_ARE_EQUAL(std::wstring::npos, lines[0].find(L"\x1b["));
1014 VERIFY_ARE_EQUAL(std::wstring{L"Options:"}, lines[0]);
1015 }
1016 };
1017
1018 } // namespace WSLCCLITableOutputUnitTests