using XFESystem.Core;
using XFESystem.Desktop;
using Xunit;
namespace XFESystem.Desktop.Tests;
public sealed class BrowserAddressDraftTests
{
private const string Bing = "https://www.bing.com/";
private const string Redirected = "https://cn.bing.com/";
[Fact]
public void InternalNavigationRepaintsTheAddressBarAfterTheSceneCacheHasSettled()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.Desktop.Render(); f.Desktop.AdvanceFeedback(2000); f.Desktop.Render(); f.Canvas.Damages.Clear();
const string search = "https://cn.bing.com/search?q=test";
f.Page.Publish(search, "Actual search results"); f.Desktop.AdvanceFeedback(3000); f.Desktop.Render();
Assert.Equal(search, window.Location);
Assert.Contains(f.Canvas.Damages, damage => damage.Contains(window.Bounds.X + 38, window.Bounds.Y + 43));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DraftSurvivesEveryBackgroundSnapshotAndTheCtrlLRelease(bool pointerFocus)
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window, pointerFocus);
Assert.False(f.Desktop.BrowserKey(new("keyup", "Control", "ControlLeft")));
string draft = "";
foreach (char character in "about:Start")
{
f.Desktop.Key(DesktopKey.Character, character); draft += character;
f.Page.Publish(Redirected, "Timer snapshot " + draft.Length);
f.Desktop.AdvanceFeedback(20 * draft.Length);
Assert.Equal(draft, window.Location);
Assert.False(f.Desktop.HasBrowserContentFocus);
}
Assert.Empty(f.Page.Keys);
Assert.Single(f.Page.Navigations);
f.Desktop.Key(DesktopKey.Enter);
Assert.Equal("about:Start", f.Page.Navigations[^1]);
Assert.Equal("about:Start", window.Location);
f.Page.Publish("xfe-app://Start/index.html", "Loaded local page");
f.Desktop.AdvanceFeedback(400);
Assert.Equal("xfe-app://Start/index.html", window.Location);
}
[Fact]
public void EscapeCancelsToTheLatestCommittedUrlWithoutClosingTheWindow()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window); f.Type("about:St");
f.Page.Publish(Redirected, "The original navigation redirected"); f.Desktop.AdvanceFeedback(20);
Assert.Equal("about:St", window.Location);
f.Desktop.Key(DesktopKey.Escape);
Assert.Contains(window, f.Desktop.Windows);
Assert.Equal(Redirected, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
f.Page.Publish("https://cn.bing.com/new", "Another page snapshot"); f.Desktop.AdvanceFeedback(40);
Assert.Equal("https://cn.bing.com/new", window.Location);
Assert.Single(f.Page.Navigations);
}
[Fact]
public void ClickingContentCancelsTheDraftAndRestoresActualPageKeyboardInput()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window); f.Type("about:Draft");
f.Page.Publish(Redirected, "Current page"); f.Desktop.AdvanceFeedback(20);
f.Click(window.Bounds.X + 70, window.Bounds.Y + 120);
Assert.Equal(Redirected, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
f.Desktop.Key(DesktopKey.Character, 'z');
Assert.Contains(f.Page.Keys, input => input.Type == "keydown" && input.Text == "z");
Assert.Single(f.Page.Navigations);
}
[Fact]
public void CompletingPendingNavigationKeepsTheDraftAndDoesNotStartAnUnsubmittedRequest()
{
var f = new Fixture(background: true); var window = f.Desktop.Open(DesktopApplication.Browser);
var original = Assert.Single(f.Service.Operations);
f.FocusAddress(window); f.Type("about:St");
var page = new Page(Redirected); original.Complete(page); f.Desktop.AdvanceFeedback(20);
Assert.Same(page, window.BrowserSession);
Assert.Equal("about:St", window.Location);
Assert.False(f.Desktop.HasBrowserContentFocus);
Assert.False(f.Desktop.BrowserKey(new("keyup", "Control", "ControlLeft")));
Assert.Single(f.Service.Operations);
f.Type("art"); page.Publish(Redirected, "An animation snapshot"); f.Desktop.AdvanceFeedback(40);
Assert.Equal("about:Start", window.Location);
f.Desktop.Key(DesktopKey.Enter);
Assert.Equal(2, f.Service.Operations.Count);
var submitted = f.Service.Operations[^1];
Assert.Equal(EditorPreparationKind.Navigation, submitted.Request.Kind);
Assert.Equal("about:Start", submitted.Request.Path);
Assert.True(page.Released);
var local = new Page("xfe-app://Start/index.html"); submitted.Complete(local); f.Desktop.AdvanceFeedback(60);
Assert.Equal(local.CurrentPage.Address, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
}
[Fact]
public void SubmitWhilePendingRetiresOnlyTheOldRequestAndQueuesTheCompleteDraft()
{
var f = new Fixture(background: true); var window = f.Desktop.Open(DesktopApplication.Browser);
var original = Assert.Single(f.Service.Operations);
f.FocusAddress(window); f.Type("about:Start"); f.Desktop.AdvanceFeedback(20);
Assert.Single(f.Service.Operations);
f.Desktop.Key(DesktopKey.Enter);
Assert.True(original.Cancelled);
Assert.Equal(2, f.Service.Operations.Count);
Assert.Equal("about:Start", f.Service.Operations[^1].Request.Path);
f.Service.Operations[^1].Complete(new Page("xfe-app://Start/index.html")); f.Desktop.AdvanceFeedback(40);
Assert.Equal("xfe-app://Start/index.html", window.Location);
Assert.Equal(2, f.Service.Operations.Count);
}
[Fact]
public void EscapeWhilePendingRestoresSubmittedUrlAndStillAcceptsItsLaterRedirect()
{
var f = new Fixture(background: true); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window); f.Type("about:Draft"); f.Desktop.Key(DesktopKey.Escape);
Assert.Contains(window, f.Desktop.Windows);
Assert.Equal(Bing, window.Location);
var operation = Assert.Single(f.Service.Operations);
operation.Complete(new Page(Redirected)); f.Desktop.AdvanceFeedback(20);
Assert.Equal(Redirected, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
Assert.Single(f.Service.Operations);
}
private sealed class Fixture
{
public readonly Page Page = new(Bing);
public readonly QueuedService Service = new();
public readonly DesktopSession Desktop;
public readonly Canvas Canvas = new();
public Fixture(bool background = false) => Desktop = new(Canvas, new MemoryWorkspace(seed: false), new Backend(Page), editorPreparation: background ? Service : null);
public void Type(string text) { foreach (char character in text) Desktop.Key(DesktopKey.Character, character); }
public void FocusAddress(DesktopWindow window, bool pointer = false)
{ if (pointer) Click(window.Bounds.X + 150, window.Bounds.Y + 48); else Desktop.Key(DesktopKey.FocusAddress); }
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 Backend(Page page) : IBrowserService, IInteractiveBrowserFactory
{
public IInteractiveBrowserSession CreatePageSession() => page;
public BrowserPage Navigate(string address) => throw new InvalidOperationException("Must use the explicit page session");
public string ExecuteJavaScript(string source) => throw new NotSupportedException();
public string PreviewMarkdown(string source) => throw new NotSupportedException();
}
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 = null; }
public bool TryTakeResult(out EditorPreparationResult value) { value = result!; result = null; return value is not null; }
}
private sealed class Page(string address) : IInteractiveBrowserSession
{
private BrowserPage current = new(address, "Page", "Page content");
private bool pending;
public readonly List<string> Navigations = [];
public readonly List<BrowserKeyboardInput> Keys = [];
public bool Released;
public BrowserPage CurrentPage => current;
public long Revision { get; private set; }
public double DocumentHeight => 400;
public BrowserPage Navigate(string target) { Navigations.Add(target); Publish(target, "Page content"); return current; }
public void Publish(string target, string content) { current = new(target, "Page", content); pending = true; Revision++; }
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport, double scrollY) { }
public bool Pointer(BrowserPointerInput input) => false;
public bool Key(BrowserKeyboardInput input) { Keys.Add(input); return true; }
public bool AdvanceTime(long milliseconds) { bool changed = pending; pending = false; return changed; }
public void Release() => Released = true;
}
private sealed class Canvas : IDesktopCanvas
{
public readonly List<DesktopBounds> Damages = [];
public bool SupportsSceneCache => true;
public void Present(DesktopBounds damage) => Damages.Add(damage);
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;
public sealed class BrowserAddressDraftTests
{
private const string Bing = "https://www.bing.com/";
private const string Redirected = "https://cn.bing.com/";
[Fact]
public void InternalNavigationRepaintsTheAddressBarAfterTheSceneCacheHasSettled()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.Desktop.Render(); f.Desktop.AdvanceFeedback(2000); f.Desktop.Render(); f.Canvas.Damages.Clear();
const string search = "https://cn.bing.com/search?q=test";
f.Page.Publish(search, "Actual search results"); f.Desktop.AdvanceFeedback(3000); f.Desktop.Render();
Assert.Equal(search, window.Location);
Assert.Contains(f.Canvas.Damages, damage => damage.Contains(window.Bounds.X + 38, window.Bounds.Y + 43));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DraftSurvivesEveryBackgroundSnapshotAndTheCtrlLRelease(bool pointerFocus)
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window, pointerFocus);
Assert.False(f.Desktop.BrowserKey(new("keyup", "Control", "ControlLeft")));
string draft = "";
foreach (char character in "about:Start")
{
f.Desktop.Key(DesktopKey.Character, character); draft += character;
f.Page.Publish(Redirected, "Timer snapshot " + draft.Length);
f.Desktop.AdvanceFeedback(20 * draft.Length);
Assert.Equal(draft, window.Location);
Assert.False(f.Desktop.HasBrowserContentFocus);
}
Assert.Empty(f.Page.Keys);
Assert.Single(f.Page.Navigations);
f.Desktop.Key(DesktopKey.Enter);
Assert.Equal("about:Start", f.Page.Navigations[^1]);
Assert.Equal("about:Start", window.Location);
f.Page.Publish("xfe-app://Start/index.html", "Loaded local page");
f.Desktop.AdvanceFeedback(400);
Assert.Equal("xfe-app://Start/index.html", window.Location);
}
[Fact]
public void EscapeCancelsToTheLatestCommittedUrlWithoutClosingTheWindow()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window); f.Type("about:St");
f.Page.Publish(Redirected, "The original navigation redirected"); f.Desktop.AdvanceFeedback(20);
Assert.Equal("about:St", window.Location);
f.Desktop.Key(DesktopKey.Escape);
Assert.Contains(window, f.Desktop.Windows);
Assert.Equal(Redirected, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
f.Page.Publish("https://cn.bing.com/new", "Another page snapshot"); f.Desktop.AdvanceFeedback(40);
Assert.Equal("https://cn.bing.com/new", window.Location);
Assert.Single(f.Page.Navigations);
}
[Fact]
public void ClickingContentCancelsTheDraftAndRestoresActualPageKeyboardInput()
{
var f = new Fixture(); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window); f.Type("about:Draft");
f.Page.Publish(Redirected, "Current page"); f.Desktop.AdvanceFeedback(20);
f.Click(window.Bounds.X + 70, window.Bounds.Y + 120);
Assert.Equal(Redirected, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
f.Desktop.Key(DesktopKey.Character, 'z');
Assert.Contains(f.Page.Keys, input => input.Type == "keydown" && input.Text == "z");
Assert.Single(f.Page.Navigations);
}
[Fact]
public void CompletingPendingNavigationKeepsTheDraftAndDoesNotStartAnUnsubmittedRequest()
{
var f = new Fixture(background: true); var window = f.Desktop.Open(DesktopApplication.Browser);
var original = Assert.Single(f.Service.Operations);
f.FocusAddress(window); f.Type("about:St");
var page = new Page(Redirected); original.Complete(page); f.Desktop.AdvanceFeedback(20);
Assert.Same(page, window.BrowserSession);
Assert.Equal("about:St", window.Location);
Assert.False(f.Desktop.HasBrowserContentFocus);
Assert.False(f.Desktop.BrowserKey(new("keyup", "Control", "ControlLeft")));
Assert.Single(f.Service.Operations);
f.Type("art"); page.Publish(Redirected, "An animation snapshot"); f.Desktop.AdvanceFeedback(40);
Assert.Equal("about:Start", window.Location);
f.Desktop.Key(DesktopKey.Enter);
Assert.Equal(2, f.Service.Operations.Count);
var submitted = f.Service.Operations[^1];
Assert.Equal(EditorPreparationKind.Navigation, submitted.Request.Kind);
Assert.Equal("about:Start", submitted.Request.Path);
Assert.True(page.Released);
var local = new Page("xfe-app://Start/index.html"); submitted.Complete(local); f.Desktop.AdvanceFeedback(60);
Assert.Equal(local.CurrentPage.Address, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
}
[Fact]
public void SubmitWhilePendingRetiresOnlyTheOldRequestAndQueuesTheCompleteDraft()
{
var f = new Fixture(background: true); var window = f.Desktop.Open(DesktopApplication.Browser);
var original = Assert.Single(f.Service.Operations);
f.FocusAddress(window); f.Type("about:Start"); f.Desktop.AdvanceFeedback(20);
Assert.Single(f.Service.Operations);
f.Desktop.Key(DesktopKey.Enter);
Assert.True(original.Cancelled);
Assert.Equal(2, f.Service.Operations.Count);
Assert.Equal("about:Start", f.Service.Operations[^1].Request.Path);
f.Service.Operations[^1].Complete(new Page("xfe-app://Start/index.html")); f.Desktop.AdvanceFeedback(40);
Assert.Equal("xfe-app://Start/index.html", window.Location);
Assert.Equal(2, f.Service.Operations.Count);
}
[Fact]
public void EscapeWhilePendingRestoresSubmittedUrlAndStillAcceptsItsLaterRedirect()
{
var f = new Fixture(background: true); var window = f.Desktop.Open(DesktopApplication.Browser);
f.FocusAddress(window); f.Type("about:Draft"); f.Desktop.Key(DesktopKey.Escape);
Assert.Contains(window, f.Desktop.Windows);
Assert.Equal(Bing, window.Location);
var operation = Assert.Single(f.Service.Operations);
operation.Complete(new Page(Redirected)); f.Desktop.AdvanceFeedback(20);
Assert.Equal(Redirected, window.Location);
Assert.True(f.Desktop.HasBrowserContentFocus);
Assert.Single(f.Service.Operations);
}
private sealed class Fixture
{
public readonly Page Page = new(Bing);
public readonly QueuedService Service = new();
public readonly DesktopSession Desktop;
public readonly Canvas Canvas = new();
public Fixture(bool background = false) => Desktop = new(Canvas, new MemoryWorkspace(seed: false), new Backend(Page), editorPreparation: background ? Service : null);
public void Type(string text) { foreach (char character in text) Desktop.Key(DesktopKey.Character, character); }
public void FocusAddress(DesktopWindow window, bool pointer = false)
{ if (pointer) Click(window.Bounds.X + 150, window.Bounds.Y + 48); else Desktop.Key(DesktopKey.FocusAddress); }
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 Backend(Page page) : IBrowserService, IInteractiveBrowserFactory
{
public IInteractiveBrowserSession CreatePageSession() => page;
public BrowserPage Navigate(string address) => throw new InvalidOperationException("Must use the explicit page session");
public string ExecuteJavaScript(string source) => throw new NotSupportedException();
public string PreviewMarkdown(string source) => throw new NotSupportedException();
}
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 = null; }
public bool TryTakeResult(out EditorPreparationResult value) { value = result!; result = null; return value is not null; }
}
private sealed class Page(string address) : IInteractiveBrowserSession
{
private BrowserPage current = new(address, "Page", "Page content");
private bool pending;
public readonly List<string> Navigations = [];
public readonly List<BrowserKeyboardInput> Keys = [];
public bool Released;
public BrowserPage CurrentPage => current;
public long Revision { get; private set; }
public double DocumentHeight => 400;
public BrowserPage Navigate(string target) { Navigations.Add(target); Publish(target, "Page content"); return current; }
public void Publish(string target, string content) { current = new(target, "Page", content); pending = true; Revision++; }
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport, double scrollY) { }
public bool Pointer(BrowserPointerInput input) => false;
public bool Key(BrowserKeyboardInput input) { Keys.Add(input); return true; }
public bool AdvanceTime(long milliseconds) { bool changed = pending; pending = false; return changed; }
public void Release() => Released = true;
}
private sealed class Canvas : IDesktopCanvas
{
public readonly List<DesktopBounds> Damages = [];
public bool SupportsSceneCache => true;
public void Present(DesktopBounds damage) => Damages.Add(damage);
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() { }
}
}