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()