Browse Source
* Basic infra for using headless platform in normal tests * Add RunJobs helper to scope returned from HeadlessUnitTestApplicationpull/22115/head
committed by
GitHub
2 changed files with 233 additions and 0 deletions
@ -0,0 +1,150 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Headless; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.VisualTree; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests; |
|||
|
|||
public class HeadlessProbeTests : ScopedTestBase |
|||
{ |
|||
[Fact] |
|||
public void Can_Show_Window_And_Lay_Out() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(); |
|||
|
|||
var button = new Button { Width = 100, Height = 30, Content = "hi" }; |
|||
var window = new Window { Width = 400, Height = 300, Content = button }; |
|||
window.Show(); |
|||
app.RunJobs(); |
|||
|
|||
Assert.Equal(new Size(100, 30), button.Bounds.Size); |
|||
Assert.Equal(new Size(400, 300), window.ClientSize); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Route_Real_Input_Through_Headless_Toplevel() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(); |
|||
|
|||
var clicked = false; |
|||
var button = new Button { Width = 100, Height = 30, Content = "hi" }; |
|||
button.Click += (_, _) => clicked = true; |
|||
var window = new Window { Width = 400, Height = 300, Content = button }; |
|||
window.Show(); |
|||
app.RunJobs(); |
|||
|
|||
var pt = button.Bounds.Center; |
|||
window.MouseDown(pt, MouseButton.Left); |
|||
window.MouseUp(pt, MouseButton.Left); |
|||
app.RunJobs(); |
|||
|
|||
Assert.True(clicked); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Keyboard_Input_Reaches_Focused_Control() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(); |
|||
|
|||
var box = new TextBox { Width = 200, Height = 30 }; |
|||
var window = new Window { Width = 400, Height = 300, Content = box }; |
|||
window.Show(); |
|||
app.RunJobs(); |
|||
|
|||
box.Focus(); |
|||
app.RunJobs(); |
|||
window.KeyTextInput("abc"); |
|||
app.RunJobs(); |
|||
|
|||
Assert.Equal("abc", box.Text); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Popup_Opens_As_Real_PopupRoot() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(); |
|||
|
|||
var popup = new Popup { Placement = PlacementMode.Pointer, Child = new Border { Width = 50, Height = 50 } }; |
|||
var window = new Window { Width = 400, Height = 300, Content = new Panel { Children = { popup } } }; |
|||
window.Show(); |
|||
app.RunJobs(); |
|||
|
|||
popup.Open(); |
|||
app.RunJobs(); |
|||
|
|||
Assert.IsType<PopupRoot>(popup.Host); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Popup_Uses_OverlayPopupHost_When_OverlayPopups_Enabled() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(new AvaloniaHeadlessPlatformOptions { OverlayPopups = true }); |
|||
|
|||
var popup = new Popup { Placement = PlacementMode.Pointer, Child = new Border { Width = 50, Height = 50 } }; |
|||
var window = new Window { Width = 400, Height = 300, Content = new Panel { Children = { popup } } }; |
|||
window.Show(); |
|||
app.RunJobs(); |
|||
|
|||
popup.Open(); |
|||
app.RunJobs(); |
|||
|
|||
Assert.IsType<OverlayPopupHost>(popup.Host); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Overlay_Popup_Requires_An_Applied_Window_Template() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(new AvaloniaHeadlessPlatformOptions { OverlayPopups = true }); |
|||
|
|||
// The overlay layer is looked up through the visual tree, so a window that was
|
|||
// never shown has nothing to find.
|
|||
var untemplated = new Popup { PlacementTarget = new Window() }; |
|||
var ex = Assert.Throws<InvalidOperationException>(() => untemplated.Open()); |
|||
Assert.Contains("no overlay layer is found", ex.Message); |
|||
|
|||
var window = new Window(); |
|||
window.Show(); |
|||
app.RunJobs(); |
|||
Assert.NotNull(window.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault()); |
|||
|
|||
var templated = new Popup { PlacementTarget = window }; |
|||
templated.Open(); |
|||
Assert.IsType<OverlayPopupHost>(templated.Host); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Platform_Services_Are_Registered_Before_The_First_Window_And_Stay_Stable() |
|||
{ |
|||
using var app = HeadlessUnitTestApplication.Start(); |
|||
|
|||
object?[] Resolve() => |
|||
[ |
|||
AvaloniaLocator.Current.GetService<IKeyboardDevice>(), |
|||
AvaloniaLocator.Current.GetService<IPlatformSettings>(), |
|||
AvaloniaLocator.Current.GetService<ICursorFactory>(), |
|||
AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(), |
|||
AvaloniaLocator.Current.GetService<IClipboard>(), |
|||
AvaloniaLocator.Current.GetService<IRenderLoop>(), |
|||
AvaloniaLocator.Current.GetService<IPlatformIconLoader>(), |
|||
AvaloniaLocator.Current.GetService<KeyGestureFormatInfo>(), |
|||
]; |
|||
|
|||
var before = Resolve(); |
|||
Assert.All(before, Assert.NotNull); |
|||
|
|||
new Window().Show(); |
|||
app.RunJobs(); |
|||
|
|||
// Initializing the platform lazily used to replace these mid-test, leaving anything that
|
|||
// resolved early holding a different instance than the window does.
|
|||
Assert.Equal(before, Resolve(), System.Collections.Generic.ReferenceEqualityComparer.Instance); |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Headless; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Themes.Simple; |
|||
using Avalonia.Threading; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.UnitTests; |
|||
|
|||
/// <summary>
|
|||
/// Test application running on the real headless platform, set up through <see cref="AppBuilder"/>
|
|||
/// so that the platform owns its own slice of the locator.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is deliberately not a <see cref="TestServices"/> flavour: the headless platform registers
|
|||
/// services of its own (keyboard device, clipboard, render loop, platform settings, hotkey config),
|
|||
/// and <see cref="UnitTestApplication"/> binds the same keys from its service bag, so the two
|
|||
/// overwrite each other depending on when the platform happens to initialize.
|
|||
/// </remarks>
|
|||
public class HeadlessUnitTestApplication : Application |
|||
{ |
|||
public HeadlessUnitTestApplication() |
|||
{ |
|||
Styles.Add(new SimpleTheme()); |
|||
} |
|||
|
|||
public static Scope Start(AvaloniaHeadlessPlatformOptions? options = null) |
|||
{ |
|||
var scope = AvaloniaLocator.EnterScope(); |
|||
var oldContext = SynchronizationContext.Current; |
|||
|
|||
try |
|||
{ |
|||
Dispatcher.ResetBeforeUnitTests(); |
|||
|
|||
AppBuilder.Configure<HeadlessUnitTestApplication>() |
|||
// Popups default to dedicated top-levels here, matching the desktop platforms
|
|||
// and the app used by Avalonia.Headless.UnitTests.
|
|||
.UseHeadless(options ?? new AvaloniaHeadlessPlatformOptions { OverlayPopups = false }) |
|||
.AfterPlatformServicesSetup(_ => AvaloniaLocator.CurrentMutable |
|||
.Bind<IFontManagerImpl>().ToConstant(new TestFontManager())) |
|||
.SetupUnsafe(); |
|||
} |
|||
catch |
|||
{ |
|||
scope.Dispose(); |
|||
throw; |
|||
} |
|||
|
|||
return new Scope(scope, oldContext); |
|||
} |
|||
|
|||
public sealed class Scope(IDisposable locatorScope, SynchronizationContext? oldContext) : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Runs pending dispatcher jobs, tied to the test's cancellation token. Headless top-levels
|
|||
/// post activation, resizes and rendering, so tests have to let those settle between acts.
|
|||
/// </summary>
|
|||
public void RunJobs(DispatcherPriority? priority = null) => |
|||
Dispatcher.UIThread.RunJobs(priority, TestContext.Current.CancellationToken); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Dispatcher.UIThread.RunJobs(); |
|||
} |
|||
|
|||
(AvaloniaLocator.Current.GetService<IToolTipService>() as ToolTipService)?.Dispose(); |
|||
(AvaloniaLocator.Current.GetService<FontManager>() as IDisposable)?.Dispose(); |
|||
(AvaloniaLocator.Current.GetService<IInputManager>() as IDisposable)?.Dispose(); |
|||
|
|||
Dispatcher.ResetForUnitTests(); |
|||
locatorScope.Dispose(); |
|||
Dispatcher.ResetBeforeUnitTests(); |
|||
SynchronizationContext.SetSynchronizationContext(oldContext); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue