diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index becc03c5..9fcc75f8 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -45,16 +45,112 @@ steps:
script: yarn tsc | yarn lint
- task: CmdLine@2
- displayName: Build project (Debug)
+ displayName: Build project (Release)
timeoutInMinutes: 60
inputs:
- script: npx react-native run-windows --arch x64 --no-deploy --logging --buildLogDirectory $BuildLogDirectory\Debug
+ script: npx react-native run-windows --arch x64 --no-deploy --logging --release --buildLogDirectory $BuildLogDirectory\Debug
- task: CmdLine@2
displayName: Snapshot Tests
inputs:
script: yarn test
+ - task: MSBuild@1
+ displayName: Build AccessibilityUnitTest
+ inputs:
+ solution: windows\AccessibilityUnitTest\AccessibilityUnitTest.csproj
+ configuration: Debug
+ platform: AnyCPU
+ msbuildArguments: /restore
+
+ - task: CmdLine@2
+ displayName: Deploy App
+ timeoutInMinutes: 5
+ inputs:
+ script: npx react-native run-windows --arch x64 --no-build --no-launch --release
+
+ - task: PowerShell@2
+ displayName: Launch App from Start Menu
+ inputs:
+ targetType: inline
+ script: |
+ Start-Process "shell:AppsFolder\Microsoft.ReactNativeGallery_8wekyb3d8bbwe!App"
+ Start-Sleep -Seconds 10
+
+ $process = Get-Process -Name "rngallery" -ErrorAction SilentlyContinue
+ if ($process) {
+ Write-Host "App is running (PID: $($process.Id))"
+ } else {
+ Write-Error "App failed to start"
+ }
+
+ - task: PowerShell@2
+ displayName: Save Baseline Accessibility Snapshot
+ inputs:
+ targetType: inline
+ script: |
+ Copy-Item "windows\AccessibilityUnitTest\AccessibilitySnapshot.json" "$(Build.ArtifactStagingDirectory)\AccessibilitySnapshot.baseline.json"
+ Write-Host "Baseline snapshot saved"
+
+ - task: VSTest@2
+ displayName: Run Accessibility Tests
+ inputs:
+ testSelector: testAssemblies
+ testAssemblyVer2: |
+ **\bin\Debug\AccessibilityUnitTest.dll
+ searchFolder: $(System.DefaultWorkingDirectory)\windows\AccessibilityUnitTest
+ resultsFolder: $(Build.ArtifactStagingDirectory)\TestResults
+
+ - task: PowerShell@2
+ displayName: Validate Accessibility Snapshot
+ condition: succeededOrFailed()
+ inputs:
+ targetType: inline
+ script: |
+ $baselinePath = "$(Build.ArtifactStagingDirectory)\AccessibilitySnapshot.baseline.json"
+ $currentPath = "windows\AccessibilityUnitTest\AccessibilitySnapshot.json"
+
+ if (-not (Test-Path $currentPath)) {
+ Write-Host "##[error]Accessibility snapshot was not generated by the test run."
+ exit 1
+ }
+
+ # Sort results by TestName for stable comparison
+ function Normalize-Snapshot($path) {
+ $json = Get-Content $path -Raw | ConvertFrom-Json
+ $json.Results = @($json.Results | Sort-Object TestName)
+ return ($json | ConvertTo-Json -Depth 10)
+ }
+
+ $baselineNorm = Normalize-Snapshot $baselinePath
+ $currentNorm = Normalize-Snapshot $currentPath
+
+ if ($baselineNorm -eq $currentNorm) {
+ Write-Host "Accessibility snapshot matches baseline."
+ } else {
+ Write-Host "##[error]Accessibility snapshot does not match the baseline in the repo."
+ Write-Host "##[section]Baseline:"
+ Write-Host $baselineNorm
+ Write-Host "##[section]Current:"
+ Write-Host $currentNorm
+ Write-Host "##[error]Update AccessibilitySnapshot.json if the changes are intentional."
+ exit 1
+ }
+
+ - task: PublishBuildArtifacts@1
+ displayName: Upload Accessibility Scan Results
+ condition: succeededOrFailed()
+ inputs:
+ pathtoPublish: windows\AccessibilityUnitTest\ScanResults
+ artifactName: 'Accessibility Scan Results - $(Agent.JobName)-$(System.JobAttempt)'
+
+ - task: PublishBuildArtifacts@1
+ displayName: Upload Accessibility Snapshot
+ condition: succeededOrFailed()
+ inputs:
+ pathtoPublish: windows\AccessibilityUnitTest\AccessibilitySnapshot.json
+ artifactName: 'Accessibility Snapshot - $(Agent.JobName)-$(System.JobAttempt)'
+
- task: PublishBuildArtifacts@1
displayName: Upload build logs
condition: succeededOrFailed()
diff --git a/ci.yml b/ci.yml
index 9a4f621c..893c2353 100644
--- a/ci.yml
+++ b/ci.yml
@@ -100,6 +100,107 @@ steps:
inputs:
script: del /f .\windows\rngallery\rngallery_Key.pfx
+ - task: MSBuild@1
+ displayName: Build AccessibilityUnitTest
+ condition: eq(variables['PUBLISH_APP'], 'true')
+ inputs:
+ solution: windows\AccessibilityUnitTest\AccessibilityUnitTest.csproj
+ configuration: Release
+ platform: AnyCPU
+ msbuildArguments: /restore
+
+ - task: CmdLine@2
+ displayName: Deploy App
+ condition: eq(variables['PUBLISH_APP'], 'true')
+ timeoutInMinutes: 5
+ inputs:
+ script: npx react-native run-windows --arch x64 --no-build --no-launch --release
+
+ - task: PowerShell@2
+ displayName: Launch App from Start Menu
+ condition: eq(variables['PUBLISH_APP'], 'true')
+ inputs:
+ targetType: inline
+ script: |
+ Start-Process "shell:AppsFolder\Microsoft.ReactNativeGallery_8wekyb3d8bbwe!App"
+ Start-Sleep -Seconds 10
+
+ $process = Get-Process -Name "rngallery" -ErrorAction SilentlyContinue
+ if ($process) {
+ Write-Host "App is running (PID: $($process.Id))"
+ } else {
+ Write-Error "App failed to start"
+ }
+
+ - task: PowerShell@2
+ displayName: Save Baseline Accessibility Snapshot
+ condition: eq(variables['PUBLISH_APP'], 'true')
+ inputs:
+ targetType: inline
+ script: |
+ Copy-Item "windows\AccessibilityUnitTest\AccessibilitySnapshot.json" "$(Build.ArtifactStagingDirectory)\AccessibilitySnapshot.baseline.json"
+ Write-Host "Baseline snapshot saved"
+
+ - task: VSTest@2
+ displayName: Run Accessibility Tests
+ condition: eq(variables['PUBLISH_APP'], 'true')
+ inputs:
+ testSelector: testAssemblies
+ testAssemblyVer2: |
+ **\bin\Release\AccessibilityUnitTest.dll
+ searchFolder: $(System.DefaultWorkingDirectory)\windows\AccessibilityUnitTest
+ resultsFolder: $(Build.ArtifactStagingDirectory)\TestResults
+
+ - task: PowerShell@2
+ displayName: Validate Accessibility Snapshot
+ condition: and(succeededOrFailed(), eq(variables['PUBLISH_APP'], 'true'))
+ inputs:
+ targetType: inline
+ script: |
+ $baselinePath = "$(Build.ArtifactStagingDirectory)\AccessibilitySnapshot.baseline.json"
+ $currentPath = "windows\AccessibilityUnitTest\AccessibilitySnapshot.json"
+
+ if (-not (Test-Path $currentPath)) {
+ Write-Host "##[error]Accessibility snapshot was not generated by the test run."
+ exit 1
+ }
+
+ # Sort results by TestName for stable comparison
+ function Normalize-Snapshot($path) {
+ $json = Get-Content $path -Raw | ConvertFrom-Json
+ $json.Results = @($json.Results | Sort-Object TestName)
+ return ($json | ConvertTo-Json -Depth 10)
+ }
+
+ $baselineNorm = Normalize-Snapshot $baselinePath
+ $currentNorm = Normalize-Snapshot $currentPath
+
+ if ($baselineNorm -eq $currentNorm) {
+ Write-Host "Accessibility snapshot matches baseline."
+ } else {
+ Write-Host "##[error]Accessibility snapshot does not match the baseline in the repo."
+ Write-Host "##[section]Baseline:"
+ Write-Host $baselineNorm
+ Write-Host "##[section]Current:"
+ Write-Host $currentNorm
+ Write-Host "##[error]Update AccessibilitySnapshot.json if the changes are intentional."
+ exit 1
+ }
+
+ - task: PublishBuildArtifacts@1
+ displayName: Upload Accessibility Scan Results
+ condition: and(succeededOrFailed(), eq(variables['PUBLISH_APP'], 'true'))
+ inputs:
+ pathtoPublish: windows\AccessibilityUnitTest\ScanResults
+ artifactName: 'Accessibility Scan Results - $(Agent.JobName)-$(System.JobAttempt)'
+
+ - task: PublishBuildArtifacts@1
+ displayName: Upload Accessibility Snapshot
+ condition: and(succeededOrFailed(), eq(variables['PUBLISH_APP'], 'true'))
+ inputs:
+ pathtoPublish: windows\AccessibilityUnitTest\AccessibilitySnapshot.json
+ artifactName: 'Accessibility Snapshot - $(Agent.JobName)-$(System.JobAttempt)'
+
- task: PublishBuildArtifacts@1
displayName: Upload App
condition: and(succeededOrFailed(), eq(${{ matrix.reactNativeWindowsVersion }}, "current"))
diff --git a/windows/.gitignore b/windows/.gitignore
index fcee016b..74963be2 100644
--- a/windows/.gitignore
+++ b/windows/.gitignore
@@ -44,3 +44,9 @@ Ankh.NoLoad
*.binlog
*.err
*.wrn
+
+# Accessibility scan results
+AccessibilityUnitTest/ScanResults/
+
+# NuGet packages
+packages/
diff --git a/windows/AccessibilityUnitTest/AccessibilitySnapshot.json b/windows/AccessibilityUnitTest/AccessibilitySnapshot.json
new file mode 100644
index 00000000..77ea1557
--- /dev/null
+++ b/windows/AccessibilityUnitTest/AccessibilitySnapshot.json
@@ -0,0 +1,194 @@
+{
+ "TotalTests": 17,
+ "TotalErrors": 16,
+ "Results": [
+ {
+ "TestName": "TestZNavigation",
+ "ErrorCount": 2,
+ "Errors": [
+ {
+ "RuleId": 123,
+ "Description": "The Name property must not include the element's control type.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the control type.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Button, IsEnabled: True, IsOffscreen: False"
+ },
+ {
+ "RuleId": 124,
+ "Description": "The Name must not include the same text as the LocalizedControlType.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the same text as the element's LocalizedControlType property.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Button, IsEnabled: True, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToTouchableOpacityAndValidate",
+ "ErrorCount": 2,
+ "Errors": [
+ {
+ "RuleId": 11,
+ "Description": "A button must support one of these patterns: Invoke, Toggle, or ExpandCollapse.",
+ "HowToFix": "Modify the button to support exactly one of the following patterns:\r\n · Support the Invoke pattern if the button performs a command at the request of the user.\r\n · Support the Toggle pattern if the button can cycle through a series of up to three states.\r\n · Support the ExpandCollapse pattern if the button shows or hides additional content.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Decrease counter. Current value is 0, IsEnabled: True, HelpText: Decreases the counter by 1, IsOffscreen: False"
+ },
+ {
+ "RuleId": 11,
+ "Description": "A button must support one of these patterns: Invoke, Toggle, or ExpandCollapse.",
+ "HowToFix": "Modify the button to support exactly one of the following patterns:\r\n · Support the Invoke pattern if the button performs a command at the request of the user.\r\n · Support the Toggle pattern if the button can cycle through a series of up to three states.\r\n · Support the ExpandCollapse pattern if the button shows or hides additional content.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Increase counter. Current value is 0, IsEnabled: True, HelpText: Increases the counter by 1, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToSwitchAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToTouchableHighlightAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToTextInputAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToViewAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToPressableAndValidate",
+ "ErrorCount": 1,
+ "Errors": [
+ {
+ "RuleId": 11,
+ "Description": "A button must support one of these patterns: Invoke, Toggle, or ExpandCollapse.",
+ "HowToFix": "Modify the button to support exactly one of the following patterns:\r\n · Support the Invoke pattern if the button performs a command at the request of the user.\r\n · Support the Toggle pattern if the button can cycle through a series of up to three states.\r\n · Support the ExpandCollapse pattern if the button shows or hides additional content.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Disabled Pressable, IsEnabled: False, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToHomeAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToTouchableWithoutFeedbackAndValidate",
+ "ErrorCount": 2,
+ "Errors": [
+ {
+ "RuleId": 11,
+ "Description": "A button must support one of these patterns: Invoke, Toggle, or ExpandCollapse.",
+ "HowToFix": "Modify the button to support exactly one of the following patterns:\r\n · Support the Invoke pattern if the button performs a command at the request of the user.\r\n · Support the Toggle pattern if the button can cycle through a series of up to three states.\r\n · Support the ExpandCollapse pattern if the button shows or hides additional content.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Decrease counter. Current value is 0, IsEnabled: True, HelpText: Decreases the counter by 1, IsOffscreen: False"
+ },
+ {
+ "RuleId": 11,
+ "Description": "A button must support one of these patterns: Invoke, Toggle, or ExpandCollapse.",
+ "HowToFix": "Modify the button to support exactly one of the following patterns:\r\n · Support the Invoke pattern if the button performs a command at the request of the user.\r\n · Support the Toggle pattern if the button can cycle through a series of up to three states.\r\n · Support the ExpandCollapse pattern if the button shows or hides additional content.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Increase counter. Current value is 0, IsEnabled: True, HelpText: Increases the counter by 1, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToFlatListAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToButtonAndValidate",
+ "ErrorCount": 6,
+ "Errors": [
+ {
+ "RuleId": 123,
+ "Description": "The Name property must not include the element's control type.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the control type.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Simple Button, IsEnabled: True, IsOffscreen: False"
+ },
+ {
+ "RuleId": 124,
+ "Description": "The Name must not include the same text as the LocalizedControlType.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the same text as the element's LocalizedControlType property.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Simple Button, IsEnabled: True, IsOffscreen: False"
+ },
+ {
+ "RuleId": 123,
+ "Description": "The Name property must not include the element's control type.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the control type.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: colored button, IsEnabled: True, IsOffscreen: False"
+ },
+ {
+ "RuleId": 124,
+ "Description": "The Name must not include the same text as the LocalizedControlType.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the same text as the element's LocalizedControlType property.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: colored button, IsEnabled: True, IsOffscreen: False"
+ },
+ {
+ "RuleId": 123,
+ "Description": "The Name property must not include the element's control type.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the control type.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Disabled Button, IsEnabled: False, IsOffscreen: False"
+ },
+ {
+ "RuleId": 124,
+ "Description": "The Name must not include the same text as the LocalizedControlType.",
+ "HowToFix": "Provide a UI Automation Name property for the element that:\r\n · Concisely identifies the element, AND\r\n · Does not include the same text as the element's LocalizedControlType property.",
+ "Element": "ControlType: Button(50000), LocalizedControlType: button, Name: Disabled Button, IsEnabled: False, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToSettingsAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToTextAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToModalAndValidate",
+ "ErrorCount": 2,
+ "Errors": [
+ {
+ "RuleId": 2,
+ "Description": "An on-screen element must not have a null BoundingRectangle property.",
+ "HowToFix": "If the element is off-screen, set its IsOffscreen property to true.\r\nIf the element is on-screen, provide a BoundingRectangle property.",
+ "Element": "ControlType: Text(50020), LocalizedControlType: text, Name: , IsEnabled: True, IsOffscreen: False"
+ },
+ {
+ "RuleId": 133,
+ "Description": "The Name property must not contain only whitespace.",
+ "HowToFix": "Provide a UI Automation Name property that concisely identifies the element.",
+ "Element": "ControlType: Text(50020), LocalizedControlType: text, Name: , IsEnabled: True, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToClipboardAndValidate",
+ "ErrorCount": 1,
+ "Errors": [
+ {
+ "RuleId": 2,
+ "Description": "An on-screen element must not have a null BoundingRectangle property.",
+ "HowToFix": "If the element is off-screen, set its IsOffscreen property to true.\r\nIf the element is on-screen, provide a BoundingRectangle property.",
+ "Element": "ControlType: Text(50020), LocalizedControlType: text, IsEnabled: True, IsOffscreen: False"
+ }
+ ]
+ },
+ {
+ "TestName": "TestNavigateToScrollViewAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ },
+ {
+ "TestName": "TestNavigateToImageAndValidate",
+ "ErrorCount": 0,
+ "Errors": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/windows/AccessibilityUnitTest/AccessibilityTests.cs b/windows/AccessibilityUnitTest/AccessibilityTests.cs
new file mode 100644
index 00000000..040d07d8
--- /dev/null
+++ b/windows/AccessibilityUnitTest/AccessibilityTests.cs
@@ -0,0 +1,189 @@
+using AccessibilityUnitTest;
+using Axe.Windows.Automation;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Windows.Automation;
+
+namespace AccessibilityUnitTest
+{
+ [TestClass]
+ public class AccessibilityTests : GallerySession
+ {
+ private static AutomationElement GalleryWindow;
+ private static int processId;
+
+ public TestContext TestContext { get; set; }
+
+ ///
+ /// Creates a scanner whose output file is named after the current test.
+ ///
+ private IScanner GetTestScanner()
+ {
+ return CreateScannerForTest(processId, TestContext.TestName);
+ }
+
+ [TestMethod]
+ public void TestNavigateToHomeAndValidate()
+ {
+ FindAndInvokeElement(GalleryWindow, "Navigation menu", waitMs: 2000);
+ FindAndInvokeElement(GalleryWindow, "Home");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToSettingsAndValidate()
+ {
+ FindAndInvokeElement(GalleryWindow, "Navigation menu", waitMs: 2000);
+ FindAndInvokeElement(GalleryWindow, "Settings");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToButtonAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Button1 control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToPressableAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Pressable control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToSwitchAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Switch control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToFlatListAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "FlatList control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ //[TestMethod]
+ //public void TestNavigateToVirtualizedListAndValidate()
+ //{
+ // NavigateToComponent(GalleryWindow, "VirtualizedList control");
+ // AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ //}
+
+ [TestMethod]
+ public void TestNavigateToModalAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Modal control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToViewAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "View control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToImageAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Image control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToScrollViewAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "ScrollView control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToTextAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Text control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToTextInputAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "TextInput control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToClipboardAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "Clipboard control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToTouchableHighlightAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "TouchableHighlight control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToTouchableOpacityAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "TouchableOpacity control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestNavigateToTouchableWithoutFeedbackAndValidate()
+ {
+ NavigateToComponent(GalleryWindow, "TouchableWithoutFeedback control");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner()));
+ }
+
+ [TestMethod]
+ public void TestZNavigation()
+ {
+ // Navigate home first to reset UI state when running after other tests
+ FindAndInvokeElement(GalleryWindow, "Navigation menu", waitMs: 2000);
+ FindAndInvokeElement(GalleryWindow, "Home", waitMs: 1000);
+ FindAndInvokeElement(GalleryWindow, "Navigation menu", waitMs: 2000);
+ var navMenu = FindElement(GalleryWindow, "Navigation menu");
+ var parent = GetParentElement(navMenu);
+ FindAndExpandElement(GalleryWindow, "Basic Input");
+ FindAndExpandElement(GalleryWindow, "Collections");
+ FindAndExpandElement(GalleryWindow, "Dialogs & flyouts");
+ FindAndExpandElement(GalleryWindow, "Layout");
+ FindAndExpandElement(GalleryWindow, "Media");
+ FindAndExpandElement(GalleryWindow, "Scrolling");
+ FindAndExpandElement(GalleryWindow, "Text");
+ FindAndExpandElement(GalleryWindow, "System");
+ FindAndExpandElement(GalleryWindow, "Legacy");
+ AddTestSnapshot(TestContext.TestName, GetScanResults(GetTestScanner(), parent));
+ }
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext context)
+ {
+ try
+ {
+ processId = GetProcessId();
+ GalleryWindow = InitializeGalleryWindow();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error during initialization: {ex.Message}");
+ throw;
+ }
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ string snapshotPath = WriteSnapshotFile();
+ Console.WriteLine($"Snapshot saved to: {snapshotPath}");
+ }
+ }
+}
diff --git a/windows/AccessibilityUnitTest/AccessibilityUnitTest.csproj b/windows/AccessibilityUnitTest/AccessibilityUnitTest.csproj
new file mode 100644
index 00000000..6418426a
--- /dev/null
+++ b/windows/AccessibilityUnitTest/AccessibilityUnitTest.csproj
@@ -0,0 +1,124 @@
+
+
+
+
+
+ Debug
+ AnyCPU
+ {4494014E-461B-45A6-BAC5-91FCD1936FDC}
+ Library
+ Properties
+ AccessibilityUnitTest
+ AccessibilityUnitTest
+ v4.8.1
+ 512
+ {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 15.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+ $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
+ False
+ UnitTest
+
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Actions.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Automation.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Core.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Desktop.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Rules.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.RuleSelection.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.SystemAbstractions.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Telemetry.dll
+
+
+ ..\packages\Axe.Windows.2.4.2\lib\netstandard20\Axe.Windows.Win32.dll
+
+
+ ..\packages\Interop.UIAutomationClient.10.19041.0\lib\net45\Interop.UIAutomationClient.dll
+ False
+
+
+ ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
+
+
+ ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
+
+
+ ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll
+
+
+ ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+ ..\packages\System.Drawing.Common.8.0.10\lib\net462\System.Drawing.Common.dll
+
+
+ ..\packages\System.IO.Packaging.8.0.1\lib\net462\System.IO.Packaging.dll
+
+
+ ..\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll
+
+
+ ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
+
diff --git a/windows/AccessibilityUnitTest/GallerySession.cs b/windows/AccessibilityUnitTest/GallerySession.cs
new file mode 100644
index 00000000..25471f62
--- /dev/null
+++ b/windows/AccessibilityUnitTest/GallerySession.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Windows.Automation;
+using Axe.Windows.Automation;
+using Axe.Windows.Automation.Data;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+
+namespace AccessibilityUnitTest
+{
+ public class GallerySession
+ {
+ private const string ApplicationProcessName = "rngallery";
+ private const string WindowName = "React Native Gallery";
+
+ // Snapshot: accumulates per-test scan results
+ private static readonly ConcurrentDictionary _snapshotResults =
+ new ConcurrentDictionary();
+ private static int _totalSnapshotErrors = 0;
+
+ public static int GetProcessId()
+ {
+ var processes = System.Diagnostics.Process.GetProcessesByName(ApplicationProcessName);
+ return processes[0].Id;
+ }
+
+ public static IScanner CreateScanner(int processId, string outputDirectory = null)
+ {
+ Console.WriteLine("Creating scanner...");
+ var configBuilder = Config.Builder.ForProcessId(processId)
+ .WithOutputFileFormat(OutputFileFormat.A11yTest);
+
+ if (!string.IsNullOrEmpty(outputDirectory))
+ {
+ Directory.CreateDirectory(outputDirectory);
+ configBuilder.WithOutputDirectory(outputDirectory);
+ }
+
+ var config = configBuilder.Build();
+ var scanner = ScannerFactory.CreateScanner(config);
+ return scanner;
+ }
+
+ ///
+ /// Creates a scanner whose output file goes into a folder named after the test.
+ ///
+ public static IScanner CreateScannerForTest(int processId, string testName)
+ {
+ string baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "ScanResults");
+ string testOutputDir = Path.GetFullPath(Path.Combine(baseDir, testName));
+ Console.WriteLine($"Scan output directory: {testOutputDir}");
+ return CreateScanner(processId, testOutputDir);
+ }
+
+ public static ScanOutput GetScanResults(IScanner scanner)
+ {
+ Console.WriteLine("Scanning...");
+ var scanResults = scanner.Scan(null);
+ return scanResults;
+ }
+
+ ///
+ /// Scans a specific element's subtree using its AutomationId.
+ /// Falls back to a full scan if the element has no AutomationId.
+ ///
+ public static ScanOutput GetScanResults(IScanner scanner, AutomationElement element)
+ {
+ Console.WriteLine($"Scanning element: {element.Current.Name} (Control Type: {element.Current.ControlType.LocalizedControlType})...");
+
+ string automationId = element.Current.AutomationId;
+ if (string.IsNullOrEmpty(automationId))
+ {
+ Console.WriteLine("Element has no AutomationId — performing full scan instead.");
+ return scanner.Scan(null);
+ }
+
+ var scanOptions = new ScanOptions(automationId);
+ var scanResults = scanner.Scan(scanOptions);
+ return scanResults;
+ }
+
+ ///
+ /// Adds the scan results for a test to the in-memory snapshot.
+ ///
+ public static void AddTestSnapshot(string testName, ScanOutput scanResults)
+ {
+ var errors = new List