diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
index f133fa34f6..45d78b3926 100644
--- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
@@ -224,7 +224,7 @@ HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) {
if (ret == nullptr)
return E_POINTER;
- if(Window != nullptr){
+ if(Window != nullptr && _shown){
auto frame = [Window frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index 6db586f3ca..85a89955f4 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm
@@ -119,13 +119,16 @@ void WindowImpl::BringToFront()
{
if(Window != nullptr)
{
- if(IsDialog())
+ if (![Window isMiniaturized])
{
- Activate();
- }
- else
- {
- [Window orderFront:nullptr];
+ if(IsDialog())
+ {
+ Activate();
+ }
+ else
+ {
+ [Window orderFront:nullptr];
+ }
}
[Window invalidateShadow];
@@ -488,6 +491,8 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
}
if (_shown) {
+ _actualWindowState = _lastWindowState;
+
switch (state) {
case Maximized:
if (currentState == FullScreen) {
@@ -545,7 +550,6 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
break;
}
- _actualWindowState = _lastWindowState;
WindowEvents->WindowStateChanged(_actualWindowState);
}
diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index aa2191c26b..3377979199 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -112,6 +112,7 @@
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index 1aba10ec30..9e180b12c5 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -82,7 +82,7 @@ namespace IntegrationTestApp
break;
}
}
-
+
private void SendToBack()
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
@@ -92,7 +92,18 @@ namespace IntegrationTestApp
window.Activate();
}
}
-
+
+ private void RestoreAll()
+ {
+ var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
+
+ foreach (var window in lifetime.Windows)
+ {
+ if (window.WindowState == WindowState.Minimized)
+ window.WindowState = WindowState.Normal;
+ }
+ }
+
private void MenuClicked(object? sender, RoutedEventArgs e)
{
var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem");
@@ -117,6 +128,8 @@ namespace IntegrationTestApp
SendToBack();
if (source?.Name == "ExitFullscreen")
WindowState = WindowState.Normal;
+ if (source?.Name == "RestoreAll")
+ RestoreAll();
}
}
}
diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml
index a263d8ab46..40c1642e91 100644
--- a/samples/IntegrationTestApp/ShowWindowTest.axaml
+++ b/samples/IntegrationTestApp/ShowWindowTest.axaml
@@ -3,12 +3,14 @@
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
Title="Show Window Test">
-
+
-
+
-
+
@@ -21,5 +23,13 @@
+
+
+
+ Normal
+ Minimized
+ Maximized
+ Fullscreen
+
diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs
index 720f7b1c12..001f186761 100644
--- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs
+++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
@@ -11,6 +12,8 @@ namespace IntegrationTestApp
public ShowWindowTest()
{
InitializeComponent();
+ DataContext = this;
+ PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}";
}
private void InitializeComponent()
@@ -21,17 +24,16 @@ namespace IntegrationTestApp
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
- this.GetControl("ClientSize").Text = $"{Width}, {Height}";
- this.GetControl("FrameSize").Text = $"{FrameSize}";
+ var scaling = PlatformImpl!.DesktopScaling;
this.GetControl("Position").Text = $"{Position}";
this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
- this.GetControl("Scaling").Text = $"{PlatformImpl?.DesktopScaling}";
+ this.GetControl("Scaling").Text = $"{scaling}";
if (Owner is not null)
{
var ownerRect = this.GetControl("OwnerRect");
var owner = (Window)Owner;
- ownerRect.Text = $"{owner.Position}, {owner.FrameSize}";
+ ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}
}
}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 92f74530e2..2dd391945b 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -901,10 +901,10 @@ namespace Avalonia.Controls
{
if (owner != null)
{
- // TODO: We really need non-client size here.
+ var ownerSize = owner.FrameSize ?? owner.ClientSize;
var ownerRect = new PixelRect(
owner.Position,
- PixelSize.FromSize(owner.ClientSize, scaling));
+ PixelSize.FromSize(ownerSize, scaling));
Position = ownerRect.CenterRect(rect).Position;
}
}
diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
index 16d37e4beb..4b361c6716 100644
--- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
+++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs
@@ -130,17 +130,10 @@ namespace Avalonia.IntegrationTests.Appium
public static void SendClick(this AppiumWebElement element)
{
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- element.Click();
- }
- else
- {
- // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls
- // such as list items don't support this action, so instead simulate a physical click as VoiceOver
- // does.
- new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform();
- }
+ // The Click() method seems to correspond to accessibilityPerformPress on macOS but certain controls
+ // such as list items don't support this action, so instead simulate a physical click as VoiceOver
+ // does. On Windows, Click() seems to fail with the WindowState checkbox for some reason.
+ new Actions(element.WrappedDriver).MoveToElement(element).Click().Perform();
}
public static void MovePointerOver(this AppiumWebElement element)
diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs
new file mode 100644
index 0000000000..7ac30ee11b
--- /dev/null
+++ b/tests/Avalonia.IntegrationTests.Appium/PlatformTheoryAttribute.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Appium
+{
+ internal class PlatformTheoryAttribute : TheoryAttribute
+ {
+ public PlatformTheoryAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms;
+
+ public TestPlatforms Platforms { get; }
+
+ public override string? Skip
+ {
+ get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}";
+ set => throw new NotSupportedException();
+ }
+
+ private bool IsSupported()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return Platforms.HasAnyFlag(TestPlatforms.Windows);
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return Platforms.HasAnyFlag(TestPlatforms.MacOS);
+ return false;
+ }
+ }
+}
diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
index f1a625dbb4..2b10c302bc 100644
--- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
+++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
@@ -1,7 +1,9 @@
using System;
using System.Runtime.InteropServices;
+using System.Threading;
using Avalonia.Controls;
using OpenQA.Selenium.Appium;
+using OpenQA.Selenium.Interactions;
using Xunit;
using Xunit.Sdk;
@@ -23,35 +25,86 @@ namespace Avalonia.IntegrationTests.Appium
[Theory]
[MemberData(nameof(StartupLocationData))]
- public void StartupLocation(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location)
+ public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location)
{
using var window = OpenWindow(size, mode, location);
- var clientSize = Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text);
- var frameSize = Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text);
- var position = PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text);
- var screenRect = PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text);
- var scaling = double.Parse(_session.FindElementByAccessibilityId("Scaling").Text);
+ var info = GetWindowInfo();
- Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width.");
- Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height.");
+ if (size.HasValue)
+ Assert.Equal(size.Value, info.ClientSize);
+
+ Assert.True(info.FrameSize.Width >= info.ClientSize.Width, "Expected frame width >= client width.");
+ Assert.True(info.FrameSize.Height > info.ClientSize.Height, "Expected frame height > client height.");
- var frameRect = new PixelRect(position, PixelSize.FromSize(frameSize, scaling));
+ var frameRect = new PixelRect(info.Position, PixelSize.FromSize(info.FrameSize, info.Scaling));
switch (location)
{
case WindowStartupLocation.CenterScreen:
- {
- var expected = screenRect.CenterRect(frameRect);
- AssertCloseEnough(expected.Position, frameRect.Position);
- break;
- }
+ {
+ var expected = info.ScreenRect.CenterRect(frameRect);
+ AssertCloseEnough(expected.Position, frameRect.Position);
+ break;
+ }
+ case WindowStartupLocation.CenterOwner:
+ {
+ Assert.NotNull(info.OwnerRect);
+ var expected = info.OwnerRect!.Value.CenterRect(frameRect);
+ AssertCloseEnough(expected.Position, frameRect.Position);
+ break;
+ }
+ }
+ }
+
+
+ [Theory]
+ [InlineData(ShowWindowMode.NonOwned)]
+ [InlineData(ShowWindowMode.Owned)]
+ [InlineData(ShowWindowMode.Modal)]
+ public void WindowState(ShowWindowMode mode)
+ {
+ using var window = OpenWindow(null, mode, WindowStartupLocation.Manual);
+ var windowState = _session.FindElementByAccessibilityId("WindowState");
+ var original = GetWindowInfo();
+
+ Assert.Equal("Normal", windowState.GetComboBoxValue());
+
+ windowState.Click();
+ _session.FindElementByName("Maximized").SendClick();
+ Assert.Equal("Maximized", windowState.GetComboBoxValue());
+
+ windowState.Click();
+ _session.FindElementByName("Normal").SendClick();
+
+ var current = GetWindowInfo();
+ Assert.Equal(original.Position, current.Position);
+ Assert.Equal(original.FrameSize, current.FrameSize);
+
+ // On macOS, only non-owned windows can go fullscreen.
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || mode == ShowWindowMode.NonOwned)
+ {
+ windowState.Click();
+ _session.FindElementByName("Fullscreen").SendClick();
+ Assert.Equal("Fullscreen", windowState.GetComboBoxValue());
+
+ current = GetWindowInfo();
+ var clientSize = PixelSize.FromSize(current.ClientSize, current.Scaling);
+ Assert.True(clientSize.Width >= current.ScreenRect.Width);
+ Assert.True(clientSize.Height >= current.ScreenRect.Height);
+
+ windowState.Click();
+ _session.FindElementByName("Normal").SendClick();
+
+ current = GetWindowInfo();
+ Assert.Equal(original.Position, current.Position);
+ Assert.Equal(original.FrameSize, current.FrameSize);
}
}
-
- public static TheoryData StartupLocationData()
+
+ public static TheoryData StartupLocationData()
{
- var sizes = new PixelSize?[] { null, new PixelSize(400, 300) };
- var data = new TheoryData();
+ var sizes = new Size?[] { null, new Size(400, 300) };
+ var data = new TheoryData();
foreach (var size in sizes)
{
@@ -96,7 +149,7 @@ namespace Avalonia.IntegrationTests.Appium
}
}
- private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location)
+ private IDisposable OpenWindow(Size? size, ShowWindowMode mode, WindowStartupLocation location)
{
var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode");
@@ -115,11 +168,50 @@ namespace Avalonia.IntegrationTests.Appium
return showButton.OpenWindowWithClick();
}
+ private WindowInfo GetWindowInfo()
+ {
+ PixelRect? ReadOwnerRect()
+ {
+ var text = _session.FindElementByAccessibilityId("OwnerRect").Text;
+ return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null;
+ }
+
+ var retry = 0;
+
+ for (;;)
+ {
+ try
+ {
+ return new(
+ Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text),
+ Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text),
+ PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text),
+ ReadOwnerRect(),
+ PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text),
+ double.Parse(_session.FindElementByAccessibilityId("Scaling").Text));
+ }
+ catch (OpenQA.Selenium.NoSuchElementException e) when (retry++ < 3)
+ {
+ // MacOS sometimes seems to need a bit of time to get itself back in order after switching out
+ // of fullscreen.
+ Thread.Sleep(1000);
+ }
+ }
+ }
+
public enum ShowWindowMode
{
NonOwned,
Owned,
Modal
}
+
+ private record WindowInfo(
+ Size ClientSize,
+ Size FrameSize,
+ PixelPoint Position,
+ PixelRect? OwnerRect,
+ PixelRect ScreenRect,
+ double Scaling);
}
}
diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
index a460dc1d68..15ca78fdac 100644
--- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
+++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
@@ -17,11 +17,27 @@ namespace Avalonia.IntegrationTests.Appium
public WindowTests_MacOS(TestAppFixture fixture)
{
+ var retry = 0;
+
_session = fixture.Session;
- var tabs = _session.FindElementByAccessibilityId("MainTabs");
- var tab = tabs.FindElementByName("Window");
- tab.Click();
+ for (;;)
+ {
+ try
+ {
+ var tabs = _session.FindElementByAccessibilityId("MainTabs");
+ var tab = tabs.FindElementByName("Window");
+ tab.Click();
+ return;
+ }
+ catch (WebDriverException e) when (retry++ < 3)
+ {
+ // MacOS sometimes seems to need a bit of time to get itself back in order after switching out
+ // of fullscreen.
+ Thread.Sleep(1000);
+ }
+ }
+
}
[PlatformFact(TestPlatforms.MacOS)]
@@ -29,7 +45,7 @@ namespace Avalonia.IntegrationTests.Appium
{
var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
- using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
+ using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
{
mainWindow.Click();
@@ -41,13 +57,13 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal(1, mainWindowIndex);
}
}
-
+
[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip()
{
var mainWindow = FindWindow(_session, "MainWindow");
- using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
+ using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
{
new Actions(_session)
.MoveToElement(mainWindow, 100, 1)
@@ -57,7 +73,7 @@ namespace Avalonia.IntegrationTests.Appium
var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow");
-
+
new Actions(_session)
.MoveToElement(mainWindow, 100, 1)
.Release()
@@ -67,20 +83,20 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal(1, mainWindowIndex);
}
}
-
+
[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen()
{
var mainWindow = FindWindow(_session, "MainWindow");
var buttons = mainWindow.GetChromeButtons();
-
+
buttons.maximize.Click();
Thread.Sleep(500);
try
{
- using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner))
+ using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
{
var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow"));
var mainWindowIndex = GetWindowOrder(windows, "MainWindow");
@@ -88,6 +104,8 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal(0, secondaryWindowIndex);
Assert.Equal(1, mainWindowIndex);
+
+ Thread.Sleep(5000);
}
}
finally
@@ -95,13 +113,13 @@ namespace Avalonia.IntegrationTests.Appium
_session.FindElementByAccessibilityId("ExitFullscreen").Click();
}
}
-
+
[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent()
{
var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
- using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner))
+ using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual))
{
mainWindow.Click();
@@ -119,7 +137,7 @@ namespace Avalonia.IntegrationTests.Appium
{
var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
- using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner))
+ using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
{
mainWindow.Click();
@@ -140,7 +158,7 @@ namespace Avalonia.IntegrationTests.Appium
{
var window = FindWindow(_session, "MainWindow");
var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons();
-
+
Assert.True(closeButton.Enabled);
Assert.True(zoomButton.Enabled);
Assert.True(miniaturizeButton.Enabled);
@@ -152,7 +170,7 @@ namespace Avalonia.IntegrationTests.Appium
Assert.False(miniaturizeButton.Enabled);
}
}
-
+
[PlatformFact(TestPlatforms.MacOS)]
public void Minimize_Button_Is_Disabled_On_Modal_Dialog()
{
@@ -160,13 +178,39 @@ namespace Avalonia.IntegrationTests.Appium
{
var secondaryWindow = FindWindow(_session, "SecondaryWindow");
var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons();
-
+
Assert.True(closeButton.Enabled);
Assert.True(zoomButton.Enabled);
Assert.False(miniaturizeButton.Enabled);
}
}
+ [PlatformTheory(TestPlatforms.MacOS)]
+ [InlineData(ShowWindowMode.NonOwned)]
+ [InlineData(ShowWindowMode.Owned)]
+ public void Minimize_Button_Minimizes_Window(ShowWindowMode mode)
+ {
+ using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual))
+ {
+ var secondaryWindow = FindWindow(_session, "SecondaryWindow");
+ var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons();
+
+ miniaturizeButton.Click();
+ Thread.Sleep(1000);
+
+ var hittable = _session.FindElementsByXPath("/XCUIElementTypeApplication/XCUIElementTypeWindow")
+ .Select(x => x.GetAttribute("hittable")).ToList();
+ Assert.Equal(new[] { "true", "false" }, hittable);
+
+ _session.FindElementByAccessibilityId("RestoreAll").Click();
+ Thread.Sleep(1000);
+
+ hittable = _session.FindElementsByXPath("/XCUIElementTypeApplication/XCUIElementTypeWindow")
+ .Select(x => x.GetAttribute("hittable")).ToList();
+ Assert.Equal(new[] { "true", "true" }, hittable);
+ }
+ }
+
private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location)
{
var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize");
@@ -195,7 +239,7 @@ namespace Avalonia.IntegrationTests.Appium
private static AppiumWebElement FindWindow(AppiumDriver session, string identifier)
{
var windows = session.FindElementsByXPath("XCUIElementTypeWindow");
- return windows.First(x =>
+ return windows.First(x =>
x.FindElementsByXPath("XCUIElementTypeWindow")
.Any(y => y.GetAttribute("identifier") == identifier));
}
diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
new file mode 100755
index 0000000000..30a4a79f4a
--- /dev/null
+++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
@@ -0,0 +1,8 @@
+# Cleans, builds, and runs integration tests on macOS.
+# Can be used by `git bisect run` to automatically find the commit which introduced a problem.
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+cd "$SCRIPT_DIR"/../.. || exit
+git clean -xdf
+./build.sh CompileNative
+./samples/IntegrationTestApp/bundle.sh
+dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed"