using XFESystem.Core;
using XFESystem.Desktop;
using Xunit;
namespace XFESystem.Desktop.Tests;
/// <summary>Desktop ownership/input regressions. The deliberate blocking backend
/// throws on any accidental UI-thread parse or navigation.</summary>
public sealed class MarkdownDocumentModeTests
{
[Fact]
public void MarkdownOpensReadOnlyPreviewAndQueuesItsActualDocumentWithoutUiNavigation()
{
var f = new Fixture(); var window = f.Open();
Assert.Equal(MarkdownViewMode.Preview, window.MarkdownView);
Assert.Null(window.BrowserSession); Assert.True(window.IsPreviewPreparing);
var operation = Assert.Single(f.Service.Operations);
Assert.Equal(EditorPreparationKind.Document, operation.Request.Kind);
Assert.Equal(Fixture.Source, operation.Request.Source); Assert.True(operation.Request.Dark);
f.Desktop.Key(DesktopKey.Character, '!');
Assert.Equal(Fixture.Source, window.Content); Assert.False(window.IsDirty);
var files = f.Desktop.Open(DesktopApplication.Files);
f.Desktop.Render(); Assert.Same(files, f.Desktop.ActiveWindow);
Assert.Equal(0, f.Backend.Calls);
}
[Fact]
public void TopRightEditAndSplitButtonsPreserveUnsavedSourceAndKeepPagesIndependent()
{
var f = new Fixture(); var window = f.Open();
var preview = f.CompleteLast();
f.Click(window.Bounds.X + window.Bounds.Width - 50, window.Bounds.Y + 49);
Assert.Equal(MarkdownViewMode.Edit, window.MarkdownView);
Assert.Same(preview, window.PreviewSession);
Assert.Equal(EditorPreparationKind.Editor, f.Service.Operations[^1].Request.Kind);
// Loading the full editor does not stop the responsive source editor.
f.Desktop.Key(DesktopKey.Character, '!');
Assert.Equal("!" + Fixture.Source, window.Content);
Assert.True(window.IsDirty);
f.Click(window.Bounds.X + window.Bounds.Width - 148, window.Bounds.Y + 49);
Assert.Equal(MarkdownViewMode.Split, window.MarkdownView);
Assert.Equal("!" + Fixture.Source, window.Content);
Assert.Same(preview, window.PreviewSession);
Assert.Equal(Fixture.Source, f.Storage.ReadText(window.Location));
f.Desktop.Key(DesktopKey.Save);
Assert.Equal("!" + Fixture.Source, f.Storage.ReadText(window.Location));
Assert.False(window.IsDirty); Assert.Equal(0, f.Backend.Calls);
}
[Fact]
public void SplitPanesUseIndependentPaintBoundsScrollOffsetsAndPointerCoordinatesAfterResize()
{
var f = new Fixture(); var window = f.Open(); var preview = f.CompleteLast();
f.Desktop.SetMarkdownView(window.Id, MarkdownViewMode.Split);
var editor = f.CompleteLast();
f.Desktop.Render();
var a = Assert.Single(editor.Paints).Bounds; var b = Assert.Single(preview.Paints).Bounds;
Assert.True(a.X + a.Width < b.X); Assert.Equal(a.Y, b.Y); Assert.Equal(a.Height, b.Height);
f.Click(b.X + 24, b.Y + 31);
var click = Assert.Single(preview.Inputs, input => input.Type == "mousedown");
Assert.Equal(24, click.X); Assert.Equal(31, click.Y);
Assert.DoesNotContain(editor.Inputs, input => input.Type == "mousedown");
f.Desktop.Pointer(b.X + 24, b.Y + 31, false, wheel: -1);
f.Desktop.Render();
Assert.Equal(60, preview.Paints[^1].Scroll); Assert.Equal(0, editor.Paints[^1].Scroll);
int oldWidth = a.Width;
f.Desktop.ToggleMaximize(window.Id); f.Desktop.Render();
Assert.True(editor.Paints[^1].Bounds.Width > oldWidth);
Assert.Equal((editor.Paints[^1].Bounds.Width, editor.Paints[^1].Bounds.Height), editor.Viewports[^1]);
Assert.Equal((preview.Paints[^1].Bounds.Width, preview.Paints[^1].Bounds.Height), preview.Viewports[^1]);
}
[Fact]
public void SplitPreviewDebouncesTypingAndRejectsAnOutdatedPreparedDocument()
{
var f = new Fixture(); var window = f.Open(); var originalPreview = f.CompleteLast();
f.Desktop.SetMarkdownView(window.Id, MarkdownViewMode.Split);
var editor = f.CompleteLast();
editor.EditorText = "# First edit"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(100);
f.Desktop.AdvanceFeedback(200);
Assert.Equal(2, f.Service.Operations.Count);
editor.EditorText = "# Latest edit"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(240); f.Desktop.AdvanceFeedback(400);
Assert.Equal(2, f.Service.Operations.Count);
f.Desktop.AdvanceFeedback(600);
var pending = f.Service.Operations[^1];
Assert.Equal(3, f.Service.Operations.Count); Assert.Equal("# Latest edit", pending.Request.Source);
editor.EditorText = "# During parse"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(650);
var stale = new Page(); pending.Complete(stale);
f.Desktop.AdvanceFeedback(670);
Assert.True(stale.Released); Assert.Same(originalPreview, window.PreviewSession);
f.Desktop.AdvanceFeedback(1000);
Assert.Equal("# During parse", f.Service.Operations[^1].Request.Source);
var latest = f.CompleteLast(1020);
Assert.True(originalPreview.Released); Assert.Same(latest, window.PreviewSession);
Assert.Equal(Fixture.Source, f.Storage.ReadText(window.Location));
}
[Fact]
public void ClosingTheDocumentCancelsPendingPreparationAndReleasesBothPaneOwners()
{
var f = new Fixture(); var window = f.Open(); var preview = f.CompleteLast();
f.Desktop.SetMarkdownView(window.Id, MarkdownViewMode.Split);
var editor = f.CompleteLast();
editor.EditorText = "# Changed"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(200); f.Desktop.AdvanceFeedback(400); f.Desktop.AdvanceFeedback(600);
f.Desktop.Key(DesktopKey.Save); Assert.False(window.IsDirty);
var pending = f.Service.Operations[^1];
Assert.Equal(EditorPreparationKind.Document, pending.Request.Kind);
f.Desktop.Close(window.Id);
Assert.True(pending.Cancelled); Assert.True(editor.Released); Assert.True(preview.Released);
Assert.Empty(f.Desktop.Windows);
}
[Fact]
public void DefaultBrowserQueuesTheRealBingUrlWithoutBlockingDesktopNavigation()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
var operation = Assert.Single(f.Service.Operations);
Assert.Equal("https://www.bing.com/", operation.Request.Path);
Assert.Equal(EditorPreparationKind.Navigation, operation.Request.Kind);
Assert.Null(window.BrowserSession); f.Desktop.Render(); Assert.Equal(0, f.Backend.Calls);
var page = new Page { Current = new("https://www.bing.com/", "Bing", "Actual response") };
operation.Complete(page); f.Desktop.AdvanceFeedback(20);
Assert.Same(page, window.BrowserSession); Assert.Equal("Browser - Bing", window.Title);
Assert.Equal("Actual response", window.Content);
}
[Fact]
public void FailedBackgroundNavigationCannotReportThatThePageLoaded()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
var operation = Assert.Single(f.Service.Operations);
operation.Complete(new Page { Current = new("https://www.bing.com/", "Navigation failed", "HTTPS is unavailable") });
f.Desktop.AdvanceFeedback(20);
Assert.Equal("Page could not be loaded", window.Status);
Assert.DoesNotContain("loaded in background", window.Status, StringComparison.OrdinalIgnoreCase);
}
private sealed class Fixture
{
public const string Source = "# Original document\n\nThis text is preserved.\n";
public readonly MemoryWorkspace Storage = new(seed: false);
public readonly QueuedService Service = new();
public readonly ThrowingBackend Backend = new();
public readonly DesktopSession Desktop;
public Fixture() => Desktop = new(new Canvas(), Storage, Backend, editorPreparation: Service);
public DesktopWindow Open()
{
string path = SystemPaths.Combine(SystemPaths.Documents, "Example.md");
Storage.WriteText(path, Source); return Desktop.Open(DesktopApplication.Editor, path);
}
public Page CompleteLast(long time = 20)
{
var operation = Service.Operations[^1];
var page = new Page { EditorText = operation.Request.Kind == EditorPreparationKind.Editor ? operation.Request.Source : null };
operation.Complete(page); Desktop.AdvanceFeedback(time); return page;
}
public void Click(int x, int y) { Desktop.Pointer(x, y, false); Desktop.Pointer(x, y, true); Desktop.Pointer(x, y, false); }
}
private sealed class QueuedService : IEditorPreparationService
{
public readonly List<Operation> Operations = [];
public IEditorPreparationOperation Begin(EditorPreparationRequest request) { var operation = new Operation(request); Operations.Add(operation); return operation; }
}
private sealed class Operation(EditorPreparationRequest request) : IEditorPreparationOperation
{
public EditorPreparationRequest Request => request;
private EditorPreparationResult? result;
public bool Cancelled;
public void Complete(Page page) => result = new(page, null, 42);
public void Cancel() { Cancelled = true; result?.Page?.Release(); result = null; }
public bool TryTakeResult(out EditorPreparationResult value) { value = result!; result = null; return value is not null; }
}
private sealed class Page : IInteractiveBrowserSession
{
public BrowserPage Current = new("xfe-doc:Example.md", "Example", "Preview text");
public BrowserPage CurrentPage => Current;
public long RevisionValue; public long Revision => RevisionValue;
public string? EditorText; public bool Released;
public readonly List<(DesktopBounds Bounds, double Scroll)> Paints = [];
public readonly List<(int Width, int Height)> Viewports = [];
public readonly List<BrowserPointerInput> Inputs = [];
public BrowserPage Navigate(string address) => throw new InvalidOperationException("UI navigation");
public string? ReadEditorText() => EditorText;
public void ResizeViewport(int width, int height) => Viewports.Add((width, height));
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport, double scrollY) => Paints.Add((viewport, scrollY));
public bool Pointer(BrowserPointerInput input) { Inputs.Add(input); return false; }
public bool Key(BrowserKeyboardInput input) => true;
public bool AdvanceTime(long milliseconds) => true;
public double DocumentHeight => 4000;
public void Release() => Released = true;
}
private sealed class ThrowingBackend : IBrowserService, IInteractiveBrowserFactory
{
public int Calls;
private Exception Forbidden() { Calls++; return new InvalidOperationException("The desktop attempted synchronous document work."); }
public BrowserPage Navigate(string address) => throw Forbidden();
public string ExecuteJavaScript(string source) => throw Forbidden();
public string PreviewMarkdown(string source) => throw Forbidden();
public IInteractiveBrowserSession CreatePageSession() => throw Forbidden();
}
private sealed class Canvas : IDesktopCanvas
{
public int Width => 1280; public int Height => 800;
public void Fill(uint color, int x, int y, int width, int height) { }
public void Line(uint color, int x1, int y1, int x2, int y2) { }
public void Text(string text, uint color, int x, int y) { }
public void Present() { }
}
}
using XFESystem.Core;
using XFESystem.Desktop;
using Xunit;
namespace XFESystem.Desktop.Tests;
/// <summary>Desktop ownership/input regressions. The deliberate blocking backend
/// throws on any accidental UI-thread parse or navigation.</summary>
public sealed class MarkdownDocumentModeTests
{
[Fact]
public void MarkdownOpensReadOnlyPreviewAndQueuesItsActualDocumentWithoutUiNavigation()
{
var f = new Fixture(); var window = f.Open();
Assert.Equal(MarkdownViewMode.Preview, window.MarkdownView);
Assert.Null(window.BrowserSession); Assert.True(window.IsPreviewPreparing);
var operation = Assert.Single(f.Service.Operations);
Assert.Equal(EditorPreparationKind.Document, operation.Request.Kind);
Assert.Equal(Fixture.Source, operation.Request.Source); Assert.True(operation.Request.Dark);
f.Desktop.Key(DesktopKey.Character, '!');
Assert.Equal(Fixture.Source, window.Content); Assert.False(window.IsDirty);
var files = f.Desktop.Open(DesktopApplication.Files);
f.Desktop.Render(); Assert.Same(files, f.Desktop.ActiveWindow);
Assert.Equal(0, f.Backend.Calls);
}
[Fact]
public void TopRightEditAndSplitButtonsPreserveUnsavedSourceAndKeepPagesIndependent()
{
var f = new Fixture(); var window = f.Open();
var preview = f.CompleteLast();
f.Click(window.Bounds.X + window.Bounds.Width - 50, window.Bounds.Y + 49);
Assert.Equal(MarkdownViewMode.Edit, window.MarkdownView);
Assert.Same(preview, window.PreviewSession);
Assert.Equal(EditorPreparationKind.Editor, f.Service.Operations[^1].Request.Kind);
// Loading the full editor does not stop the responsive source editor.
f.Desktop.Key(DesktopKey.Character, '!');
Assert.Equal("!" + Fixture.Source, window.Content);
Assert.True(window.IsDirty);
f.Click(window.Bounds.X + window.Bounds.Width - 148, window.Bounds.Y + 49);
Assert.Equal(MarkdownViewMode.Split, window.MarkdownView);
Assert.Equal("!" + Fixture.Source, window.Content);
Assert.Same(preview, window.PreviewSession);
Assert.Equal(Fixture.Source, f.Storage.ReadText(window.Location));
f.Desktop.Key(DesktopKey.Save);
Assert.Equal("!" + Fixture.Source, f.Storage.ReadText(window.Location));
Assert.False(window.IsDirty); Assert.Equal(0, f.Backend.Calls);
}
[Fact]
public void SplitPanesUseIndependentPaintBoundsScrollOffsetsAndPointerCoordinatesAfterResize()
{
var f = new Fixture(); var window = f.Open(); var preview = f.CompleteLast();
f.Desktop.SetMarkdownView(window.Id, MarkdownViewMode.Split);
var editor = f.CompleteLast();
f.Desktop.Render();
var a = Assert.Single(editor.Paints).Bounds; var b = Assert.Single(preview.Paints).Bounds;
Assert.True(a.X + a.Width < b.X); Assert.Equal(a.Y, b.Y); Assert.Equal(a.Height, b.Height);
f.Click(b.X + 24, b.Y + 31);
var click = Assert.Single(preview.Inputs, input => input.Type == "mousedown");
Assert.Equal(24, click.X); Assert.Equal(31, click.Y);
Assert.DoesNotContain(editor.Inputs, input => input.Type == "mousedown");
f.Desktop.Pointer(b.X + 24, b.Y + 31, false, wheel: -1);
f.Desktop.Render();
Assert.Equal(60, preview.Paints[^1].Scroll); Assert.Equal(0, editor.Paints[^1].Scroll);
int oldWidth = a.Width;
f.Desktop.ToggleMaximize(window.Id); f.Desktop.Render();
Assert.True(editor.Paints[^1].Bounds.Width > oldWidth);
Assert.Equal((editor.Paints[^1].Bounds.Width, editor.Paints[^1].Bounds.Height), editor.Viewports[^1]);
Assert.Equal((preview.Paints[^1].Bounds.Width, preview.Paints[^1].Bounds.Height), preview.Viewports[^1]);
}
[Fact]
public void SplitPreviewDebouncesTypingAndRejectsAnOutdatedPreparedDocument()
{
var f = new Fixture(); var window = f.Open(); var originalPreview = f.CompleteLast();
f.Desktop.SetMarkdownView(window.Id, MarkdownViewMode.Split);
var editor = f.CompleteLast();
editor.EditorText = "# First edit"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(100);
f.Desktop.AdvanceFeedback(200);
Assert.Equal(2, f.Service.Operations.Count);
editor.EditorText = "# Latest edit"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(240); f.Desktop.AdvanceFeedback(400);
Assert.Equal(2, f.Service.Operations.Count);
f.Desktop.AdvanceFeedback(600);
var pending = f.Service.Operations[^1];
Assert.Equal(3, f.Service.Operations.Count); Assert.Equal("# Latest edit", pending.Request.Source);
editor.EditorText = "# During parse"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(650);
var stale = new Page(); pending.Complete(stale);
f.Desktop.AdvanceFeedback(670);
Assert.True(stale.Released); Assert.Same(originalPreview, window.PreviewSession);
f.Desktop.AdvanceFeedback(1000);
Assert.Equal("# During parse", f.Service.Operations[^1].Request.Source);
var latest = f.CompleteLast(1020);
Assert.True(originalPreview.Released); Assert.Same(latest, window.PreviewSession);
Assert.Equal(Fixture.Source, f.Storage.ReadText(window.Location));
}
[Fact]
public void ClosingTheDocumentCancelsPendingPreparationAndReleasesBothPaneOwners()
{
var f = new Fixture(); var window = f.Open(); var preview = f.CompleteLast();
f.Desktop.SetMarkdownView(window.Id, MarkdownViewMode.Split);
var editor = f.CompleteLast();
editor.EditorText = "# Changed"; editor.RevisionValue++;
f.Desktop.AdvanceFeedback(200); f.Desktop.AdvanceFeedback(400); f.Desktop.AdvanceFeedback(600);
f.Desktop.Key(DesktopKey.Save); Assert.False(window.IsDirty);
var pending = f.Service.Operations[^1];
Assert.Equal(EditorPreparationKind.Document, pending.Request.Kind);
f.Desktop.Close(window.Id);
Assert.True(pending.Cancelled); Assert.True(editor.Released); Assert.True(preview.Released);
Assert.Empty(f.Desktop.Windows);
}
[Fact]
public void DefaultBrowserQueuesTheRealBingUrlWithoutBlockingDesktopNavigation()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
var operation = Assert.Single(f.Service.Operations);
Assert.Equal("https://www.bing.com/", operation.Request.Path);
Assert.Equal(EditorPreparationKind.Navigation, operation.Request.Kind);
Assert.Null(window.BrowserSession); f.Desktop.Render(); Assert.Equal(0, f.Backend.Calls);
var page = new Page { Current = new("https://www.bing.com/", "Bing", "Actual response") };
operation.Complete(page); f.Desktop.AdvanceFeedback(20);
Assert.Same(page, window.BrowserSession); Assert.Equal("Browser - Bing", window.Title);
Assert.Equal("Actual response", window.Content);
}
[Fact]
public void FailedBackgroundNavigationCannotReportThatThePageLoaded()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
var operation = Assert.Single(f.Service.Operations);
operation.Complete(new Page { Current = new("https://www.bing.com/", "Navigation failed", "HTTPS is unavailable") });
f.Desktop.AdvanceFeedback(20);
Assert.Equal("Page could not be loaded", window.Status);
Assert.DoesNotContain("loaded in background", window.Status, StringComparison.OrdinalIgnoreCase);
}
private sealed class Fixture
{
public const string Source = "# Original document\n\nThis text is preserved.\n";
public readonly MemoryWorkspace Storage = new(seed: false);
public readonly QueuedService Service = new();
public readonly ThrowingBackend Backend = new();
public readonly DesktopSession Desktop;
public Fixture() => Desktop = new(new Canvas(), Storage, Backend, editorPreparation: Service);
public DesktopWindow Open()
{
string path = SystemPaths.Combine(SystemPaths.Documents, "Example.md");
Storage.WriteText(path, Source); return Desktop.Open(DesktopApplication.Editor, path);
}
public Page CompleteLast(long time = 20)
{
var operation = Service.Operations[^1];
var page = new Page { EditorText = operation.Request.Kind == EditorPreparationKind.Editor ? operation.Request.Source : null };
operation.Complete(page); Desktop.AdvanceFeedback(time); return page;
}
public void Click(int x, int y) { Desktop.Pointer(x, y, false); Desktop.Pointer(x, y, true); Desktop.Pointer(x, y, false); }
}
private sealed class QueuedService : IEditorPreparationService
{
public readonly List<Operation> Operations = [];
public IEditorPreparationOperation Begin(EditorPreparationRequest request) { var operation = new Operation(request); Operations.Add(operation); return operation; }
}
private sealed class Operation(EditorPreparationRequest request) : IEditorPreparationOperation
{
public EditorPreparationRequest Request => request;
private EditorPreparationResult? result;
public bool Cancelled;
public void Complete(Page page) => result = new(page, null, 42);
public void Cancel() { Cancelled = true; result?.Page?.Release(); result = null; }
public bool TryTakeResult(out EditorPreparationResult value) { value = result!; result = null; return value is not null; }
}
private sealed class Page : IInteractiveBrowserSession
{
public BrowserPage Current = new("xfe-doc:Example.md", "Example", "Preview text");
public BrowserPage CurrentPage => Current;
public long RevisionValue; public long Revision => RevisionValue;
public string? EditorText; public bool Released;
public readonly List<(DesktopBounds Bounds, double Scroll)> Paints = [];
public readonly List<(int Width, int Height)> Viewports = [];
public readonly List<BrowserPointerInput> Inputs = [];
public BrowserPage Navigate(string address) => throw new InvalidOperationException("UI navigation");
public string? ReadEditorText() => EditorText;
public void ResizeViewport(int width, int height) => Viewports.Add((width, height));
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport, double scrollY) => Paints.Add((viewport, scrollY));
public bool Pointer(BrowserPointerInput input) { Inputs.Add(input); return false; }
public bool Key(BrowserKeyboardInput input) => true;
public bool AdvanceTime(long milliseconds) => true;
public double DocumentHeight => 4000;
public void Release() => Released = true;
}
private sealed class ThrowingBackend : IBrowserService, IInteractiveBrowserFactory
{
public int Calls;
private Exception Forbidden() { Calls++; return new InvalidOperationException("The desktop attempted synchronous document work."); }
public BrowserPage Navigate(string address) => throw Forbidden();
public string ExecuteJavaScript(string source) => throw Forbidden();
public string PreviewMarkdown(string source) => throw Forbidden();
public IInteractiveBrowserSession CreatePageSession() => throw Forbidden();
}
private sealed class Canvas : IDesktopCanvas
{
public int Width => 1280; public int Height => 800;
public void Fill(uint color, int x, int y, int width, int height) { }
public void Line(uint color, int x1, int y1, int x2, int y2) { }
public void Text(string text, uint color, int x, int y) { }
public void Present() { }
}
}