Skip to content
7 changes: 7 additions & 0 deletions eng/cake/dotnet.cake
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,13 @@ Task("uitests-apphost")
properties.Add("_UseNativeAot", "true");
properties.Add("RuntimeIdentifier", "iossimulator-x64");
}

var useMaterial3 = Argument("usematerial3", false);
if (useMaterial3)
{
Information("Building with Material3 enabled");
properties.Add("UseMaterial3", "true");
}

if (useNuget)
{
Expand Down
49 changes: 44 additions & 5 deletions eng/pipelines/common/ui-tests-build-sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ parameters:
testFilter: ''
headless: true
runtimeVariant: 'Mono' #Mono, CoreCLR, NativeAOT
useMaterial3: false # NEW PARAMETER - Enable Material3 build

steps:
- ${{ if eq(parameters.platform, 'ios')}}:
Expand Down Expand Up @@ -53,9 +54,29 @@ steps:
- pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)"
displayName: 'Add .NET to PATH'

# Clean artifacts for Material3 build to ensure clean state
# Reference: Controls.TestCases.HostApp.csproj line 17-19 warning comment
- pwsh: |
if (("${{ parameters.useMaterial3 }}" -eq "true") -and ("${{ parameters.platform }}" -eq "android")) {
Write-Host "Cleaning artifacts folder for Material3 clean build"
Remove-Item -Path "$(System.DefaultWorkingDirectory)/artifacts" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Artifacts folder cleaned successfully"
}
displayName: 'Clean artifacts for Material3'

- pwsh: |
Get-Content $PSCommandPath
./build.ps1 --target=uitests-apphost --configuration="${{ parameters.configuration }}" --${{ parameters.platform }} --verbosity=diagnostic --usenuget=false --runtimevariant="${{ parameters.runtimeVariant }}"
$buildCommand = "./build.ps1 --target=uitests-apphost --configuration=${{ parameters.configuration }} --${{ parameters.platform }} --verbosity=diagnostic --usenuget=false --runtimevariant=${{ parameters.runtimeVariant }}"

# Add Material3 build argument if enabled
if (("${{ parameters.useMaterial3 }}" -eq "true") -and ("${{ parameters.platform }}" -eq "android")) {
$buildCommand += " --usematerial3=true"
Write-Host "Building with Material3 enabled"
} elseif ("${{ parameters.useMaterial3 }}" -eq "true") {
Write-Host "Material3 is only supported for Android platform, ignoring useMaterial3 parameter"
}

Invoke-Expression $buildCommand
displayName: 'Build the samples'

- bash: |
Expand All @@ -66,33 +87,51 @@ steps:
continueOnError: true

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'NativeAOT'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), succeeded())
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'NativeAOT'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'false'), succeeded())
artifact: ui-tests-samples

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'NativeAOT'), succeeded())
artifact: ui-tests-samples-nativeaot

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), succeeded())
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'false'), succeeded())
artifact: ui-tests-samples-coreclr

# Material3 artifacts
- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'true'), succeeded())
artifact: ui-tests-samples-material3

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'true'), succeeded())
artifact: ui-tests-samples-material3-coreclr

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(eq('${{ parameters.platform }}' , 'windows'), succeeded())
artifact: ui-tests-samples-windows

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'NativeAOT'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), failed())
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'NativeAOT'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'false'), failed())
artifact: ui-tests-samples_failed_$(System.JobAttempt)

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'NativeAOT'), failed())
artifact: ui-tests-samples-nativeaot_failed_$(System.JobAttempt)

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), failed())
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'false'), failed())
artifact: ui-tests-samples-coreclr_failed_$(System.JobAttempt)

# Material3 failure artifacts
- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'true'), failed())
artifact: ui-tests-samples-material3_failed_$(System.JobAttempt)

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'true'), failed())
artifact: ui-tests-samples-material3-coreclr_failed_$(System.JobAttempt)

- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(eq('${{ parameters.platform }}' , 'windows'), failed())
artifact: ui-tests-samples-windows_failed_$(System.JobAttempt)
Expand Down
17 changes: 15 additions & 2 deletions eng/pipelines/common/ui-tests-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ parameters:
platform: '' # [ android, ios, windows, catalyst ]
path: '' # path to csproj
device: '' # the xharness device to use
deviceType: '' # the device type/skin for Android emulator (e.g., pixel_3XL, Nexus 5X)
cakeArgs: '' # additional cake args
app: '' #path to app to test
version: '' #the iOS version'
provisionatorChannel: 'latest'
configuration: "Release"
runtimeVariant: "Mono"
useMaterial3: false # NEW PARAMETER - Enable Material3 build
testFilter: ''
headless: true
testConfigurationArgs: ''
Expand All @@ -16,17 +18,22 @@ parameters:

