using TinyTUI.Rendering; using TinyTUI.Terminal.Images; using TinyTUI.Tests.TestInfrastructure; using TinyTUI.Text; namespace TinyTUI.Tests.Rendering; public sealed class DifferentialRendererTests { private static readonly RenderPipelineOptions TestOptions = new() { ThrowOnWidthOverflow = true, UseSynchronizedOutput = false, }; [Fact] public void FirstRenderCapturesVisibleViewportAndLineResets() { var output = new VirtualTerminalOutput(20, 3); var renderer = CreateRenderer(output); renderer.Render(["Line 0", "Line 1", "Line 2", "Line 3"], output.Size); Assert.Equal(1, renderer.FullRedrawCount); Assert.Equal(new RenderViewportState(1, 3, 4, 4), renderer.ViewportState); Assert.Contains("Line 1\e[0m\e]8;;\a", output.Buffer.ToString(), StringComparison.Ordinal); Assert.DoesNotContain("Line 0", output.Buffer.ToString(), StringComparison.Ordinal); } [Fact] public void DiffRenderOnlyMovesAndClearsChangedRows() { var output = new VirtualTerminalOutput(20, 4); var renderer = CreateRenderer(output); renderer.Render(["Header", "Working...", "Footer"], output.Size); output.ClearWrites(); renderer.Render(["Header", "Working /", "Footer"], output.Size); Assert.Equal(1, renderer.DifferentialRedrawCount); Assert.Contains("\e[2;1H\e[2KWorking /", output.Buffer.ToString(), StringComparison.Ordinal); Assert.DoesNotContain("\e[1;1H\e[2KHeader", output.Buffer.ToString(), StringComparison.Ordinal); } [Fact] public void ResizeAndShrinkForceFullRedrawsToClearStaleRows() { var output = new VirtualTerminalOutput(20, 5); var renderer = CreateRenderer(output); renderer.Render(["Line 0", "Line 1", "Line 2", "Line 3"], output.Size); output.Resize(20, 6); output.ClearWrites(); renderer.Render(["Line 0", "Line 1", "Line 2", "Line 3"], output.Size); Assert.Equal(2, renderer.FullRedrawCount); Assert.True(output.Contains("\e[2J\e[H")); output.ClearWrites(); renderer.Render(["Line 0"], output.Size); Assert.Equal(3, renderer.FullRedrawCount); Assert.True(output.Contains("\e[2J\e[H")); } [Fact] public void CursorMarkerMovesHardwareCursorAndCanShowCursor() { var output = new VirtualTerminalOutput(20, 4); var renderer = new DifferentialRenderer( output, new TerminalTextMeasurer(), new RenderPipelineOptions { ThrowOnWidthOverflow = true, UseSynchronizedOutput = false, ShowHardwareCursor = true, }); renderer.Render([$"ab{CursorMarker.Marker}cd"], output.Size); Assert.True(output.Contains("\e[1;3H")); Assert.True(output.Contains("\e[?25h")); } [Fact] public void ChangedKittyImageIsDeletedBeforeReplacementLine() { var output = new VirtualTerminalOutput(20, 4); var renderer = CreateRenderer(output); var kittyLine = "\e_Ga=T,f=100,q=2,C=1,c=1,r=1,i=42;AAAA\e\\"; renderer.Render([kittyLine], output.Size); output.ClearWrites(); renderer.Render(["changed"], output.Size); var writes = output.Buffer.ToString(); var deleteIndex = writes.IndexOf(TerminalImageService.DeleteKittyImage(42), StringComparison.Ordinal); var changedIndex = writes.IndexOf("changed", StringComparison.Ordinal); Assert.True(deleteIndex >= 0); Assert.True(changedIndex >= 0); 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); }