Add basic integration tests for NativeControlHost and improve its automation/a11y support. (#15542)

* 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>
This commit is contained in:
Steven Kirk 2024-08-05 20:32:20 +02:00 коммит произвёл GitHub
Родитель d1cdb29ba0
Коммит c6cdbfec3b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
23 изменённых файлов: 501 добавлений и 20 удалений

Просмотреть файл

@ -13,7 +13,7 @@
NSMutableArray* _children;
}
+ (id _Nullable)acquire:(IAvnAutomationPeer *)peer
+ (NSAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
{
if (peer == nullptr)
return nil;
@ -23,7 +23,12 @@
if (instance != nullptr)
return dynamic_cast<AvnAutomationNode*>(instance)->GetOwner();
if (peer->IsRootProvider())
if (peer->IsInteropPeer())
{
auto view = (__bridge NSAccessibilityElement*)peer->InteropPeer_GetNativeControlHandle();
return view;
}
else if (peer->IsRootProvider())
{
auto window = peer->RootProvider_GetWindow();
@ -35,7 +40,7 @@
auto holder = dynamic_cast<INSViewHolder*>(window);
auto view = holder->GetNSView();
return [view window];
return (NSAccessibilityElement*)[view window];
}
else
{

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -4,6 +4,7 @@
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);AVP1012</NoWarn>
<ApplicationManifest>app.manifest</ApplicationManifest>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
@ -13,13 +14,14 @@
<NSHighResolutionCapable>true</NSHighResolutionCapable>
<CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dotnet.Bundle" Version="0.9.13" />
<PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -63,6 +63,7 @@ namespace IntegrationTestApp
new("ComboBox", () => new ComboBoxPage()),
new("ContextMenu", () => new ContextMenuPage()),
new("DesktopPage", () => new DesktopPage()),
new("Embedding", () => new EmbeddingPage()),
new("Gestures", () => new GesturesPage()),
new("ListBox", () => new ListBoxPage()),
new("Menu", () => new MenuPage()),

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -1,6 +1,7 @@
using System;
using System.Linq;
using Avalonia;
using IntegrationTestApp.Embedding;
namespace IntegrationTestApp
{
@ -31,6 +32,13 @@ namespace IntegrationTestApp
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.AfterSetup(builder =>
{
NativeTextBox.Factory =
OperatingSystem.IsWindows() ? new Win32TextBoxFactory() :
OperatingSystem.IsMacOS() ? new MacOSTextBoxFactory() :
null;
})
.LogToTrace();
}
}

Просмотреть файл

@ -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();
}

Просмотреть файл

@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -24,6 +25,21 @@ namespace Avalonia.Controls
FlowDirectionProperty.Changed.AddClassHandler<NativeControlHost>(OnFlowDirectionChanged);
}
internal IPlatformHandle? NativeControlHandle
{
get => _nativeControlHandle;
set
{
if (_nativeControlHandle != value)
{
_nativeControlHandle = value;
NativeControlHandleChanged?.Invoke(this, EventArgs.Empty);
}
}
}
internal event EventHandler? NativeControlHandleChanged;
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
@ -89,19 +105,19 @@ namespace Avalonia.Controls
// If there is no attachment, but the control exists,
// attempt to attach to the current toplevel or destroy the control if it's incompatible
if (_attachment == null && _nativeControlHandle != null)
if (_attachment == null && NativeControlHandle != null)
{
if (_currentHost.IsCompatibleWith(_nativeControlHandle))
_attachment = _currentHost.CreateNewAttachment(_nativeControlHandle);
if (_currentHost.IsCompatibleWith(NativeControlHandle))
_attachment = _currentHost.CreateNewAttachment(NativeControlHandle);
else
DestroyNativeControl();
}
// There is no control handle an no attachment, create both
if (_nativeControlHandle == null)
if (NativeControlHandle == null)
{
_attachment = _currentHost.CreateNewAttachment(parent =>
_nativeControlHandle = CreateNativeControlCore(parent));
NativeControlHandle = CreateNativeControlCore(parent));
}
}
else
@ -111,7 +127,7 @@ namespace Avalonia.Controls
_attachment.AttachedTo = null;
// Don't destroy the control immediately, it might be just being reparented to another TopLevel
if (_nativeControlHandle != null && !_queuedForDestruction)
if (NativeControlHandle != null && !_queuedForDestruction)
{
_queuedForDestruction = true;
Dispatcher.UIThread.Post(CheckDestruction, DispatcherPriority.Background);
@ -180,13 +196,13 @@ namespace Avalonia.Controls
private void DestroyNativeControl()
{
if (_nativeControlHandle != null)
if (NativeControlHandle != null)
{
_attachment?.Dispose();
_attachment = null;
DestroyNativeControlCore(_nativeControlHandle);
_nativeControlHandle = null;
DestroyNativeControlCore(NativeControlHandle);
NativeControlHandle = null;
}
}
@ -197,6 +213,7 @@ namespace Avalonia.Controls
nativeControlHostDestroyableControlHandle.Destroy();
}
}
protected override AutomationPeer OnCreateAutomationPeer() => new NativeControlHostPeer(this);
}
}

Просмотреть файл

@ -7,6 +7,7 @@ using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Native.Interop;
#nullable enable
@ -56,6 +57,9 @@ namespace Avalonia.Native
Node = node;
}
public int IsInteropPeer() => (_inner is InteropAutomationPeer).AsComBool();
public IntPtr InteropPeer_GetNativeControlHandle() => ((InteropAutomationPeer)_inner).NativeControlHandle.Handle;
public IAvnAutomationPeer? RootPeer
{
get

Просмотреть файл

@ -1180,6 +1180,9 @@ interface IAvnAutomationPeer : IUnknown
IAvnAutomationPeer* GetRootPeer();
bool IsInteropPeer();
[intptr]void* InteropPeer_GetNativeControlHandle();
bool IsRootProvider();
IAvnWindowBase* RootProvider_GetWindow();
IAvnAutomationPeer* RootProvider_GetFocus();

Просмотреть файл

@ -8,6 +8,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Threading;
using Avalonia.Win32.Interop.Automation;
using AAP = Avalonia.Automation.Provider;
@ -63,7 +64,7 @@ namespace Avalonia.Win32.Automation
public AutomationPeer Peer { get; protected set; }
public Rect BoundingRectangle
public virtual Rect BoundingRectangle
{
get => InvokeSync(() =>
{
@ -79,7 +80,7 @@ namespace Avalonia.Win32.Automation
}
public virtual IRawElementProviderSimple? HostRawElementProvider => null;
public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider;
public virtual ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider;
[return: MarshalAs(UnmanagedType.IUnknown)]
public virtual object? GetPatternProvider(int patternId)
@ -275,9 +276,12 @@ namespace Avalonia.Win32.Automation
private static AutomationNode Create(AutomationPeer peer)
{
return peer.GetProvider<AAP.IRootProvider>() is object ?
new RootAutomationNode(peer) :
new AutomationNode(peer);
if (peer is InteropAutomationPeer interop)
return new InteropAutomationNode(interop);
else if (peer.GetProvider<AAP.IRootProvider>() is not null)
return new RootAutomationNode(peer);
else
return new AutomationNode(peer);
}
private static UiaControlTypeId ToUiaControlType(AutomationControlType role)

Просмотреть файл

@ -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();
}
}
}
}