diff --git a/README.md b/README.md index b585413..cab3e2c 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ dotnet test ttui.slnx --configuration Release --no-build dotnet pack src/TinyTUI/TinyTUI.csproj --configuration Release --no-build ``` -`test/TinyTUI.Tests` 是正式 xUnit 回归测试项目。`test/TinyTUI.TextChecks` 和 `test/TinyTUI.ComponentChecks` 是早期最小 smoke check,后续会逐步收敛到正式测试项目。 +`test/TinyTUI.Tests` 是统一的 xUnit 回归测试项目,覆盖文本、输入、overlay、renderer、组件、自动补全和终端图像相关回归。 ## 压力示例 diff --git a/TODO.md b/TODO.md index 3e6aed1..105b81f 100644 --- a/TODO.md +++ b/TODO.md @@ -704,27 +704,34 @@ SettingsList 后续仍需补齐: - 新增 overlay 回归测试,覆盖短内容时 overlay 仍可见、短 overlay 覆盖长底层行时保留右侧内容、overlay 段 reset 隔离 - 新增差分渲染测试,覆盖 bottom viewport、行尾 reset、只重绘变化行、resize/shrink full redraw、硬件光标 marker 定位和 Kitty image 删除顺序 - 新增组件回归测试,覆盖 focusable 光标 marker、slash command autocomplete 确认/取消、Image 组件 Kitty/fallback 和 image line 任意位置识别 +- 将 `TextChecks` 残余断言完全迁移到 `TerminalTextMeasurerTests`,补齐 plain wrap、underline URL wrap、ANSI continuation 和 OSC 8 BEL hyperlink wrap 覆盖 +- 将 `ComponentChecks` 残余断言完全迁移到 `ComponentRegressionTests` 和 `DifferentialRendererTests`,补齐 SelectList focused marker、Spacer 负数 clamp、TruncatedText padding/首行截断、CancellableLoader 取消、PNG dimensions、长 iTerm2 image line overflow bypass +- 从 `ttui.slnx` 移除 `test/TinyTUI.TextChecks` 和 `test/TinyTUI.ComponentChecks`,删除两个早期 console smoke check 项目,README 测试说明改为统一 xUnit 入口 为什么先做: - `tmp/tui/test/virtual-terminal.ts` 的核心价值是让终端行为可自动断言;C# 侧当前没有 xterm.js 等完整终端模拟依赖,本次先用 `ITerminalOutput` 建立轻量 fake output,优先覆盖 renderer 实际写出的 ANSI 序列和尺寸变化 - 前面 TODO 5/6/7/8 已经产生 TextChecks 和 ComponentChecks,继续追加 console check 会让失败定位和 CI 接入变差;正式测试项目能把这些回归按模块拆分,并为第 10 项 CI 打基础 - 本次只固化已实现能力和已修过的关键回归,不把 Kitty 完整协议、真实终端屏幕缓冲或更多组件行为混进同一个 commit,降低测试基础设施增量的风险 +- 旧 smoke check 已经和 xUnit 用例重叠,继续保留会导致同一行为维护两套入口;本次先按旧 `Program.cs` 的有效断言逐项对齐,再删除旧项目,保证边界清晰 当前更好的点: - C# 侧测试辅助直接实现 `ITerminalOutput`,不需要绕过 renderer 或 runtime 私有状态,能观察 `ClearScreen`、`MoveCursorTo`、`HideCursor`、Kitty cleanup 等真实输出 - `VirtualTerminalOutput` 同时保留完整 buffer 和 write 片段,既能断言最终序列,也能检查删除图像必须早于新内容写入这类顺序问题 - 正式测试项目已覆盖 text/input/overlay/rendering/components 五个基础面,后续新增回归时有明确目录,不需要继续把所有断言塞进单个 `Program.cs` +- 相比 console check 手写 `AssertTrue`,xUnit 测试按模块命名,失败时能直接定位到文本、组件或 renderer 的具体行为 +- 现有 C# 写法用 collection expression、target-typed `new` 和 `using var` 保持测试输入短小,断言结构更接近被测行为本身 后续仍需补齐: - 还没有像 `tmp/tui/test/virtual-terminal.ts` 那样基于真实终端 emulator 解析 ANSI 后得到 viewport、scrollback、cell style 和 cursor position;当前只能断言写出的序列,不能验证终端最终屏幕状态 -- 现有 `TextChecks` 和 `ComponentChecks` 暂时保留,后续可以在所有断言迁移完成后删除或改成示例 smoke check,避免同一回归维护两套入口 +- `TextChecks` 和 `ComponentChecks` 已删除,后续新增回归应直接进入 `test/TinyTUI.Tests`,不要再恢复独立 console smoke check - `DefaultInputParser` 当前对 `ESC[Z` Shift+Tab 仍会原样透传,本次测试只覆盖当前支持的修饰 CSI;后续输入协议增量应补正式回归 - `StdinBuffer` 还没有参考实现中的超时 flush、ESC+ESC+CSI 分裂、Kitty printable duplicate drop、鼠标协议和旧式 mouse sequence 测试 - overlay 测试目前通过 `OverlayManager.Compose` 断言合成结果,还没有跑完整 Runtime + Renderer + virtual terminal 的端到端路径,多层 overlay 焦点恢复和 viewport 对齐仍需继续补 - 渲染测试尚未覆盖 synchronized output、debug 日志、Termux resize 策略、硬件光标相对移动和完整 Kitty reserved row redraw 行为 +- 图像尺寸目前只把旧 smoke check 的 PNG 样本迁入正式测试;参考 `tmp/tui/test/terminal-image.test.ts` 的大量协议负例、环境能力检测和 WebP/JPEG/GIF 样本仍建议后续拆成专门测试类 ### 10. 工程化和发布完整性 diff --git a/test/TinyTUI.ComponentChecks/Program.cs b/test/TinyTUI.ComponentChecks/Program.cs deleted file mode 100644 index 5c0a2ad..0000000 --- a/test/TinyTUI.ComponentChecks/Program.cs +++ /dev/null @@ -1,160 +0,0 @@ -using TinyTUI; -using TinyTUI.Components; -using TinyTUI.Autocomplete; -using TinyTUI.Input; -using TinyTUI.Rendering; -using TinyTUI.Stdout; -using TinyTUI.Terminal.Images; -using TinyTUI.Text; -using System.Text; - -var measurer = new TerminalTextMeasurer(); - -var input = new Input(measurer) { Prompt = "> " }; -AssertFalse(input.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "unfocused input marker"); -input.Focused = true; -AssertTrue(input.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "focused input marker"); - -var editor = new Editor(measurer) { Placeholder = "type here" }; -AssertFalse(editor.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "unfocused editor marker"); -editor.Focused = true; -AssertTrue(editor.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "focused editor marker"); - -var list = new SelectList(measurer); -list.SetItems(["alpha", "beta"]); -AssertFalse(list.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "unfocused select marker"); -list.Focused = true; -AssertTrue(list.Render(20)[0].Contains(CursorMarker.Marker, StringComparison.Ordinal), "focused select marker"); - -AssertEqual(3, new Spacer(3).Render(10).Count, "spacer line count"); -AssertEqual(0, new Spacer(-1).Render(10).Count, "spacer clamps negative lines"); - -var truncated = new TruncatedText("abcdef\nignored", measurer) -{ - PaddingX = 1, - PaddingY = 1, -}; -var truncatedLines = truncated.Render(6); -AssertEqual(3, truncatedLines.Count, "truncated text vertical padding"); -AssertEqual(6, measurer.GetWidth(truncatedLines[1]), "truncated text padded width"); -AssertTrue(truncatedLines[1].Contains("abc", StringComparison.Ordinal), "truncated text first line only"); - -var loader = new CancellableLoader(keybindings: KeybindingRegistry.CreateDefault()); -var canceled = false; -loader.OnCanceled = () => canceled = true; -loader.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape)); -AssertTrue(loader.IsCanceled, "cancellable loader token"); -AssertTrue(canceled, "cancellable loader callback"); - -var autocompleteEditor = new Editor(measurer) -{ - AutocompleteProvider = new SlashCommandAutocompleteProvider( - [ - new SlashCommand("help", "show help"), - new SlashCommand("history", "show history"), - ]), -}; -autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Text, "/h")); -AssertTrue(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete active"); -AssertTrue(autocompleteEditor.RenderAutocomplete(40).Any(line => line.Contains("help", StringComparison.Ordinal)), "slash command autocomplete render"); -autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Down)); -autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab)); -AssertEqual("/history ", autocompleteEditor.Value, "slash command autocomplete confirm"); -AssertFalse(autocompleteEditor.IsAutocompleteActive, "slash command autocomplete closes after confirm"); - -autocompleteEditor.Value = "/h"; -autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Tab)); -AssertTrue(autocompleteEditor.IsAutocompleteActive, "forced autocomplete active"); -autocompleteEditor.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape)); -AssertFalse(autocompleteEditor.IsAutocompleteActive, "autocomplete cancel"); -AssertEqual("/h", autocompleteEditor.Value, "autocomplete cancel keeps text"); - -var imageService = new TerminalImageService(); -imageService.SetCapabilities(new TerminalCapabilities(ImageProtocol.Kitty, true, true)); -imageService.SetCellDimensions(new CellDimensions(10, 10)); -var cellSize = imageService.CalculateCellSize(new ImageDimensions(20, 20), 2); -AssertEqual(new ImageCellSize(2, 2), cellSize, "image cell size"); - -var pngData = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAFElEQVR4nGNgYGBgYGBgYGBgAAAABQABJzQnCgAAAABJRU5ErkJggg=="); -AssertEqual(new ImageDimensions(2, 3), imageService.TryGetDimensions(pngData, "image/png"), "png dimensions"); - -var image = new Image([1, 2, 3, 4], "image/png", imageService: imageService, dimensions: new ImageDimensions(20, 20)) -{ - MaxWidthCells = 2, -}; -var imageLines = image.Render(4); -AssertEqual(2, imageLines.Count, "kitty image line count"); -AssertTrue(image.ImageId is > 0, "kitty image id allocated"); -AssertTrue(imageLines[0].StartsWith("\e_G", StringComparison.Ordinal), "kitty image sequence"); -AssertTrue(imageLines[0].Contains(",C=1,", StringComparison.Ordinal), "kitty image no cursor movement"); -AssertTrue(TerminalImageService.IsImageLine($"prefix {imageLines[0]} suffix"), "image line detection anywhere"); - -imageService.SetCapabilities(TerminalCapabilities.Conservative); -var fallback = new Image([1, 2, 3], "image/jpeg", imageService: imageService, dimensions: new ImageDimensions(8, 9)) -{ - FileName = "photo.jpg", -}.Render(80)[0]; -AssertTrue(fallback.Contains("[Image: photo.jpg [image/jpeg] 8x9]", StringComparison.Ordinal), "image fallback"); - -var fakeOutput = new FakeTerminalOutput(); -var renderer = new DifferentialRenderer( - fakeOutput, - measurer, - new RenderPipelineOptions { ThrowOnWidthOverflow = true, UseSynchronizedOutput = false }); -var longImageLine = $"Read image file \e]1337;File=inline=1:{new string('A', 200)}\a"; -renderer.Render([longImageLine], new TerminalSize(20, 5)); -renderer.Render(["plain"], new TerminalSize(20, 5)); - -fakeOutput.Buffer.Clear(); -var kittyLine = "\e_Ga=T,f=100,q=2,C=1,c=1,r=1,i=42;AAAA\e\\"; -renderer.Render([kittyLine], new TerminalSize(20, 5)); -fakeOutput.Buffer.Clear(); -renderer.Render(["changed"], new TerminalSize(20, 5)); -AssertTrue(fakeOutput.Buffer.ToString().Contains(TerminalImageService.DeleteKittyImage(42), StringComparison.Ordinal), "kitty image cleanup on diff"); - -Console.WriteLine("TinyTUI component checks passed"); - -static void AssertEqual(T expected, T actual, string name) -{ - if (!EqualityComparer.Default.Equals(expected, actual)) - throw new InvalidOperationException($"{name}: expected '{expected}', actual '{actual}'"); -} - -static void AssertTrue(bool condition, string name) -{ - if (!condition) - throw new InvalidOperationException($"{name}: assertion failed"); -} - -static void AssertFalse(bool condition, string name) => AssertTrue(!condition, name); - -/// -/// 捕获渲染器写出的终端序列 -/// -internal sealed class FakeTerminalOutput : ITerminalOutput -{ - /// - /// 获取已写出的终端缓冲 - /// - public StringBuilder Buffer { get; } = new(); - - /// - public void Write(string value) => Buffer.Append(value); - - /// - public void Flush() - { - } - - /// - public void ClearScreen() => Buffer.Append("\e[2J\e[H"); - - /// - public void HideCursor() => Buffer.Append("\e[?25l"); - - /// - public void ShowCursor() => Buffer.Append("\e[?25h"); - - /// - public void MoveCursorTo(int row, int column) => Buffer.Append($"\e[{row};{column}H"); -} diff --git a/test/TinyTUI.ComponentChecks/TinyTUI.ComponentChecks.csproj b/test/TinyTUI.ComponentChecks/TinyTUI.ComponentChecks.csproj deleted file mode 100644 index 12a074a..0000000 --- a/test/TinyTUI.ComponentChecks/TinyTUI.ComponentChecks.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Exe - net10.0 - enable - enable - - - diff --git a/test/TinyTUI.Tests/Components/ComponentRegressionTests.cs b/test/TinyTUI.Tests/Components/ComponentRegressionTests.cs index 61bc2e0..7cbd2bc 100644 --- a/test/TinyTUI.Tests/Components/ComponentRegressionTests.cs +++ b/test/TinyTUI.Tests/Components/ComponentRegressionTests.cs @@ -23,6 +23,46 @@ public sealed class ComponentRegressionTests Assert.DoesNotContain(CursorMarker.Marker, editor.Render(20)[0], StringComparison.Ordinal); editor.Focused = true; Assert.Contains(CursorMarker.Marker, editor.Render(20)[0], StringComparison.Ordinal); + + var list = new SelectList(_measurer); + list.SetItems(["alpha", "beta"]); + Assert.DoesNotContain(CursorMarker.Marker, list.Render(20)[0], StringComparison.Ordinal); + list.Focused = true; + Assert.Contains(CursorMarker.Marker, list.Render(20)[0], StringComparison.Ordinal); + } + + [Fact] + public void BasicLayoutComponentsClampAndPadRenderedLines() + { + Assert.Equal(3, new Spacer(3).Render(10).Count); + Assert.Empty(new Spacer(-1).Render(10)); + + var truncated = new TruncatedText("abcdef\nignored", _measurer) + { + PaddingX = 1, + PaddingY = 1, + }; + var lines = truncated.Render(6); + + Assert.Equal(3, lines.Count); + Assert.Equal(6, _measurer.GetWidth(lines[1])); + // TruncatedText 是单行组件 这里确认换行后的内容不会泄漏到渲染结果 + Assert.Contains("abc", lines[1], StringComparison.Ordinal); + Assert.DoesNotContain("ignored", string.Concat(lines), StringComparison.Ordinal); + } + + [Fact] + public void CancellableLoaderCancelsTokenAndRunsCallback() + { + using var loader = new CancellableLoader(keybindings: KeybindingRegistry.CreateDefault()); + var canceled = false; + loader.OnCanceled = () => canceled = true; + + loader.HandleInput(new TuiInputEvent(TuiInputEventKind.Key, KeyNames.Escape)); + + Assert.True(loader.IsCanceled); + Assert.True(loader.Token.IsCancellationRequested); + Assert.True(canceled); } [Fact] @@ -60,6 +100,7 @@ public sealed class ComponentRegressionTests imageService.SetCapabilities(new TerminalCapabilities(ImageProtocol.Kitty, true, true)); imageService.SetCellDimensions(new CellDimensions(10, 10)); Assert.Equal(new ImageCellSize(2, 2), imageService.CalculateCellSize(new ImageDimensions(20, 20), 2)); + Assert.Equal(new ImageDimensions(2, 3), imageService.TryGetDimensions(Png2x3, "image/png")); var image = new Image([1, 2, 3, 4], "image/png", imageService: imageService, dimensions: new ImageDimensions(20, 20)) { @@ -80,4 +121,7 @@ public sealed class ComponentRegressionTests }.Render(80)[0]; Assert.Contains("[Image: photo.jpg [image/jpeg] 8x9]", fallback, StringComparison.Ordinal); } + + private static readonly byte[] Png2x3 = Convert.FromBase64String( + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAFElEQVR4nGNgYGBgYGBgYGBgAAAABQABJzQnCgAAAABJRU5ErkJggg=="); } diff --git a/test/TinyTUI.Tests/Rendering/DifferentialRendererTests.cs b/test/TinyTUI.Tests/Rendering/DifferentialRendererTests.cs index dde3477..f46a7c3 100644 --- a/test/TinyTUI.Tests/Rendering/DifferentialRendererTests.cs +++ b/test/TinyTUI.Tests/Rendering/DifferentialRendererTests.cs @@ -100,6 +100,20 @@ public sealed class DifferentialRendererTests Assert.True(deleteIndex < changedIndex); } + [Fact] + public void LongITerm2ImageLineBypassesTextWidthOverflow() + { + var output = new VirtualTerminalOutput(20, 5); + var renderer = CreateRenderer(output); + var imageLine = $"Read image file \e]1337;File=inline=1:{new string('A', 200)}\a"; + + // 图像协议本身可能很长 但渲染器应按图像行处理而不是按可见文本宽度报错 + renderer.Render([imageLine], output.Size); + renderer.Render(["plain"], output.Size); + + Assert.Contains("plain", output.Buffer.ToString(), StringComparison.Ordinal); + } + private static DifferentialRenderer CreateRenderer(VirtualTerminalOutput output) => new(output, new TerminalTextMeasurer(), TestOptions); } diff --git a/test/TinyTUI.Tests/Text/TerminalTextMeasurerTests.cs b/test/TinyTUI.Tests/Text/TerminalTextMeasurerTests.cs index 97efa48..63e65ad 100644 --- a/test/TinyTUI.Tests/Text/TerminalTextMeasurerTests.cs +++ b/test/TinyTUI.Tests/Text/TerminalTextMeasurerTests.cs @@ -45,10 +45,19 @@ public sealed class TerminalTextMeasurerTests [Fact] public void WrapRestoresAnsiAndOsc8AcrossLines() { + var plainWrapped = _measurer.Wrap("hello world this is a test", 10); + Assert.True(plainWrapped.Count > 1); + Assert.All(plainWrapped, line => Assert.True(_measurer.GetWidth(line) <= 10)); + var redWrapped = _measurer.Wrap($"\e[31mhello world this is red\e[0m", 10); Assert.All(redWrapped.Skip(1), line => Assert.StartsWith("\e[31m", line, StringComparison.Ordinal)); Assert.All(redWrapped.Take(redWrapped.Count - 1), line => Assert.False(line.EndsWith("\e[0m", StringComparison.Ordinal))); + var underlinedUrl = _measurer.Wrap($"read this thread \e[4mhttps://example.com/very/long/path/that/will/definitely/wrap\e[24m", 40); + Assert.Equal("read this thread", underlinedUrl[0]); + Assert.StartsWith("\e[4m", underlinedUrl[1], StringComparison.Ordinal); + Assert.Contains(underlinedUrl.Take(underlinedUrl.Count - 1), line => line.EndsWith("\e[24m", StringComparison.Ordinal)); + var hyperlink = "\e]8;;https://example.com\a0123456789\e]8;;\a"; var hyperlinkWrapped = _measurer.Wrap(hyperlink, 6); Assert.True(hyperlinkWrapped.Count > 1); diff --git a/test/TinyTUI.TextChecks/Program.cs b/test/TinyTUI.TextChecks/Program.cs deleted file mode 100644 index 125bda2..0000000 --- a/test/TinyTUI.TextChecks/Program.cs +++ /dev/null @@ -1,69 +0,0 @@ -using TinyTUI.Text; - -var measurer = new TerminalTextMeasurer(); - -AssertEqual(5, measurer.GetWidth("\t\e[31m界\e[0m"), "tab and ansi width"); -AssertEqual(2, measurer.GetWidth("🇨"), "single regional indicator width"); -AssertEqual(2, measurer.GetWidth("🇨🇳"), "regional indicator pair width"); -AssertEqual(2, measurer.GetWidth("👨‍💻"), "zwj emoji width"); -AssertEqual(2, measurer.GetWidth("⚡️"), "variation selector emoji width"); - -var strictTabSlice = measurer.Slice("out 192M\t.pi/skill-tests/results-ha", 0, 10, strict: true); -AssertEqual("out 192M", strictTabSlice.Text, "strict tab slice text"); -AssertEqual(8, strictTabSlice.Width, "strict tab slice width"); -AssertEqual(measurer.GetWidth(strictTabSlice.Text), strictTabSlice.Width, "strict tab slice measured width"); - -var afterTabSlice = measurer.Slice("out 192M\t.pi/skill-tests/results-ha", 13, 10, strict: true); -AssertEqual("i/skill-te", afterTabSlice.Text, "slice after tab text"); -AssertEqual(measurer.GetWidth(afterTabSlice.Text), afterTabSlice.Width, "slice after tab measured width"); - -var ansiText = $"\e[31m{"hello ".Repeat(100)}\e[0m"; -var truncated = measurer.Truncate(ansiText, 20, "…"); -AssertTrue(measurer.GetWidth(truncated) <= 20, "ansi truncate width"); -AssertTrue(truncated.Contains("\e[31m", StringComparison.Ordinal), "ansi truncate keeps style prefix"); -AssertTrue(truncated.EndsWith("\e[0m…\e[0m", StringComparison.Ordinal), "ansi truncate brackets ellipsis"); - -var wideEllipsis = measurer.Truncate("abcdef", 2, "🙂"); -AssertEqual("\e[0m🙂\e[0m", wideEllipsis, "wide ellipsis clipping"); -AssertEqual(string.Empty, measurer.Truncate("abcdef", 1, "🙂"), "wide ellipsis too narrow"); - -var plainWrapped = measurer.Wrap("hello world this is a test", 10); -AssertTrue(plainWrapped.Count > 1, "plain wrap line count"); -AssertTrue(plainWrapped.All(line => measurer.GetWidth(line) <= 10), "plain wrap width"); - -var redWrapped = measurer.Wrap($"\e[31mhello world this is red\e[0m", 10); -AssertTrue(redWrapped.Skip(1).All(line => line.StartsWith("\e[31m", StringComparison.Ordinal)), "ansi wrap restores red"); -AssertTrue(redWrapped.Take(redWrapped.Count - 1).All(line => !line.EndsWith("\e[0m", StringComparison.Ordinal)), "ansi wrap avoids full reset before final"); - -var underlinedUrl = measurer.Wrap($"read this thread \e[4mhttps://example.com/very/long/path/that/will/definitely/wrap\e[24m", 40); -AssertEqual("read this thread", underlinedUrl[0], "underline wrap keeps prefix unstyled"); -AssertTrue(underlinedUrl[1].StartsWith("\e[4m", StringComparison.Ordinal), "underline wrap starts style on url line"); -AssertTrue(underlinedUrl.Take(underlinedUrl.Count - 1).Any(line => line.EndsWith("\e[24m", StringComparison.Ordinal)), "underline wrap closes line"); - -var hyperlink = "\e]8;;https://example.com\a0123456789\e]8;;\a"; -var hyperlinkWrapped = measurer.Wrap(hyperlink, 6); -AssertTrue(hyperlinkWrapped.Count > 1, "osc8 wrap line count"); -AssertTrue(hyperlinkWrapped.All(line => line.Contains("\e]8;;https://example.com\a", StringComparison.Ordinal)), "osc8 wrap reopens hyperlink with bel"); -AssertTrue(hyperlinkWrapped.Take(hyperlinkWrapped.Count - 1).All(line => line.EndsWith("\e]8;;\a", StringComparison.Ordinal)), "osc8 wrap closes hyperlink with bel"); - -Console.WriteLine("TinyTUI text checks passed"); - -static void AssertEqual(T expected, T actual, string name) -{ - if (!EqualityComparer.Default.Equals(expected, actual)) - throw new InvalidOperationException($"{name}: expected '{expected}', actual '{actual}'"); -} - -static void AssertTrue(bool condition, string name) -{ - if (!condition) - throw new InvalidOperationException($"{name}: assertion failed"); -} - -internal static class StringExtensions -{ - /// - /// 重复字符串用于构造长文本测试输入 - /// - public static string Repeat(this string value, int count) => string.Concat(Enumerable.Repeat(value, count)); -} diff --git a/test/TinyTUI.TextChecks/TinyTUI.TextChecks.csproj b/test/TinyTUI.TextChecks/TinyTUI.TextChecks.csproj deleted file mode 100644 index 12a074a..0000000 --- a/test/TinyTUI.TextChecks/TinyTUI.TextChecks.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Exe - net10.0 - enable - enable - - - diff --git a/ttui.slnx b/ttui.slnx index 22031b7..931c1a2 100644 --- a/ttui.slnx +++ b/ttui.slnx @@ -10,7 +10,5 @@ - -