steps:
- task: DownloadPipelineArtifact@2
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'Mono'))
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'Mono'), eq('${{ parameters.useMaterial3 }}', 'false'))
inputs:
artifact: ui-tests-samples

- task: DownloadPipelineArtifact@2
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'Mono'), eq('${{ parameters.useMaterial3 }}', 'true'))
inputs:
artifact: ui-tests-samples-material3

- task: DownloadPipelineArtifact@2
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'NativeAOT'))
inputs:
artifact: ui-tests-samples-nativeaot

- task: DownloadPipelineArtifact@2
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'))
condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'false'))
inputs:
artifact: ui-tests-samples-coreclr

Expand Down Expand Up @@ -129,6 +136,12 @@ steps:
$command += " --runtimevariant=""${{ parameters.runtimeVariant }}"""
$command += " --results=""$(TestResultsDirectory)"" --binlog=""$(LogDirectory)"" ${{ parameters.cakeArgs }} --verbosity=diagnostic"

# Add device skin parameter if specified (for Android emulator hardware profile)
$deviceType = "${{ parameters.deviceType }}"
if ($deviceType) {
$command += " --skin=""$deviceType"""
}

$testFilter = ""
$testConfigrationArgs = "${{ parameters.testConfigurationArgs }}"

Expand Down
63 changes: 63 additions & 0 deletions eng/pipelines/common/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ parameters:
windowsBuildPool: { }
macosPool: { }
androidApiLevels: [ 30 ]
androidApiLevelsExtended: [ 36 ] # API 36 for Material3 tests with Pixel 3 XL
iosVersions: [ 'latest' ]
provisionatorChannel: 'latest'
timeoutInMinutes: 180
Expand All @@ -15,6 +16,7 @@ parameters:
categoryGroupsToTest:
# Make sure that this list is always up-to-date with src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs
# we might want to improve this somehow depending on how much the categories change over time
# Note: Material3 is intentionally excluded from this list and has its own dedicated build and test stages below with a separate test filter
- 'Accessibility,ActionSheet,ActivityIndicator,Animation,AppLinks'
- 'Border,BoxView,Brush,Button'
- 'CarouselView'
Expand Down Expand Up @@ -115,6 +117,27 @@ stages:
platform: windows
skipProvisioning: ${{ parameters.skipProvisioning }}

# Material3 Build Stage - Separate build with UseMaterial3=true (Android-only)
- stage: build_ui_tests_material3
displayName: Build UITests Material3 Sample App
dependsOn: []
jobs:
- job: build_ui_tests_material3
displayName: Build Sample App (Material3)
workspace:
clean: all
pool: ${{ parameters.androidPool }}
variables:
REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE)
APPIUM_HOME: $(System.DefaultWorkingDirectory)/.appium/
steps:
- template: ui-tests-build-sample.yml
parameters:
runtimeVariant: "Mono"
platform: android
useMaterial3: true
skipProvisioning: ${{ parameters.skipProvisioning }}

- stage: android_ui_tests
displayName: Android UITests
dependsOn: build_ui_tests
Expand Down Expand Up @@ -205,6 +228,46 @@ stages:
platform: 'Android'
artifactName: 'uitest-snapshot-results-android-$(System.StageName)-$(System.JobName)-$(System.JobAttempt)'

- stage: android_ui_tests_material3
displayName: Android UITests Material3
dependsOn: build_ui_tests_material3
jobs:
- ${{ each project in parameters.projects }}:
- ${{ if ne(project.android, '') }}:
- ${{ each api in parameters.androidApiLevelsExtended }}:
- ${{ if not(containsValue(project.androidApiLevelsExclude, api)) }}:
- job: M3_android_ui_tests_${{ project.name }}_${{ api }}
timeoutInMinutes: 240
workspace:
clean: all
displayName: ${{ coalesce(project.desc, project.name) }} Material3 (API ${{ api }})
pool: ${{ parameters.androidLinuxPool }}
variables:
REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE)
APPIUM_HOME: $(System.DefaultWorkingDirectory)/.appium/
steps:
- template: ui-tests-steps.yml
parameters:
platform: android
version: ${{ api }}
path: ${{ project.android }}
app: ${{ project.app }}
${{ if eq(api, 27) }}:
device: android-emulator-32_${{ api }}
${{ if not(eq(api, 27)) }}:
device: android-emulator-64_${{ api }}
deviceType: "pixel_3_xl"
provisionatorChannel: ${{ parameters.provisionatorChannel }}
useMaterial3: true
testFilter: "Material3"
skipProvisioning: ${{ parameters.skipProvisioning }}

# Collect and publish Android Material3 snapshot diffs
- template: ui-tests-collect-snapshot-diffs.yml
parameters:
platform: 'Android Material3'
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform name in the artifact parameters is 'Android Material3' with a space, which may cause issues with artifact naming or filtering. Consider using a consistent naming pattern without spaces, such as 'AndroidMaterial3' or 'Android-Material3', to match the pattern used in artifact names like 'uitest-snapshot-results-android-material3-...'.

