Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.ReactNative.AccessibilityDiagnostics.Core", "src\Microsoft.ReactNative.AccessibilityDiagnostics.Core\Microsoft.ReactNative.AccessibilityDiagnostics.Core.csproj", "{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.ReactNative.AccessibilityDiagnostics.Cli", "src\Microsoft.ReactNative.AccessibilityDiagnostics.Cli\Microsoft.ReactNative.AccessibilityDiagnostics.Cli.csproj", "{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.ReactNative.AccessibilityDiagnostics.Core.Tests", "tests\Microsoft.ReactNative.AccessibilityDiagnostics.Core.Tests\Microsoft.ReactNative.AccessibilityDiagnostics.Core.Tests.csproj", "{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Debug|x64.ActiveCfg = Debug|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Debug|x64.Build.0 = Debug|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Debug|x86.ActiveCfg = Debug|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Debug|x86.Build.0 = Debug|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Release|Any CPU.Build.0 = Release|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Release|x64.ActiveCfg = Release|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Release|x64.Build.0 = Release|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Release|x86.ActiveCfg = Release|Any CPU
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD}.Release|x86.Build.0 = Release|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Debug|x64.ActiveCfg = Debug|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Debug|x64.Build.0 = Debug|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Debug|x86.ActiveCfg = Debug|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Debug|x86.Build.0 = Debug|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Release|Any CPU.Build.0 = Release|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Release|x64.ActiveCfg = Release|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Release|x64.Build.0 = Release|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Release|x86.ActiveCfg = Release|Any CPU
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8}.Release|x86.Build.0 = Release|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Debug|x64.ActiveCfg = Debug|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Debug|x64.Build.0 = Debug|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Debug|x86.ActiveCfg = Debug|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Debug|x86.Build.0 = Debug|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Release|Any CPU.Build.0 = Release|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Release|x64.ActiveCfg = Release|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Release|x64.Build.0 = Release|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Release|x86.ActiveCfg = Release|Any CPU
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C016F4D8-CDEB-40F3-A1DE-C48ADAD15DBD} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{7FECBD50-E2EC-48A5-AA86-931B20F2E4F8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{49FE0304-FDB3-4CF3-9AEE-47D811C192B2} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<Deterministic>true</Deterministic>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Project />
130 changes: 130 additions & 0 deletions packages/@rnw-scripts/accessibility-diagnostics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# RNW Accessibility Diagnostics

`rnw-a11y` captures the Windows UI Automation tree exposed by a running application and compares two captures. It
helps React Native Windows engineers detect accessibility changes that are invisible in screenshots, including lost
accessible names, control type changes, lost keyboard focusability, removed interactive controls, and tree-order
changes.

The tool is read-only. It does not invoke controls, change application state, or add code to the inspected
application.

## Prerequisites

- Windows 10 version 22H2 or newer
- .NET SDK 10
- A running desktop application to inspect

## Build and test

From the repository root:

```powershell
yarn workspace @rnw-scripts/accessibility-diagnostics build
yarn workspace @rnw-scripts/accessibility-diagnostics test
```

## Capture a snapshot

Select exactly one target using a process ID, process name, native window handle, or exact window title:

```powershell
yarn workspace @rnw-scripts/accessibility-diagnostics rnw-a11y capture `
--process RNTesterApp-Fabric `
--output before.json
```

```powershell
yarn workspace @rnw-scripts/accessibility-diagnostics rnw-a11y capture `
--hwnd 0x000A04BC `
--output after.json
```

Process-name selection supports hosted or packaged applications by finding the top-level window containing content
from the requested process. If multiple windows match, use `--hwnd` to make the target explicit.

Capture limits bound the generated snapshot and recursive processing:

```powershell
rnw-a11y capture --process MyApp --output app.json --max-depth 50 --max-nodes 5000
```

The defaults are a maximum depth of 100, a maximum of 10,000 nodes, and a 100 millisecond settling delay. The maximum
supported depth is 200. A capture
that reaches a limit or loses an element while reading is written with `captureStatus: "incomplete"` and returns exit
code 4.

## Compare snapshots

Print differences to the console:

```powershell
yarn workspace @rnw-scripts/accessibility-diagnostics rnw-a11y diff before.json after.json
```

Create a Markdown report:

```powershell
yarn workspace @rnw-scripts/accessibility-diagnostics rnw-a11y diff before.json after.json `
--format markdown `
--output accessibility-diff.md `
--fail-on potential-regression
```

Available formats are `console`, `json`, and `markdown`.

`--fail-on` controls when differences return exit code 1:

| Value | Behavior |
| --- | --- |
| `none` | Always return success after a valid comparison. |
| `potential-regression` | Fail only for changes classified as potential accessibility regressions. |
| `any` | Fail for any difference. This is the default. |

Incomplete snapshots are rejected by default because they can produce misleading results. Use `--allow-incomplete`
only when intentionally investigating a partial capture.

## Snapshot contents

Snapshots use a versioned JSON schema and capture the Control View of the UI Automation tree. Each element can
include:

- accessible name and Automation ID;
- control type, localized type, class name, and framework ID;
- enabled, keyboard-focusable, and offscreen states;
- help text and access key;
- selection, toggle, and expand/collapse states;
- ordered child elements.

Runtime IDs, bounding rectangles, keyboard focus, and text-field values are excluded from comparisons because they
are volatile or may contain sensitive information.

## Matching and classification

Elements are matched only within an already matched parent. The matcher uses unique Automation IDs first, then stable
element characteristics and sequence alignment. This prevents one inserted element from turning the rest of the
subtree into unrelated changes.

The report keeps structural changes separate from risk classification. Potential regressions currently include:

- changing an element's control type;
- removing a keyboard-focusable or interactive element;
- removing the accessible name from a focusable element;
- making a previously focusable element unfocusable;
- changing the relative order of existing sibling elements.

These findings require engineering review. A clean diff does not prove that an application is fully accessible, and
the tool does not replace keyboard or screen-reader testing.

## Privacy

Snapshots remain on the local machine unless the engineer explicitly shares or checks them in. Accessible names,
help text, and item labels can contain application or user-provided content. Review snapshots before attaching them
to bugs or pull requests.

The tool intentionally does not read `ValuePattern` or text document contents.

## Future focus-path recorder

The snapshot model, target selection, cached UI Automation reader, serialization, and reporting layers are designed
to support a future focus-path recorder. That extension can subscribe to UI Automation focus events and describe
each focused element using the same property reader without changing the target application.
45 changes: 45 additions & 0 deletions packages/@rnw-scripts/accessibility-diagnostics/bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env node

/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*
* @format
*/

'use strict';

const {spawnSync} = require('child_process');
const path = require('path');

if (process.platform !== 'win32') {
console.error('rnw-a11y is only supported on Windows.');
process.exit(2);
}

const project = path.join(
__dirname,
'src',
'Microsoft.ReactNative.AccessibilityDiagnostics.Cli',
'Microsoft.ReactNative.AccessibilityDiagnostics.Cli.csproj',
);
const result = spawnSync(
'dotnet',
[
'run',
'--project',
project,
'--configuration',
'Release',
'--',
...process.argv.slice(2),
],
{stdio: 'inherit'},
);

if (result.error) {
console.error(`Failed to start rnw-a11y: ${result.error.message}`);
process.exit(3);
}

process.exit(result.status ?? 3);
25 changes: 25 additions & 0 deletions packages/@rnw-scripts/accessibility-diagnostics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@rnw-scripts/accessibility-diagnostics",
"version": "0.0.0",
"private": true,
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/react-native-windows",
"directory": "packages/@rnw-scripts/accessibility-diagnostics"
},
"scripts": {
"build": "node scripts/run-dotnet.js build",
"clean": "node scripts/run-dotnet.js clean",
"lint": "node --check bin.js && node --check scripts/run-dotnet.js && node scripts/run-dotnet.js lint",
"lint:fix": "node scripts/run-dotnet.js lint:fix",
"rnw-a11y": "node bin.js",
"test": "node scripts/run-dotnet.js test"
},
"bin": {
"rnw-a11y": "bin.js"
},
"engines": {
"node": ">= 22"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*
* @format
*/

'use strict';

const {spawnSync} = require('child_process');
const path = require('path');

const command = process.argv[2];
const root = path.resolve(__dirname, '..');
const solution = path.join(root, 'AccessibilityDiagnostics.sln');
const tests = path.join(
root,
'tests',
'Microsoft.ReactNative.AccessibilityDiagnostics.Core.Tests',
'Microsoft.ReactNative.AccessibilityDiagnostics.Core.Tests.csproj',
);

if (process.platform !== 'win32') {
console.log(`Skipping Windows-only accessibility diagnostics ${command}.`);
process.exit(0);
}

const commands = {
build: ['build', solution, '--configuration', 'Release'],
clean: ['clean', solution, '--configuration', 'Release'],
lint: ['format', solution, '--verify-no-changes'],
'lint:fix': ['format', solution],
test: ['test', tests, '--configuration', 'Release'],
};

const args = commands[command];
if (!args) {
console.error(`Unknown command: ${command}`);
process.exit(2);
}

const result = spawnSync('dotnet', args, {cwd: root, stdio: 'inherit'});
if (result.error) {
console.error(`Failed to run dotnet: ${result.error.message}`);
process.exit(3);
}

process.exit(result.status ?? 3);
Loading