diff --git a/Microsoft.Maui.sln b/Microsoft.Maui.sln
index 3ae8942ce0ec..b0454362fc6e 100644
--- a/Microsoft.Maui.sln
+++ b/Microsoft.Maui.sln
@@ -249,11 +249,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.Xaml.UnitTests.Int
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.Sample.UITests", "src\Controls\samples\Controls.Sample.UITests\Controls.Sample.UITests.csproj", "{F39F75DC-671B-4649-8005-1929797B3217}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.Core", "src\TestUtils\src\UITest.Core\UITest.Core.csproj", "{352C2381-1DEC-4487-819D-340D1EA98FBE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Core", "src\TestUtils\src\UITest.Core\UITest.Core.csproj", "{352C2381-1DEC-4487-819D-340D1EA98FBE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.Appium", "src\TestUtils\src\UITest.Appium\UITest.Appium.csproj", "{8C8CD467-11F9-4A14-8AF3-047B2CFD19A7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Appium", "src\TestUtils\src\UITest.Appium\UITest.Appium.csproj", "{8C8CD467-11F9-4A14-8AF3-047B2CFD19A7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{8050448A-E08F-4972-9B47-16042A5DFE82}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{8050448A-E08F-4972-9B47-16042A5DFE82}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlGallery.Android.Appium.UITests", "src\Compatibility\ControlGallery\test\Android.Appium.UITests\ControlGallery.Android.Appium.UITests.csproj", "{F748974F-A8E4-4659-801C-804B739D6326}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlGallery.iOS.Appium.UITests", "src\Compatibility\ControlGallery\test\iOS.Appium.UITests\ControlGallery.iOS.Appium.UITests.csproj", "{5923B35B-EA24-4B86-A384-9DAF9F2AFD56}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlGallery.Shared.Appium.UITests", "src\Compatibility\ControlGallery\test\Shared.Appium.UITests\ControlGallery.Shared.Appium.UITests.csproj", "{07D8D4B5-C89D-4BE3-A14A-17668358587C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -644,6 +650,18 @@ Global
{8050448A-E08F-4972-9B47-16042A5DFE82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8050448A-E08F-4972-9B47-16042A5DFE82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8050448A-E08F-4972-9B47-16042A5DFE82}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F748974F-A8E4-4659-801C-804B739D6326}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F748974F-A8E4-4659-801C-804B739D6326}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F748974F-A8E4-4659-801C-804B739D6326}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F748974F-A8E4-4659-801C-804B739D6326}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Release|Any CPU.Build.0 = Release|Any CPU
+ {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -762,6 +780,9 @@ Global
{352C2381-1DEC-4487-819D-340D1EA98FBE} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C}
{8C8CD467-11F9-4A14-8AF3-047B2CFD19A7} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C}
{8050448A-E08F-4972-9B47-16042A5DFE82} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C}
+ {F748974F-A8E4-4659-801C-804B739D6326} = {DDBA9144-36FC-429E-99E1-2A64825434C1}
+ {5923B35B-EA24-4B86-A384-9DAF9F2AFD56} = {DDBA9144-36FC-429E-99E1-2A64825434C1}
+ {07D8D4B5-C89D-4BE3-A14A-17668358587C} = {DDBA9144-36FC-429E-99E1-2A64825434C1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50}
diff --git a/eng/pipelines/common/ui-tests-legacy-steps.yml b/eng/pipelines/common/ui-tests-legacy-steps.yml
new file mode 100644
index 000000000000..45f154de55a4
--- /dev/null
+++ b/eng/pipelines/common/ui-tests-legacy-steps.yml
@@ -0,0 +1,146 @@
+parameters:
+ platform: '' # [ android, ios, windows, catalyst ]
+ path: '' # path to csproj
+ device: '' # the xharness device to use
+ cakeArgs: '' # additional cake args
+ app: '' #path to app to test
+ version: '' #the iOS version'
+ provisionatorChannel: 'latest'
+ agentPoolAccessToken: ''
+ targetSample: "dotnet-legacy-controlgallery"
+ configuration : "Release"
+
+steps:
+ - ${{ if eq(parameters.platform, 'ios')}}:
+ - bash: |
+ chmod +x $(System.DefaultWorkingDirectory)/eng/scripts/clean-bot.sh
+ $(System.DefaultWorkingDirectory)/eng/scripts/clean-bot.sh
+ displayName: 'Clean bot'
+ continueOnError: true
+ timeoutInMinutes: 60
+
+ - template: provision.yml
+ parameters:
+ skipProvisioning: ${{ eq(parameters.platform, 'windows') }}
+ skipAndroidSdks: ${{ ne(parameters.platform, 'android') }}
+ skipXcode: ${{ or(eq(parameters.platform, 'android'), eq(parameters.platform, 'windows')) }}
+ provisionatorChannel: ${{ parameters.provisionatorChannel }}
+
+ - task: PowerShell@2
+ condition: ne('${{ parameters.platform }}' , 'windows')
+ inputs:
+ targetType: 'inline'
+ script: |
+ defaults write -g NSAutomaticCapitalizationEnabled -bool false
+ defaults write -g NSAutomaticTextCompletionEnabled -bool false
+ defaults write -g NSAutomaticSpellingCorrectionEnabled -bool false
+ displayName: "Modify defaults"
+ continueOnError: true
+
+ # AzDO hosted agents default to 1024x768; set something bigger for Windows UI tests
+ - task: ScreenResolutionUtility@1
+ condition: eq('${{ parameters.platform }}' , 'windows')
+ inputs:
+ displaySettings: 'specific'
+ width: '1920'
+ height: '1080'
+ displayName: "Set screen resolution"
+
+ - task: UseNode@1
+ inputs:
+ version: "20.3.1"
+ displayName: "Install node"
+
+ - bash: |
+ echo "##[group]Running ls -al $(npm root -g)"
+ ls -al $(npm root -g)
+ echo "##[endgroup]"
+
+ echo "##[group]Running ls -al $(npm root -g)/appium"
+ ls -al $(npm root -g)/appium
+ echo "##[endgroup]"
+
+ echo "##[group]Running ls -al $(npm root -g)/.appium-????????"
+ ls -al $(npm root -g)/.appium-????????
+ echo "##[endgroup]"
+
+ echo "##[group]Running ls -al $(npm root -g)/appium-doctor"
+ ls -al $(npm root -g)/appium-doctor
+ echo "##[endgroup]"
+
+ echo "##[group]Running ls -al $(npm root -g)/.appium-doctor-????????"
+ ls -al $(npm root -g)/.appium-doctor-????????
+ echo "##[endgroup]"
+
+ echo "##[group]Running ps aux"
+ ps aux
+ echo "##[endgroup]"
+ displayName: "Debugging output"
+ continueOnError: true
+ condition: startsWith(variables['Agent.Name'], 'XAMBOT')
+
+ # Clean up any leftover cached folders of appium and appium-doctor node modules
+ - bash: |
+ rm -rf $(npm root -g)/.appium-????????
+ rm -rf $(npm root -g)/.appium-doctor-????????
+ displayName: "Delete temp .appium-???????? and .appium-doctor-???????? folders"
+ continueOnError: true
+
+ - pwsh: ./eng/scripts/appium-install.ps1
+ displayName: "Install Appium (Drivers)"
+ continueOnError: false
+ retryCountOnTaskFailure: 1
+
+ - pwsh: ./build.ps1 --target=dotnet --configuration="${{ parameters.configuration }}" --verbosity=diagnostic
+ displayName: 'Install .NET'
+ retryCountOnTaskFailure: 2
+ env:
+ DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token)
+ PRIVATE_BUILD: $(PrivateBuild)
+
+ - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)"
+ displayName: 'Add .NET to PATH'
+
+ - pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="${{ parameters.configuration }}"
+ displayName: 'Build the MSBuild Tasks'
+
+ - pwsh: ./build.ps1 --target=${{ parameters.targetSample }} --configuration="${{ parameters.configuration }}" --${{ parameters.platform }} --verbosity=diagnostic --usenuget=false
+ displayName: 'Build the Legacy ControlGallery'
+
+ - bash: |
+ if [ -f "$HOME/Library/Logs/CoreSimulator/*" ]; then rm -r $HOME/Library/Logs/CoreSimulator/*; fi
+ if [ -f "$HOME/Library/Logs/DiagnosticReports/*" ]; then rm -r $HOME/Library/Logs/DiagnosticReports/*; fi
+ displayName: Delete Old Simulator Logs
+ condition: ${{ eq(parameters.platform, 'ios') }}
+ continueOnError: true
+
+ - pwsh: ./build.ps1 -Script eng/devices/${{ parameters.platform }}.cake --target=uitest --project="${{ parameters.path }}" --appproject="${{ parameters.app }}" --device="${{ parameters.device }}" --apiversion="${{ parameters.version }}" --configuration="${{ parameters.configuration }}" --results="$(TestResultsDirectory)" --binlog="$(LogDirectory)" ${{ parameters.cakeArgs }} --verbosity=diagnostic
+ displayName: $(Agent.JobName)
+ ${{ if ne(parameters.platform, 'android')}}:
+ retryCountOnTaskFailure: 1
+
+ - bash: |
+ suffix=$(date +%Y%m%d%H%M%S)
+ zip -9r "$(LogDirectory)/CoreSimulatorLog_${suffix}.zip" "$HOME/Library/Logs/CoreSimulator/"
+ zip -9r "$(LogDirectory)/DiagnosticReports_${suffix}.zip" "$HOME/Library/Logs/DiagnosticReports/"
+ displayName: Zip Simulator Logs
+ condition: ${{ eq(parameters.platform, 'ios') }}
+ continueOnError: true
+
+ - task: PublishTestResults@2
+ displayName: Publish the $(System.PhaseName) test results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: '$(TestResultsDirectory)/*.trx'
+ testRunTitle: '$(System.PhaseName)'
+ failTaskOnFailedTests: true
+
+ - task: PublishBuildArtifacts@1
+ condition: always()
+ displayName: publish artifacts
+
+ # This must always be placed as the last step in the job
+ - template: agent-rebooter/mac.v1.yml@yaml-templates
+ parameters:
+ AgentPoolAccessToken: ${{ parameters.agentPoolAccessToken }}
diff --git a/eng/pipelines/common/ui-tests.yml b/eng/pipelines/common/ui-tests.yml
index 639c8de45dfc..8865a818f0fa 100644
--- a/eng/pipelines/common/ui-tests.yml
+++ b/eng/pipelines/common/ui-tests.yml
@@ -5,11 +5,14 @@ parameters:
macosPool: { }
androidCompatibilityPool: { }
iosCompatibilityPool: { }
+ androidLegacyPool: { }
+ iosLegacyPool: { }
androidApiLevels: [ 30 ]
iosVersions: [ 'latest' ]
provisionatorChannel: 'latest'
agentPoolAccessToken: ''
runCompatibilityTests: false
+ runLegacyTests: true
projects:
- name: name
desc: Human Description
@@ -22,6 +25,10 @@ parameters:
compatibilityAndroidTestProject: /optional/path/to/android.csproj
compatibilityiOSTestProject: /optional/path/to/ios.csproj
compatibilityiOSApp: /optional/path/to/app.csproj
+ legacyAndroidApp: /optional/path/to/app.csproj
+ legacyAndroidTestProject: /optional/path/to/android.csproj
+ legacyiOSTestProject: /optional/path/to/ios.csproj
+ legacyiOSApp: /optional/path/to/app.csproj
stages:
@@ -170,7 +177,6 @@ stages:
provisionatorChannel: ${{ parameters.provisionatorChannel }}
agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }}
-
- stage: ios_compatibility_ui_tests
displayName: iOS Compatibility UITests
dependsOn: []
@@ -206,3 +212,68 @@ stages:
device: ios-simulator-64_${{ version }}
provisionatorChannel: ${{ parameters.provisionatorChannel }}
agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }}
+ - ${{ if eq(parameters.runLegacyTests, true) }}:
+ - stage: android_legacy_ui_tests
+ displayName: Android Legacy UITests
+ dependsOn: []
+ jobs:
+ - ${{ each project in parameters.projects }}:
+ - ${{ if ne(project.android, '') }}:
+ - ${{ each api in parameters.androidApiLevels }}:
+ - ${{ if not(containsValue(project.androidApiLevelsExclude, api)) }}:
+ - job: android_legacy_ui_tests_${{ project.name }}_${{ api }}
+ timeoutInMinutes: 240
+ workspace:
+ clean: all
+ displayName: ${{ coalesce(project.desc, project.name) }} (API ${{ api }})
+ pool: ${{ parameters.androidLegacyPool }}
+ variables:
+ REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE)
+ steps:
+ - template: ui-tests-legacy-steps.yml
+ parameters:
+ platform: android
+ version: ${{ api }}
+ path: ${{ project.legacyAndroidTestProject }}
+ app: ${{ project.legacyAndroidApp }}
+ targetSample: "dotnet-legacy-controlgallery-android"
+ ${{ if eq(api, 27) }}:
+ device: android-emulator-32_${{ api }}
+ ${{ if not(eq(api, 27)) }}:
+ device: android-emulator-64_${{ api }}
+ provisionatorChannel: ${{ parameters.provisionatorChannel }}
+ agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }}
+
+ - stage: ios_legacy_ui_tests
+ displayName: iOS Legacy UITests
+ dependsOn: []
+ jobs:
+ - ${{ each project in parameters.projects }}:
+ - ${{ if ne(project.ios, '') }}:
+ - ${{ each version in parameters.iosVersions }}:
+ - ${{ if not(containsValue(project.iosVersionsExclude, version)) }}:
+ - job: ios_legacy_ui_tests_${{ project.name }}_${{ replace(version, '.', '_') }}
+ timeoutInMinutes: 240
+ workspace:
+ clean: all
+ displayName: ${{ coalesce(project.desc, project.name) }} (v${{ version }})
+ pool: ${{ parameters.iosLegacyPool }}
+ variables:
+ REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE)
+ steps:
+ - template: ui-tests-legacy-steps.yml
+ parameters:
+ platform: ios
+ ${{ if eq(version, 'latest') }}:
+ version: 16.4
+ ${{ if ne(version, 'latest') }}:
+ version: ${{ version }}
+ path: ${{ project.legacyiOSTestProject }}
+ app: ${{ project.legacyiOSApp }}
+ targetSample: "dotnet-legacy-controlgallery-ios"
+ ${{ if eq(version, 'latest') }}:
+ device: ios-simulator-64
+ ${{ if ne(version, 'latest') }}:
+ device: ios-simulator-64_${{ version }}
+ provisionatorChannel: ${{ parameters.provisionatorChannel }}
+ agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }}
\ No newline at end of file
diff --git a/eng/pipelines/ui-tests.yml b/eng/pipelines/ui-tests.yml
index c2269ed23e66..46d4de15c7de 100644
--- a/eng/pipelines/ui-tests.yml
+++ b/eng/pipelines/ui-tests.yml
@@ -109,6 +109,23 @@ parameters:
- macOS.Name -equals Ventura
- macOS.Architecture -equals x64
+ - name: androidLegacyPool
+ type: object
+ default:
+ name: $(androidTestsVmPool)
+ vmImage: $(androidTestsVmImage)
+ demands:
+ - macOS.Name -equals Ventura
+ - macOS.Architecture -equals x64
+
+ - name: iosLegacyPool
+ type: object
+ default:
+ name: $(iosTestsVmPool)
+ vmImage: $(iosTestsVmImage)
+ demands:
+ - macOS.Name -equals Ventura
+ - macOS.Architecture -equals x64
resources:
repositories:
@@ -128,6 +145,8 @@ stages:
macosPool: ${{ parameters.macosPool }}
androidCompatibilityPool: ${{ parameters.androidCompatibilityPool }}
iosCompatibilityPool: ${{ parameters.iosCompatibilityPool }}
+ iosLegacyPool: ${{ parameters.iosLegacyPool }}
+ androidLegacyPool: ${{ parameters.androidLegacyPool }}
agentPoolAccessToken: $(AgentPoolAccessToken)
${{ if or(parameters.BuildEverything, and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'devdiv'))) }}:
androidApiLevels: [ 30 ]
@@ -153,4 +172,8 @@ stages:
compatibilityAndroidTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/Android.UITests/Compatibility.ControlGallery.Android.UITests.csproj
compatibilityiOSApp: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/src/iOS/Compatibility.ControlGallery.iOS.csproj
compatibilityiOSTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/iOS.UITests/Compatibility.ControlGallery.iOS.UITests.csproj
+ legacyAndroidApp: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/src/Android/Compatibility.ControlGallery.Android.csproj
+ legacyAndroidTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj
+ legacyiOSApp: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/src/iOS/Compatibility.ControlGallery.iOS.csproj
+ legacyiOSTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj
diff --git a/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs b/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs
index 2ec5edd2c0f9..0d555a4dbc19 100644
--- a/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs
+++ b/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs
@@ -4,6 +4,7 @@
using Android.Content;
using Android.Content.PM;
using Android.OS;
+using Android.Runtime;
using Java.Interop;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Controls.ControlGallery;
@@ -28,6 +29,7 @@ namespace Microsoft.Maui.Controls.ControlGallery.Android
DataScheme = "http", DataHost = App.AppName, DataPathPrefix = "/gallery/"
)
]
+ [Register("com.microsoft.mauicompatibilitygallery.MainActivity")]
public partial class Activity1 : MauiAppCompatActivity
{
App App => Microsoft.Maui.Controls.Application.Current as App;
diff --git a/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj
new file mode 100644
index 000000000000..5d66c158ad15
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj
@@ -0,0 +1,43 @@
+
+
+
+ $(_MauiDotNetTfm)
+ enable
+ enable
+ true
+ UITests
+ $(DefineConstants);ANDROID
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Android.Appium.UITests/PlatformSpecificSampleTest.cs b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/PlatformSpecificSampleTest.cs
new file mode 100644
index 000000000000..98435f47f3e8
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/PlatformSpecificSampleTest.cs
@@ -0,0 +1,16 @@
+using NUnit.Framework;
+
+namespace UITests;
+
+public class PlatformSpecificSampleTest : UITest
+{
+ public PlatformSpecificSampleTest(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ [Test]
+ public void SampleTest()
+ {
+ Driver?.GetScreenshot().SaveAsFile($"{nameof(SampleTest)}.png");
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/AppiumServerHelper.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/AppiumServerHelper.cs
new file mode 100644
index 000000000000..6165a3e9a341
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/AppiumServerHelper.cs
@@ -0,0 +1,33 @@
+using OpenQA.Selenium.Appium.Service;
+
+namespace UITests;
+
+public static class AppiumServerHelper
+{
+ static AppiumLocalService? AppiumLocalService;
+
+ public const string DefaultHostAddress = "127.0.0.1";
+ public const int DefaultHostPort = 4723;
+
+ public static void StartAppiumLocalServer(string host = DefaultHostAddress,
+ int port = DefaultHostPort)
+ {
+ if (AppiumLocalService is not null)
+ {
+ return;
+ }
+
+ var builder = new AppiumServiceBuilder()
+ .WithIPAddress(host)
+ .UsingPort(port);
+
+ // Start the server with the builder
+ AppiumLocalService = builder.Build();
+ AppiumLocalService.Start();
+ }
+
+ public static void DisposeAppiumLocalServer()
+ {
+ AppiumLocalService?.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/ControlGallery.Shared.Appium.UITests.csproj b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/ControlGallery.Shared.Appium.UITests.csproj
new file mode 100644
index 000000000000..5c5de9da4a79
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/ControlGallery.Shared.Appium.UITests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ $(_MauiDotNetTfm)
+ False
+ False
+ UITests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(DefineConstants);ANDROID
+
+
+
+ $(DefineConstants);IOS;IOSUITEST
+
+
+
+ $(DefineConstants);WINDOWS;WINTEST
+
+
+
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/GalleryQueries.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/GalleryQueries.cs
new file mode 100644
index 000000000000..889551735add
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/GalleryQueries.cs
@@ -0,0 +1,30 @@
+namespace UITests
+{
+ internal static class GalleryQueries
+ {
+ public const string ActivityIndicatorGallery = "ActivityIndicator Gallery";
+ public const string BoxViewGallery = "BoxView Gallery";
+ public const string ButtonGallery = "Button Gallery";
+ public const string CheckBoxGallery = "CheckBox Gallery";
+ public const string CollectionViewGallery = "CollectionView Gallery";
+ public const string CarouselViewGallery = "CarouselView Gallery";
+ public const string DatePickerGallery = "DatePicker Gallery";
+ public const string EditorGallery = "Editor Gallery";
+ public const string EntryGallery = "Entry Gallery";
+ public const string FrameGallery = "Frame Gallery";
+ public const string ImageGallery = "Image Gallery";
+ public const string ImageButtonGallery = "Image Button Gallery";
+ public const string LabelGallery = "Label Gallery";
+ public const string ListViewGallery = "ListView Gallery";
+ public const string PickerGallery = "Picker Gallery";
+ public const string ProgressBarGallery = "ProgressBar Gallery";
+ public const string RadioButtonGallery = "RadioButton Core Gallery";
+ public const string ScrollViewGallery = "ScrollView Gallery";
+ public const string SearchBarGallery = "SearchBar Gallery";
+ public const string SliderGallery = "Slider Gallery";
+ public const string StepperGallery = "Stepper Gallery";
+ public const string SwitchGallery = "Switch Gallery";
+ public const string TimePickerGallery = "TimePicker Gallery";
+ public const string WebViewGallery = "WebView Gallery";
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/IssuesUITest.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/IssuesUITest.cs
new file mode 100644
index 000000000000..b8931ebf2c4e
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/IssuesUITest.cs
@@ -0,0 +1,63 @@
+using NUnit.Framework;
+using UITest.Appium;
+
+namespace UITests
+{
+ public abstract class IssuesUITest : UITest
+ {
+ public IssuesUITest(TestDevice device) : base(device) { }
+
+ protected override void FixtureSetup()
+ {
+ int retries = 0;
+ while (true)
+ {
+ try
+ {
+ base.FixtureSetup();
+ NavigateToIssue(Issue);
+ break;
+ }
+ catch (Exception e)
+ {
+ TestContext.Error.WriteLine($">>>>> {DateTime.Now} The FixtureSetup threw an exception. Attempt {retries}/{SetupMaxRetries}.{Environment.NewLine}Exception details: {e}");
+ if (retries++ < SetupMaxRetries)
+ {
+ Reset();
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+ }
+
+ protected override void FixtureTeardown()
+ {
+ base.FixtureTeardown();
+ try
+ {
+ this.Back();
+ RunningApp.Tap("GoBackToGalleriesButton");
+ }
+ catch (Exception e)
+ {
+ var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
+ TestContext.Error.WriteLine($">>>>> {DateTime.Now} The FixtureTeardown threw an exception during {name}.{Environment.NewLine}Exception details: {e}");
+ }
+ }
+
+ public abstract string Issue { get; }
+
+ private void NavigateToIssue(string issue)
+ {
+ RunningApp.NavigateToIssues();
+
+ RunningApp.EnterText("SearchBarGo", issue);
+
+ RunningApp.WaitForElement("SearchButton");
+ RunningApp.Tap("SearchButton");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestAttributes.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestAttributes.cs
new file mode 100644
index 000000000000..39b219bf0846
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestAttributes.cs
@@ -0,0 +1,71 @@
+namespace UITests
+{
+ public static class Test
+ {
+ public enum Button
+ {
+ Clicked,
+ Command,
+ Text,
+ TextColor,
+ Font,
+ BorderWidth,
+ BorderColor,
+ BorderRadius,
+ Image,
+ Padding,
+ Pressed,
+ LineBreakMode
+ }
+
+ public enum ImageButton
+ {
+ Source,
+ Aspect,
+ IsOpaque,
+ IsLoading,
+ AspectFill,
+ AspectFit,
+ Fill,
+ BorderColor,
+ CornerRadius,
+ BorderWidth,
+ Clicked,
+ Command,
+ Image,
+ Pressed,
+ Padding
+ }
+
+ public enum VisualElement
+ {
+ IsEnabled,
+ Navigation,
+ InputTransparent,
+ Layout,
+ X,
+ Y,
+ AnchorX,
+ AnchorY,
+ TranslationX,
+ TranslationY,
+ Width,
+ Height,
+ Bounds,
+ Rotation,
+ RotationX,
+ RotationY,
+ Scale,
+ IsVisible,
+ Opacity,
+ BackgroundColor,
+ Background,
+ IsFocused,
+ Focus,
+ Unfocus,
+ Focused,
+ Unfocused,
+ Default
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestContextSetupFixture.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestContextSetupFixture.cs
new file mode 100644
index 000000000000..b8631bc9026f
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestContextSetupFixture.cs
@@ -0,0 +1,17 @@
+using UITest.Appium;
+
+// SetupFixture runs once for all tests under the same namespace, if placed outside the namespace it will run once for all tests in the assembly
+namespace UITests
+{
+ public class TestContextSetupFixture : UITestContextSetupFixture
+ {
+ AppiumServerContext? _appiumServerContext;
+
+ public override void Initialize()
+ {
+ _appiumServerContext = new AppiumServerContext();
+ _appiumServerContext.CreateAndStartServer();
+ _serverContext = _appiumServerContext;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestDevice.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestDevice.cs
new file mode 100644
index 000000000000..bb0f09240ac1
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestDevice.cs
@@ -0,0 +1,10 @@
+namespace UITests
+{
+ public enum TestDevice
+ {
+ Windows,
+ Android,
+ iOS,
+ Mac
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/Tests/Issues/Issue11853.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/Tests/Issues/Issue11853.cs
new file mode 100644
index 000000000000..1aa0f8291d18
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/Tests/Issues/Issue11853.cs
@@ -0,0 +1,32 @@
+#if IOS
+using NUnit.Framework;
+using UITest.Appium;
+
+namespace UITests
+{
+ public class Issue11853 : IssuesUITest
+ {
+ const string Run = "Run";
+
+ public Issue11853(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ public override string Issue => "[Bug][iOS] Concurrent issue leading to crash in SemaphoreSlim.Release in ObservableItemsSource";
+
+ [Test]
+ [Category(UITestCategories.CollectionView)]
+ public void JustWhalingAwayOnTheCollectionViewWithAddsAndClearsShouldNotCrash()
+ {
+ RunningApp.WaitForElement(Run);
+ RunningApp.Tap(Run);
+ Task.Delay(5000).Wait();
+ RunningApp.Tap(Run);
+ Task.Delay(5000).Wait();
+
+ // If we can still find the button, then we didn't crash
+ RunningApp.WaitForElement(Run);
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITest.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITest.cs
new file mode 100644
index 000000000000..f129ab406969
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITest.cs
@@ -0,0 +1,169 @@
+using NUnit.Framework;
+using UITest.Core;
+using VisualTestUtils;
+using VisualTestUtils.MagickNet;
+
+namespace UITests
+{
+#if ANDROID
+ [TestFixture(TestDevice.Android)]
+#elif IOSUITEST
+ [TestFixture(TestDevice.iOS)]
+#elif MACUITEST
+ [TestFixture(TestDevice.Mac)]
+#elif WINTEST
+ [TestFixture(TestDevice.Windows)]
+#else
+ [TestFixture(TestDevice.iOS)]
+ [TestFixture(TestDevice.Mac)]
+ [TestFixture(TestDevice.Windows)]
+ [TestFixture(TestDevice.Android)]
+#endif
+ public abstract class UITest : UITestBase
+ {
+ protected const int SetupMaxRetries = 1;
+ readonly VisualRegressionTester _visualRegressionTester;
+ readonly IImageEditorFactory _imageEditorFactory;
+ readonly VisualTestContext _visualTestContext;
+
+ protected UITest(TestDevice testDevice) : base(testDevice)
+ {
+ string? ciArtifactsDirectory = Environment.GetEnvironmentVariable("BUILD_ARTIFACTSTAGINGDIRECTORY");
+ if (ciArtifactsDirectory != null)
+ ciArtifactsDirectory = Path.Combine(ciArtifactsDirectory, "Controls.AppiumTests");
+
+ string assemblyDirectory = Path.GetDirectoryName(System.AppDomain.CurrentDomain.BaseDirectory)!;
+ string projectRootDirectory = Path.GetFullPath(Path.Combine(assemblyDirectory, "..", "..", ".."));
+ _visualRegressionTester = new VisualRegressionTester(testRootDirectory: projectRootDirectory,
+ visualComparer: new MagickNetVisualComparer(),
+ visualDiffGenerator: new MagickNetVisualDiffGenerator(),
+ ciArtifactsDirectory: ciArtifactsDirectory);
+
+ _imageEditorFactory = new MagickNetImageEditorFactory();
+ _visualTestContext = new VisualTestContext();
+ }
+
+ public override IConfig GetTestConfig()
+ {
+ IConfig config = new Config();
+ config.SetProperty("AppId", "com.microsoft.mauicompatibilitygallery");
+
+ switch (_testDevice)
+ {
+ case TestDevice.Android:
+ config.SetProperty("DeviceName", Environment.GetEnvironmentVariable("DEVICE_SKIN") ?? "");
+ config.SetProperty("PlatformVersion", Environment.GetEnvironmentVariable("PLATFORM_VERSION") ?? "");
+ config.SetProperty("Udid", Environment.GetEnvironmentVariable("DEVICE_UDID") ?? "");
+ break;
+ case TestDevice.iOS:
+ config.SetProperty("DeviceName", Environment.GetEnvironmentVariable("DEVICE_NAME") ?? "iPhone X");
+ config.SetProperty("PlatformVersion", Environment.GetEnvironmentVariable("PLATFORM_VERSION") ?? "17.0");
+ config.SetProperty("Udid", Environment.GetEnvironmentVariable("DEVICE_UDID") ?? "");
+ break;
+ }
+
+ return config;
+ }
+
+ public void VerifyScreenshot(string? name = null)
+ {
+ string deviceName = GetTestConfig().GetProperty("DeviceName") ?? string.Empty;
+ // Remove the XHarness suffix if present
+ deviceName = deviceName.Replace(" - created by XHarness", "", StringComparison.Ordinal);
+
+ /*
+ Determine the environmentName, used as the directory name for visual testing snaphots. Here are the rules/conventions:
+ - Names are lower case, no spaces.
+ - By default, the name matches the platform (android, ios, windows, or mac).
+ - Each platform has a default device (or set of devices) - if the snapshot matches the default no suffix is needed (e.g. just ios).
+ - If tests are run on secondary devices that produce different snapshots, the device name is used as suffix (e.g. ios-iphonex).
+ - If tests are run on secondary devices with multiple OS versions that produce different snapshots, both device name and os version are
+ used as a suffix (e.g. ios-iphonex-16_4). We don't have any cases of this today but may eventually. The device name comes first here,
+ before os version, because most visual testing differences come from different sceen size (a device thing), not OS version differences,
+ but both can happen.
+ */
+ string environmentName = string.Empty;
+
+ switch (_testDevice)
+ {
+ case TestDevice.Android:
+ if (deviceName == "Nexus 5X")
+ {
+ environmentName = "android";
+ }
+ else
+ {
+ Assert.Fail($"Android visual tests should be run on an Nexus 5X (API 30) emulator image, but the current device is '{deviceName}'. Follow the steps on the MAUI UI testing wiki.");
+ }
+ break;
+
+ case TestDevice.iOS:
+ if (deviceName == "iPhone Xs (iOS 17.2)")
+ {
+ environmentName = "ios";
+ }
+ else if (deviceName == "iPhone X (iOS 16.4)")
+ {
+ environmentName = "ios-iphonex";
+ }
+ else
+ {
+ Assert.Fail($"iOS visual tests should be run on iPhone Xs (iOS 17.2) or iPhone X (iOS 16.4) simulator images, but the current device is '{deviceName}'. Follow the steps on the MAUI UI testing wiki.");
+ }
+ break;
+
+ case TestDevice.Windows:
+ environmentName = "windows";
+ break;
+
+ case TestDevice.Mac:
+ // For now, ignore visual tests on Mac Catalyst since the Appium screenshot on Mac (unlike Windows)
+ // is of the entire screen, not just the app. Later when xharness relay support is in place to
+ // send a message to the MAUI app to get the screenshot, we can use that to just screenshot
+ // the app.
+ Assert.Ignore("MacCatalyst isn't supported yet for visual tests");
+ break;
+
+ default:
+ throw new NotImplementedException($"Unknown device type {_testDevice}");
+ }
+
+ name ??= TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
+
+ byte[] screenshotPngBytes = RunningApp.Screenshot() ?? throw new InvalidOperationException("Failed to get screenshot");
+
+ var actualImage = new ImageSnapshot(screenshotPngBytes, ImageSnapshotFormat.PNG);
+
+ // For Android and iOS, crop off the OS status bar at the top since it's not part of the
+ // app itself and contains the time, which always changes. For WinUI, crop off the title
+ // bar at the top as it varies slightly based on OS theme and is also not part of the app.
+ int cropFromTop = _testDevice switch
+ {
+ TestDevice.Android => 60,
+ TestDevice.iOS => environmentName == "ios-iphonex" ? 90 : 110,
+ TestDevice.Windows => 32,
+ _ => 0,
+ };
+
+ // For Android also crop the 3 button nav from the bottom, since it's not part of the
+ // app itself and the button color can vary (the buttons change clear briefly when tapped)
+ int cropFromBottom = _testDevice switch
+ {
+ TestDevice.Android => 125,
+ _ => 0,
+ };
+
+ if (cropFromTop > 0 || cropFromBottom > 0)
+ {
+ IImageEditor imageEditor = _imageEditorFactory.CreateImageEditor(actualImage);
+ (int width, int height) = imageEditor.GetSize();
+
+ imageEditor.Crop(0, cropFromTop, width, height - cropFromTop - cropFromBottom);
+
+ actualImage = imageEditor.GetUpdatedImage();
+ }
+
+ _visualRegressionTester.VerifyMatchesSnapshot(name!, actualImage, environmentName: environmentName, testContext: _visualTestContext);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestBase.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestBase.cs
new file mode 100644
index 000000000000..936e5a1f7780
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestBase.cs
@@ -0,0 +1,175 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using NUnit.Framework;
+using NUnit.Framework.Interfaces;
+using UITest.Core;
+
+namespace UITests
+{
+ public abstract class UITestBase : UITestContextBase
+ {
+ public UITestBase(TestDevice testDevice)
+ : base(testDevice)
+ {
+ }
+
+ [SetUp]
+ public void RecordTestSetup()
+ {
+ var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
+ TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {name} Start");
+ }
+
+ [TearDown]
+ public void RecordTestTeardown()
+ {
+ var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
+ TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {name} Stop");
+ }
+
+ protected virtual void FixtureSetup()
+ {
+ var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
+ TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {nameof(FixtureSetup)} for {name}");
+ }
+
+ protected virtual void FixtureTeardown()
+ {
+ var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name;
+ TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {nameof(FixtureTeardown)} for {name}");
+ }
+
+ [TearDown]
+ public void UITestBaseTearDown()
+ {
+ if (App.AppState == ApplicationState.NotRunning)
+ {
+ SaveDeviceDiagnosticInfo();
+
+ Reset();
+ FixtureSetup();
+
+ // Assert.Fail will immediately exit the test which is desirable as the app is not
+ // running anymore so we can't capture any UI structures or any screenshots
+ Assert.Fail("The app was expected to be running still, investigate as possible crash");
+ }
+
+ var testOutcome = TestContext.CurrentContext.Result.Outcome;
+ if (testOutcome == ResultState.Error ||
+ testOutcome == ResultState.Failure)
+ {
+ SaveDeviceDiagnosticInfo();
+ SaveUIDiagnosticInfo();
+ }
+ }
+
+ [OneTimeSetUp]
+ public void OneTimeSetup()
+ {
+ InitialSetup(UITestContextSetupFixture.ServerContext);
+ try
+ {
+ FixtureSetup();
+ }
+ catch
+ {
+ SaveDeviceDiagnosticInfo();
+ SaveUIDiagnosticInfo();
+ throw;
+ }
+ }
+
+ [OneTimeTearDown]
+ public void OneTimeTearDown()
+ {
+ var outcome = TestContext.CurrentContext.Result.Outcome;
+
+ // We only care about setup failures as regular test failures will already do logging
+ if (outcome.Status == ResultState.SetUpFailure.Status &&
+ outcome.Site == ResultState.SetUpFailure.Site)
+ {
+ SaveDeviceDiagnosticInfo();
+ SaveUIDiagnosticInfo();
+ }
+
+ FixtureTeardown();
+ }
+
+ void SaveDeviceDiagnosticInfo([CallerMemberName] string? note = null)
+ {
+ var types = App.GetLogTypes().ToArray();
+ TestContext.Progress.WriteLine($">>>>> {DateTime.Now} Log types: {string.Join(", ", types)}");
+
+ foreach (var logType in new[] { "logcat" })
+ {
+ if (!types.Contains(logType, StringComparer.InvariantCultureIgnoreCase))
+ continue;
+
+ var logsPath = GetGeneratedFilePath($"AppLogs-{logType}.log", note);
+ if (logsPath is not null)
+ {
+ var entries = App.GetLogEntries(logType);
+ File.WriteAllLines(logsPath, entries);
+
+ AddTestAttachment(logsPath, Path.GetFileName(logsPath));
+ }
+ }
+ }
+
+ void SaveUIDiagnosticInfo([CallerMemberName] string? note = null)
+ {
+ var screenshotPath = GetGeneratedFilePath("ScreenShot.png", note);
+ if (screenshotPath is not null)
+ {
+ _ = RunningApp.Screenshot(screenshotPath);
+
+ AddTestAttachment(screenshotPath, Path.GetFileName(screenshotPath));
+ }
+
+ var pageSourcePath = GetGeneratedFilePath("PageSource.txt", note);
+ if (pageSourcePath is not null)
+ {
+ File.WriteAllText(pageSourcePath, App.ElementTree);
+
+ AddTestAttachment(pageSourcePath, Path.GetFileName(pageSourcePath));
+ }
+ }
+
+ string? GetGeneratedFilePath(string filename, string? note = null)
+ {
+ // App could be null if UITestContext was not able to connect to the test process (e.g. port already in use etc...)
+ if (UITestContext is null)
+ return null;
+
+ if (string.IsNullOrEmpty(note))
+ note = "-";
+ else
+ note = $"-{note}-";
+
+ filename = $"{Path.GetFileNameWithoutExtension(filename)}-{Guid.NewGuid().ToString("N")}{Path.GetExtension(filename)}";
+
+ var logDir =
+ Path.GetDirectoryName(Environment.GetEnvironmentVariable("APPIUM_LOG_FILE") ??
+ Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))!;
+
+ var name =
+ TestContext.CurrentContext.Test.MethodName ??
+ TestContext.CurrentContext.Test.Name;
+
+ return Path.Combine(logDir, $"{name}-{_testDevice}{note}{filename}");
+ }
+
+ void AddTestAttachment(string filePath, string? description = null)
+ {
+ try
+ {
+ TestContext.AddTestAttachment(filePath, description);
+ }
+ catch (FileNotFoundException e) when (e.Message == "Test attachment file path could not be found.")
+ {
+ // Add the file path to better troubleshoot when these errors occur
+ throw new FileNotFoundException($"Test attachment file path could not be found: '{filePath}' {description}", e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestCategories.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestCategories.cs
new file mode 100644
index 000000000000..aa8c41a63837
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestCategories.cs
@@ -0,0 +1,68 @@
+namespace UITests
+{
+ internal static class UITestCategories
+ {
+ public const string ViewBaseTests = "ViewBaseTests";
+ public const string ActionSheet = "ActionSheet";
+ public const string ActivityIndicator = "ActivityIndicator";
+ public const string Animation = "Animation";
+ public const string AutomationId = "AutomationID";
+ public const string BoxView = "BoxView";
+ public const string Button = "Button";
+ public const string CarouselView = "CarouselView";
+ public const string Cells = "Cells";
+ public const string CheckBox = "CheckBox";
+ public const string CollectionView = "CollectionView";
+ public const string ContextActions = "ContextActions";
+ public const string DatePicker = "DatePicker";
+ public const string DragAndDrop = "DragAndDrop";
+ public const string DisplayAlert = "DisplayAlert";
+ public const string Editor = "Editor";
+ public const string Entry = "Entry";
+ public const string Frame = "Frame";
+ public const string Image = "Image";
+ public const string ImageButton = "ImageButton";
+ public const string Label = "Label";
+ public const string Layout = "Layout";
+ public const string ListView = "ListView";
+ public const string UwpIgnore = "UwpIgnore";
+ public const string LifeCycle = "Lifecycle";
+ public const string FlyoutPage = "FlyoutPage";
+ public const string Picker = "Picker";
+ public const string ProgressBar = "ProgressBar";
+ public const string RequiresInternetConnection = "RequiresInternetConnection";
+ public const string RootGallery = "RootGallery";
+ public const string ScrollView = "ScrollView";
+ public const string SearchBar = "SearchBar";
+ public const string Slider = "Slider";
+ public const string Stepper = "Stepper";
+ public const string Switch = "Switch";
+ public const string SwipeView = "SwipeView";
+ public const string TableView = "TableView";
+ public const string TimePicker = "TimePicker";
+ public const string ToolbarItem = "ToolbarItem";
+ public const string WebView = "WebView";
+ public const string Maps = "Maps";
+ public const string InputTransparent = "InputTransparent";
+ public const string IsEnabled = "IsEnabled";
+ public const string Gestures = "Gestures";
+ public const string Navigation = "Navigation";
+ public const string Effects = "Effects";
+ public const string Focus = "Focus";
+ public const string ManualReview = "ManualReview";
+ public const string Performance = "Performance";
+ public const string AppLinks = "AppLinks";
+ public const string Shell = "Shell";
+ public const string TabbedPage = "TabbedPage";
+ public const string CustomHandlers = "CustomHandlers";
+ public const string Page = "Page";
+ public const string RefreshView = "RefreshView";
+ public const string TitleView = "TitleView";
+ public const string DisplayPrompt = "DisplayPrompt";
+ public const string IndicatorView = "IndicatorView";
+ public const string RadioButton = "RadioButton";
+ public const string Shape = "Shape";
+ public const string Accessibility = "Accessibility";
+ public const string Brush = "Brush";
+ }
+}
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestContextBase.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestContextBase.cs
new file mode 100644
index 000000000000..68f0a129e5dd
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestContextBase.cs
@@ -0,0 +1,91 @@
+using OpenQA.Selenium.Appium;
+using UITest.Appium;
+using UITest.Core;
+
+namespace UITests
+{
+ public abstract class UITestContextBase
+ {
+ static IUIClientContext? UiTestContext;
+ IServerContext? _context;
+ protected TestDevice _testDevice;
+
+ public UITestContextBase(TestDevice testDevice)
+ {
+ _testDevice = testDevice;
+ }
+
+ public static IUIClientContext? UITestContext { get { return UiTestContext; } }
+
+ protected AppiumDriver? Driver
+ {
+ get
+ {
+ if (App is AppiumApp app)
+ {
+ return app.Driver;
+ }
+
+ return null;
+ }
+ }
+
+ public TestDevice Device
+ {
+ get
+ {
+ return UITestContext == null
+ ? throw new InvalidOperationException($"Call {nameof(InitialSetup)} before accessing the {nameof(Device)} property.")
+ : UITestContext.Config.GetProperty("TestDevice");
+ }
+ }
+
+ public IApp App
+ {
+ get
+ {
+ return UITestContext == null
+ ? throw new InvalidOperationException($"Call {nameof(InitialSetup)} before accessing the {nameof(App)} property.")
+ : UITestContext.App;
+ }
+ }
+
+ internal IApp RunningApp => App;
+
+ public abstract IConfig GetTestConfig();
+
+ public void InitialSetup(IServerContext context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ InitialSetup(context, false);
+ }
+
+ public void Reset()
+ {
+ if (_context == null)
+ {
+ throw new InvalidOperationException($"Cannot {nameof(Reset)} if {nameof(InitialSetup)} has not been called.");
+ }
+
+ InitialSetup(_context, true);
+ }
+
+ private void InitialSetup(IServerContext context, bool reset)
+ {
+ var testConfig = GetTestConfig();
+ testConfig.SetProperty("TestDevice", _testDevice);
+
+ // Check to see if we have a context already from a previous test and re-use it as creating the driver is expensive
+ if (reset || UiTestContext == null)
+ {
+ UiTestContext?.Dispose();
+ UiTestContext = context.CreateUIClientContext(testConfig);
+ }
+
+ if (UiTestContext == null)
+ {
+ throw new InvalidOperationException("Failed to get the driver.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestExtensions.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestExtensions.cs
new file mode 100644
index 000000000000..c1b1b1461e67
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestExtensions.cs
@@ -0,0 +1,90 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+using System.Drawing;
+
+namespace UITests
+{
+ public static class UITestExtensions
+ {
+ const string GoToTestButtonId = "GoToTestButton";
+
+ public static void Back(this UITestContextBase testBase)
+ {
+ if (testBase.Device == TestDevice.Android)
+ {
+ var query = testBase.App.Query.ByAccessibilityId("Navigate up").First();
+ query.Click();
+ }
+ else if (testBase.Device == TestDevice.iOS || testBase.Device == TestDevice.Mac)
+ {
+ // Get the first NavigationBar we can find and the first button in it (the back button), index starts at 1
+ var queryBy = testBase.App.Query.ByClass("XCUIElementTypeNavigationBar").First().ByClass("XCUIElementTypeButton").First();
+ queryBy.Click();
+ }
+ else
+ {
+ testBase.RunningApp.FindElement("NavigationViewBackButton").Click();
+ }
+ }
+
+ public static void NavigateToGallery(this IApp app, string page)
+ {
+ app.WaitForElement(GoToTestButtonId, "Timed out waiting for Go To Test button to appear", TimeSpan.FromMinutes(2));
+ NavigateTo(app, page);
+ }
+
+ public static void NavigateTo(this IApp app, string text)
+ {
+ app.WaitForElement("SearchBar");
+ app.ClearText("SearchBar");
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ app.EnterText("SearchBar", text);
+ }
+ app.Tap(GoToTestButtonId);
+
+ app.WaitForNoElement(GoToTestButtonId, "Timed out waiting for Go To Test button to disappear", TimeSpan.FromMinutes(1));
+ }
+
+ public static void NavigateToIssues(this IApp app)
+ {
+ app.WaitForElement(GoToTestButtonId, "Timed out waiting for Go To Test button to appear", TimeSpan.FromMinutes(2));
+
+ app.WaitForElement("SearchBar");
+ app.ClearText("SearchBar");
+
+ app.Tap(GoToTestButtonId);
+ app.WaitForElement("TestCasesIssueList");
+ }
+
+ public static void IgnoreIfPlatforms(this UITestBase? test, IEnumerable devices, string? message = null)
+ {
+ foreach (var device in devices)
+ {
+ test?.IgnoreIfPlatform(device, message);
+ }
+ }
+
+ public static void IgnoreIfPlatform(this UITestBase? test, TestDevice device, string? message = null)
+ {
+ if (test != null && test.Device == device)
+ {
+ if (string.IsNullOrEmpty(message))
+ Assert.Ignore();
+ else
+ Assert.Ignore(message);
+ }
+ }
+
+ public static int CenterX(this Rectangle rect)
+ {
+ return rect.X + rect.Width / 2;
+ }
+
+ public static int CenterY(this Rectangle rect)
+ {
+ return rect.Y + rect.Height / 2;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestIgnoreAttributes.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestIgnoreAttributes.cs
new file mode 100644
index 000000000000..4113ca8aaa5e
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestIgnoreAttributes.cs
@@ -0,0 +1,112 @@
+using NUnit.Framework;
+
+namespace UITests
+{
+ public class IgnoredDuringMoveToAppium : IgnoreAttribute
+ {
+ public IgnoredDuringMoveToAppium() : base(nameof(IgnoredDuringMoveToAppium))
+ {
+ }
+ public IgnoredDuringMoveToAppium(string reason) : base(reason)
+ {
+ }
+ }
+
+ public class FailsOnAllPlatforms : IgnoreAttribute
+ {
+ public FailsOnAllPlatforms() : base(nameof(FailsOnAndroid))
+ {
+ }
+ public FailsOnAllPlatforms(string reason) : base(reason)
+ {
+ }
+ }
+
+#if ANDROID
+ public class FailsOnAndroid : IgnoreAttribute
+ {
+ public FailsOnAndroid() : base(nameof(FailsOnAndroid))
+ {
+ }
+ public FailsOnAndroid(string reason) : base(reason)
+ {
+ }
+ }
+#else
+ public class FailsOnAndroid : CategoryAttribute
+ {
+ public FailsOnAndroid() : base(nameof(FailsOnAndroid))
+ {
+ }
+ public FailsOnAndroid(string name) : base(name)
+ {
+ }
+ }
+#endif
+
+#if IOS
+ public class FailsOnIOS : IgnoreAttribute
+ {
+ public FailsOnIOS() : base(nameof(FailsOnIOS))
+ {
+ }
+ public FailsOnIOS(string reason) : base(reason)
+ {
+ }
+ }
+#else
+ public class FailsOnIOS : CategoryAttribute
+ {
+ public FailsOnIOS() : base(nameof(FailsOnIOS))
+ {
+ }
+ public FailsOnIOS(string name) : base(name)
+ {
+ }
+ }
+#endif
+
+#if MACCATALYST
+ public class FailsOnMac : IgnoreAttribute
+ {
+ public FailsOnMac() : base(nameof(FailsOnMac))
+ {
+ }
+ public FailsOnMac(string reason) : base(reason)
+ {
+ }
+ }
+#else
+ public class FailsOnMac : CategoryAttribute
+ {
+ public FailsOnMac() : base(nameof(FailsOnMac))
+ {
+ }
+ public FailsOnMac(string name) : base(name)
+ {
+ }
+ }
+#endif
+
+#if WINDOWS
+ public class FailsOnWindows : IgnoreAttribute
+ {
+ public FailsOnWindows() : base(nameof(FailsOnWindows))
+ {
+ }
+ public FailsOnWindows(string reason) : base(reason)
+ {
+ }
+ }
+#else
+ public class FailsOnWindows : CategoryAttribute
+ {
+ public FailsOnWindows() : base(nameof(FailsOnWindows))
+ {
+ }
+ public FailsOnWindows(string name) : base(name)
+ {
+ }
+ }
+#endif
+}
diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/VisualTestContext.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/VisualTestContext.cs
new file mode 100644
index 000000000000..38598c27c489
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/VisualTestContext.cs
@@ -0,0 +1,11 @@
+using NUnit.Framework;
+using VisualTestUtils;
+
+namespace UITests
+{
+ public class VisualTestContext : ITestContext
+ {
+ public void AddTestAttachment(string filePath, string? description = null) =>
+ TestContext.AddTestAttachment(filePath, description);
+ }
+}
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj
new file mode 100644
index 000000000000..4558c59d5333
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ $(_MauiDotNetTfm)
+ enable
+ enable
+ true
+ UITests
+ $(DefineConstants);IOS;IOSUITEST
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/PlatformSpecificSampleTest.cs b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/PlatformSpecificSampleTest.cs
new file mode 100644
index 000000000000..98435f47f3e8
--- /dev/null
+++ b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/PlatformSpecificSampleTest.cs
@@ -0,0 +1,16 @@
+using NUnit.Framework;
+
+namespace UITests;
+
+public class PlatformSpecificSampleTest : UITest
+{
+ public PlatformSpecificSampleTest(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ [Test]
+ public void SampleTest()
+ {
+ Driver?.GetScreenshot().SaveAsFile($"{nameof(SampleTest)}.png");
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj b/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj
index caa9fefa2c06..f5356ca4fbbb 100644
--- a/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj
+++ b/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ netstandard2.0; net8.0
diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue19509.cs b/src/Controls/tests/UITests/Tests/Issues/Issue19509.cs
index 3426ac05ad99..fef68ee04db2 100644
--- a/src/Controls/tests/UITests/Tests/Issues/Issue19509.cs
+++ b/src/Controls/tests/UITests/Tests/Issues/Issue19509.cs
@@ -1,4 +1,4 @@
-using NUnit.Framework;
+using NUnit.Framework;
using UITest.Appium;
using UITest.Core;
diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
index b0e8e2f4db5d..fa7afa785e18 100644
--- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
+++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs
@@ -24,6 +24,11 @@ public static void Click(this IApp app, string element)
app.FindElement(element).Click();
}
+ public static void Tap(this IApp app, string element)
+ {
+ app.FindElement(element).Click();
+ }
+
public static string? GetText(this IUIElement element)
{
var response = element.Command.Execute("getText", new Dictionary()