Suggested change
platform: 'Android Material3'
platform: 'AndroidMaterial3'

Copilot uses AI. Check for mistakes.
artifactName: 'uitest-snapshot-results-android-material3-$(System.StageName)-$(System.JobName)-$(System.JobAttempt)'

- stage: ios_ui_tests_mono
displayName: iOS UITests Mono
dependsOn: build_ui_tests
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Material3CheckBoxDefaultAppearance">

<ScrollView>
<VerticalStackLayout Padding="20"
Spacing="16">
<Label Text="Material3 CheckBox Test"
FontSize="18"
FontAttributes="Bold"/>
<CheckBox
x:Name="DefaultCheckBox"
AutomationId="DefaultCheckBox"
IsChecked="True"/>
<Button
Text="Sample Button"/>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.None, 0, "Material3 CheckBox Testing", PlatformAffected.Android)]
public partial class Material3CheckBoxDefaultAppearance : ContentPage
{
public Material3CheckBoxDefaultAppearance()
{
InitializeComponent();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if ANDROID
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is wrapped in an #if ANDROID directive which limits it to Android only. According to the UI Testing Guidelines (CodingGuidelineID: 1000002), platform-specific directives should only be used when there is a specific technical limitation. Since Material3 is Android-specific by design (as indicated by the PlatformAffected.Android in the HostApp), this usage is acceptable. However, the guideline states to "document any platform-specific limitations with clear comments". Consider adding a comment above the directive explaining why this test is Android-only, such as: // Material3 is Android-specific

Copilot uses AI. Check for mistakes.
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Material3CheckBoxDefaultAppearanceTest : _IssuesUITest
{
public Material3CheckBoxDefaultAppearanceTest(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "Material3 CheckBox Testing";

[Test]
[Category(UITestCategories.Material3)]
public void Material3CheckBox_DefaultAppearance()
{
App.WaitForElement("DefaultCheckBox");
VerifyScreenshot();
}
}
}
#endif
8 changes: 4 additions & 4 deletions src/Controls/tests/TestCases.Shared.Tests/UITest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,9 @@ but both can happen.
}

if (!((deviceApiLevel == 30 && (deviceScreenSize.Equals("1080x1920", StringComparison.OrdinalIgnoreCase) || deviceScreenSize.Equals("1920x1080", StringComparison.OrdinalIgnoreCase)) && deviceScreenDensity == 420) ||
(deviceApiLevel == 36 && (deviceScreenSize.Equals("1080x2424", StringComparison.OrdinalIgnoreCase) || deviceScreenSize.Equals("2424x1080", StringComparison.OrdinalIgnoreCase)) && deviceScreenDensity == 420)))
(deviceApiLevel == 36 && (deviceScreenSize.Equals("1440x2960", StringComparison.OrdinalIgnoreCase) || deviceScreenSize.Equals("2960x1440", StringComparison.OrdinalIgnoreCase)) && deviceScreenDensity == 560)))
{
Assert.Fail($"Android visual tests should be run on an API30 emulator image with 1080x1920 420dpi screen or API36 emulator image with 1080x2424 420dpi screen, but the current device is API {deviceApiLevel} with a {deviceScreenSize} {deviceScreenDensity}dpi screen. Follow the steps on the MAUI UI testing wiki to launch the Android emulator with the right image.");
Assert.Fail($"Android visual tests should be run on an API30 emulator image with 1080x1920 420dpi screen or API36 emulator image with 1440x2960 560dpi screen, but the current device is API {deviceApiLevel} with a {deviceScreenSize} {deviceScreenDensity}dpi screen. Follow the steps on the MAUI UI testing wiki to launch the Android emulator with the right image.");
}
break;

Expand Down Expand Up @@ -322,7 +322,7 @@ but both can happen.
// 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 => environmentName == "android-notch-36" ? 95 : 60,
TestDevice.Android => environmentName == "android-notch-36" ? 112 : 60,
TestDevice.iOS => environmentName == "ios-iphonex" ? 90 : 110,
TestDevice.Windows => 32,
TestDevice.Mac => 29,
Expand All @@ -341,7 +341,7 @@ but both can happen.
// For iOS, crop the home indicator at the bottom.
int cropFromBottom = _testDevice switch
{
TestDevice.Android => environmentName == "android-notch-36" ? 40 : 125,
TestDevice.Android => environmentName == "android-notch-36" ? 52 : 125,
TestDevice.iOS => 40,
_ => 0,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ internal static class UITestCategories
public const string GraphicsView = "GraphicsView";
public const string Fonts = "Fonts";
public const string SafeAreaEdges = "SafeAreaEdges";
public const string Material3 = "Material3";
}
}
Loading