Browse Source
* Added embedding page to IntegrationTestApp. Currently embeds a lone native text box, and only when running on Windows. * Win32 automation support for native control host. Allows native controls to appear in the Avalonia automation tree as a child of the `NativeControlHost`. They also appear in the _wrong_ place - as a direct child of the `Window` - but this appears to be expected behavior as it happens when hosting a win32 control in WPF as well. * Basic native control integration test on win32. * Test editing native win32 control in popup. * Add embedded text box on macOS. * macOS automation support for native control host. Implements special-casing of `InteropAutomationPeer`on macOS. * Make native control integration test work on macOS. The test for the native control in a popup is disabled on macOS because we have a bug there. * Add missing parts * Fix build error * Skip test to see if CI passes again. * Log more info about integration tests on win32. * Try to fix flaky test. * The tests won't yet work on macOS yet. Will require #16577. --------- Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com> Co-authored-by: Max Katz <maxkatz6@outlook.com>pull/16604/head
committed by
GitHub
23 changed files with 501 additions and 20 deletions
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal interface INativeControlFactory |
|||
{ |
|||
IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using MonoMac.AppKit; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class MacHelper |
|||
{ |
|||
private static bool s_isInitialized; |
|||
|
|||
public static void EnsureInitialized() |
|||
{ |
|||
if (s_isInitialized) |
|||
return; |
|||
s_isInitialized = true; |
|||
NSApplication.Init(); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System; |
|||
using System.Text; |
|||
using Avalonia.Platform; |
|||
using MonoMac.AppKit; |
|||
using MonoMac.WebKit; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class MacOSTextBoxFactory : INativeControlFactory |
|||
{ |
|||
public IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
|||
{ |
|||
MacHelper.EnsureInitialized(); |
|||
|
|||
var textView = new NSTextView(); |
|||
textView.TextStorage.Append(new("Native text box")); |
|||
|
|||
return new MacOSViewHandle(textView); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using Avalonia.Controls.Platform; |
|||
using MonoMac.AppKit; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class MacOSViewHandle(NSView view) : INativeControlHostDestroyableControlHandle |
|||
{ |
|||
private NSView? _view = view; |
|||
|
|||
public IntPtr Handle => _view?.Handle ?? IntPtr.Zero; |
|||
public string HandleDescriptor => "NSView"; |
|||
|
|||
public void Destroy() |
|||
{ |
|||
_view?.Dispose(); |
|||
_view = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class NativeTextBox : NativeControlHost |
|||
{ |
|||
public static INativeControlFactory? Factory { get; set; } |
|||
|
|||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) |
|||
{ |
|||
return Factory?.CreateControl(parent, () => base.CreateNativeControlCore(parent)) |
|||
?? base.CreateNativeControlCore(parent); |
|||
} |
|||
|
|||
protected override void DestroyNativeControlCore(IPlatformHandle control) |
|||
{ |
|||
base.DestroyNativeControlCore(control); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using System.Text; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class Win32TextBoxFactory : INativeControlFactory |
|||
{ |
|||
public IPlatformHandle CreateControl(IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
|||
{ |
|||
var handle = WinApi.CreateWindowEx(0, "EDIT", |
|||
@"Native text box", |
|||
(uint)(WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_BORDER), |
|||
0, 0, 1, 1, |
|||
parent.Handle, |
|||
IntPtr.Zero, |
|||
WinApi.GetModuleHandle(null), |
|||
IntPtr.Zero); |
|||
return new Win32WindowControlHandle(handle, "HWND"); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class Win32WindowControlHandle : PlatformHandle, INativeControlHostDestroyableControlHandle |
|||
{ |
|||
public Win32WindowControlHandle(IntPtr handle, string descriptor) : base(handle, descriptor) { } |
|||
public void Destroy() => WinApi.DestroyWindow(Handle); |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace IntegrationTestApp.Embedding; |
|||
|
|||
internal class WinApi |
|||
{ |
|||
[Flags] |
|||
public enum WindowStyles : uint |
|||
{ |
|||
WS_BORDER = 0x800000, |
|||
WS_CAPTION = 0xc00000, |
|||
WS_CHILD = 0x40000000, |
|||
WS_CLIPCHILDREN = 0x2000000, |
|||
WS_CLIPSIBLINGS = 0x4000000, |
|||
WS_DISABLED = 0x8000000, |
|||
WS_DLGFRAME = 0x400000, |
|||
WS_GROUP = 0x20000, |
|||
WS_HSCROLL = 0x100000, |
|||
WS_MAXIMIZE = 0x1000000, |
|||
WS_MAXIMIZEBOX = 0x10000, |
|||
WS_MINIMIZE = 0x20000000, |
|||
WS_MINIMIZEBOX = 0x20000, |
|||
WS_OVERLAPPED = 0x0, |
|||
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, |
|||
WS_POPUP = 0x80000000u, |
|||
WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, |
|||
WS_SYSMENU = 0x80000, |
|||
WS_TABSTOP = 0x10000, |
|||
WS_THICKFRAME = 0x40000, |
|||
WS_VISIBLE = 0x10000000, |
|||
WS_VSCROLL = 0x200000, |
|||
WS_EX_DLGMODALFRAME = 0x00000001, |
|||
WS_EX_NOPARENTNOTIFY = 0x00000004, |
|||
WS_EX_NOREDIRECTIONBITMAP = 0x00200000, |
|||
WS_EX_TOPMOST = 0x00000008, |
|||
WS_EX_ACCEPTFILES = 0x00000010, |
|||
WS_EX_TRANSPARENT = 0x00000020, |
|||
WS_EX_MDICHILD = 0x00000040, |
|||
WS_EX_TOOLWINDOW = 0x00000080, |
|||
WS_EX_WINDOWEDGE = 0x00000100, |
|||
WS_EX_CLIENTEDGE = 0x00000200, |
|||
WS_EX_CONTEXTHELP = 0x00000400, |
|||
WS_EX_RIGHT = 0x00001000, |
|||
WS_EX_LEFT = 0x00000000, |
|||
WS_EX_RTLREADING = 0x00002000, |
|||
WS_EX_LTRREADING = 0x00000000, |
|||
WS_EX_LEFTSCROLLBAR = 0x00004000, |
|||
WS_EX_RIGHTSCROLLBAR = 0x00000000, |
|||
WS_EX_CONTROLPARENT = 0x00010000, |
|||
WS_EX_STATICEDGE = 0x00020000, |
|||
WS_EX_APPWINDOW = 0x00040000, |
|||
WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, |
|||
WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, |
|||
WS_EX_LAYERED = 0x00080000, |
|||
WS_EX_NOINHERITLAYOUT = 0x00100000, |
|||
WS_EX_LAYOUTRTL = 0x00400000, |
|||
WS_EX_COMPOSITED = 0x02000000, |
|||
WS_EX_NOACTIVATE = 0x08000000 |
|||
} |
|||
|
|||
[DllImport("user32.dll", SetLastError = true)] |
|||
public static extern bool DestroyWindow(IntPtr hwnd); |
|||
|
|||
[DllImport("kernel32.dll")] |
|||
public static extern IntPtr GetModuleHandle(string? lpModuleName); |
|||
|
|||
[DllImport("user32.dll", SetLastError = true)] |
|||
public static extern IntPtr CreateWindowEx( |
|||
int dwExStyle, |
|||
string lpClassName, |
|||
string lpWindowName, |
|||
uint dwStyle, |
|||
int x, |
|||
int y, |
|||
int nWidth, |
|||
int nHeight, |
|||
IntPtr hWndParent, |
|||
IntPtr hMenu, |
|||
IntPtr hInstance, |
|||
IntPtr lpParam); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:embedding="using:IntegrationTestApp.Embedding" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="IntegrationTestApp.EmbeddingPage"> |
|||
<StackPanel> |
|||
<embedding:NativeTextBox Name="NativeTextBox" Height="23"/> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<CheckBox Name="EmbeddingPopupOpenCheckBox">Open Popup</CheckBox> |
|||
<Popup IsOpen="{Binding #EmbeddingPopupOpenCheckBox.IsChecked}" |
|||
PlacementTarget="EmbeddingPopupOpenCheckBox" |
|||
Placement="Right"> |
|||
<embedding:NativeTextBox Name="NativeTextBoxInPopup" Width="200" Height="23"/> |
|||
</Popup> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace IntegrationTestApp; |
|||
|
|||
public partial class EmbeddingPage : UserControl |
|||
{ |
|||
public EmbeddingPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> |
|||
<assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/> |
|||
|
|||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
|||
<application> |
|||
<!-- A list of the Windows versions that this application has been tested on |
|||
and is designed to work with. Uncomment the appropriate elements |
|||
and Windows will automatically select the most compatible environment. --> |
|||
|
|||
<!-- Windows Vista --> |
|||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />--> |
|||
|
|||
<!-- Windows 7 --> |
|||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />--> |
|||
|
|||
<!-- Windows 8 --> |
|||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />--> |
|||
|
|||
<!-- Windows 8.1 --> |
|||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />--> |
|||
|
|||
<!-- Windows 10 --> |
|||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> |
|||
|
|||
</application> |
|||
</compatibility> |
|||
</assembly> |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Automation.Peers; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Controls.Automation.Peers; |
|||
|
|||
/// <summary>
|
|||
/// Represents the root of a native control automation tree hosted by a <see cref="NativeControlHost"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This peer should be special-cased in the platform backend, as it represents a native control
|
|||
/// and hence none of the standard automation peer methods are applicable.
|
|||
/// </remarks>
|
|||
internal class InteropAutomationPeer : AutomationPeer |
|||
{ |
|||
private AutomationPeer? _parent; |
|||
|
|||
public InteropAutomationPeer(IPlatformHandle nativeControlHandle) => NativeControlHandle = nativeControlHandle; |
|||
public IPlatformHandle NativeControlHandle { get; } |
|||
|
|||
protected override void BringIntoViewCore() => throw new NotImplementedException(); |
|||
protected override string? GetAcceleratorKeyCore() => throw new NotImplementedException(); |
|||
protected override string? GetAccessKeyCore() => throw new NotImplementedException(); |
|||
protected override AutomationControlType GetAutomationControlTypeCore() => throw new NotImplementedException(); |
|||
protected override string? GetAutomationIdCore() => throw new NotImplementedException(); |
|||
protected override Rect GetBoundingRectangleCore() => throw new NotImplementedException(); |
|||
protected override string GetClassNameCore() => throw new NotImplementedException(); |
|||
protected override AutomationPeer? GetLabeledByCore() => throw new NotImplementedException(); |
|||
protected override string? GetNameCore() => throw new NotImplementedException(); |
|||
protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore() => throw new NotImplementedException(); |
|||
protected override AutomationPeer? GetParentCore() => _parent; |
|||
protected override bool HasKeyboardFocusCore() => throw new NotImplementedException(); |
|||
protected override bool IsContentElementCore() => throw new NotImplementedException(); |
|||
protected override bool IsControlElementCore() => throw new NotImplementedException(); |
|||
protected override bool IsEnabledCore() => throw new NotImplementedException(); |
|||
protected override bool IsKeyboardFocusableCore() => throw new NotImplementedException(); |
|||
protected override void SetFocusCore() => throw new NotImplementedException(); |
|||
protected override bool ShowContextMenuCore() => throw new NotImplementedException(); |
|||
|
|||
protected internal override bool TrySetParent(AutomationPeer? parent) |
|||
{ |
|||
_parent = parent; |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Automation.Peers; |
|||
|
|||
namespace Avalonia.Controls.Automation.Peers; |
|||
|
|||
internal class NativeControlHostPeer : ControlAutomationPeer |
|||
{ |
|||
public NativeControlHostPeer(NativeControlHost owner) |
|||
: base(owner) |
|||
{ |
|||
owner.NativeControlHandleChanged += OnNativeControlHandleChanged; |
|||
} |
|||
|
|||
protected override IReadOnlyList<AutomationPeer>? GetChildrenCore() |
|||
{ |
|||
if (Owner is NativeControlHost host && host.NativeControlHandle != null) |
|||
return [new InteropAutomationPeer(host.NativeControlHandle)]; |
|||
return null; |
|||
} |
|||
|
|||
private void OnNativeControlHandleChanged(object? sender, EventArgs e) => InvalidateChildren(); |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Controls.Automation.Peers; |
|||
using Avalonia.Win32.Interop.Automation; |
|||
|
|||
namespace Avalonia.Win32.Automation; |
|||
|
|||
/// <summary>
|
|||
/// An automation node which serves as the root of an embedded native control automation tree.
|
|||
/// </summary>
|
|||
[RequiresUnreferencedCode("Requires .NET COM interop")] |
|||
internal class InteropAutomationNode : AutomationNode, IRawElementProviderFragmentRoot |
|||
{ |
|||
private readonly IntPtr _handle; |
|||
|
|||
public InteropAutomationNode(InteropAutomationPeer peer) |
|||
: base(peer) |
|||
{ |
|||
_handle = peer.NativeControlHandle.Handle; |
|||
} |
|||
|
|||
public override Rect BoundingRectangle => default; |
|||
public override IRawElementProviderFragmentRoot? FragmentRoot => null; |
|||
public override ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider | ProviderOptions.OverrideProvider; |
|||
|
|||
public override object? GetPatternProvider(int patternId) => null; |
|||
public override object? GetPropertyValue(int propertyId) => null; |
|||
|
|||
public override IRawElementProviderSimple? HostRawElementProvider |
|||
{ |
|||
get |
|||
{ |
|||
var hr = UiaCoreProviderApi.UiaHostProviderFromHwnd(_handle, out var result); |
|||
Marshal.ThrowExceptionForHR(hr); |
|||
return result; |
|||
} |
|||
} |
|||
|
|||
public override IRawElementProviderFragment? Navigate(NavigateDirection direction) |
|||
{ |
|||
return direction == NavigateDirection.Parent ? base.Navigate(direction) : null; |
|||
} |
|||
|
|||
public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y) => null; |
|||
public IRawElementProviderFragment? GetFocus() => null; |
|||
public IRawElementProviderSimple[]? GetEmbeddedFragmentRoots() => null; |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
using System; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.IntegrationTests.Appium |
|||
{ |
|||
[Collection("Default")] |
|||
public class EmbeddingTests : TestBase |
|||
{ |
|||
public EmbeddingTests(DefaultAppFixture fixture) |
|||
: base(fixture, "Embedding") |
|||
{ |
|||
} |
|||
|
|||
[PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] |
|||
public void Can_Edit_Native_TextBox() |
|||
{ |
|||
// Appium has different XPath syntax between Windows and macOS.
|
|||
var textBox = OperatingSystem.IsWindows() ? |
|||
Session.FindElementByXPath($"//*[@AutomationId='NativeTextBox']//*[1]") : |
|||
Session.FindElementByXPath($"//*[@identifier='NativeTextBox']//*[1]"); |
|||
|
|||
Assert.Equal("Native text box", textBox.Text); |
|||
|
|||
textBox.SendKeys("Hello world!"); |
|||
|
|||
// SendKeys behaves differently between Windows and macOS. On Windows it inserts at the start
|
|||
// of the text box, on macOS it replaces the text for some reason. Sigh.
|
|||
var expected = OperatingSystem.IsWindows() ? |
|||
"Hello world!Native text box" : |
|||
"Hello world!"; |
|||
Assert.Equal(expected, textBox.Text); |
|||
} |
|||
|
|||
[PlatformFact(TestPlatforms.Windows, "Not yet working on macOS")] |
|||
public void Can_Edit_Native_TextBox_In_Popup() |
|||
{ |
|||
var checkBox = Session.FindElementByAccessibilityId("EmbeddingPopupOpenCheckBox"); |
|||
checkBox.Click(); |
|||
|
|||
try |
|||
{ |
|||
// Appium has different XPath syntax between Windows and macOS.
|
|||
var textBox = OperatingSystem.IsWindows() ? |
|||
Session.FindElementByXPath($"//*[@AutomationId='NativeTextBoxInPopup']//*[1]") : |
|||
Session.FindElementByXPath($"//*[@identifier='NativeTextBoxInPopup']//*[1]"); |
|||
|
|||
Assert.Equal("Native text box", textBox.Text); |
|||
|
|||
textBox.SendKeys("Hello world!"); |
|||
|
|||
// SendKeys behaves differently between Windows and macOS. On Windows it inserts at the start
|
|||
// of the text box, on macOS it replaces the text for some reason. Sigh.
|
|||
var expected = OperatingSystem.IsWindows() ? |
|||
"Hello world!Native text box" : |
|||
"Hello world!"; |
|||
Assert.Equal(expected, textBox.Text); |
|||
} |
|||
finally |
|||
{ |
|||
checkBox.Click(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue