Skip to content

Commit 7e83afd

Browse files
authored
Enable AutomationClient for E2E Testing on Fabric (#12037)
* Enable App Launch and Close in Testing * Save State * Save State * Save State * Add Package Provider * Save State * Save State: Working AutomationClient * Code Cleanup * Format * Format * Fix Build * Update CI * Fix Path
1 parent 2ff641e commit 7e83afd

File tree

7 files changed

+98
-161
lines changed

7 files changed

+98
-161
lines changed

.ado/jobs/e2e-test.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,15 @@ jobs:
180180
feature: UseHermes
181181
value: ${{ matrix.UseHermes }}
182182

183-
- template: ../templates/msbuild-sln.yml
183+
- template: ../templates/run-windows-with-certificates.yml
184184
parameters:
185-
solutionDir: packages/e2e-test-app-fabric/windows
186-
solutionName: RNTesterApp-Fabric.sln
187-
buildPlatform: ${{ matrix.BuildPlatform}}
185+
buildEnvironment: ${{ parameters.BuildEnvironment }}
186+
certificateName: reactUWPTestAppEncodedKey
188187
buildConfiguration: Debug
189-
warnAsError: false
188+
buildPlatform: ${{ matrix.BuildPlatform }}
189+
buildLogDirectory: $(BuildLogDirectory)
190+
deployOption: '--no-deploy'
191+
workingDirectory: packages/e2e-test-app-fabric
190192

191193
- script: |
192194
echo ##vso[task.setvariable variable=StartedFabricTests]true
@@ -217,7 +219,7 @@ jobs:
217219
- task: CopyFiles@2
218220
displayName: Copy RNTesterApp artifacts
219221
inputs:
220-
sourceFolder: $(Build.SourcesDirectory)/packages/e2e-test-app-fabic/windows/RNTesterApp-Fabric
222+
sourceFolder: $(Build.SourcesDirectory)/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric
221223
targetFolder: $(Build.StagingDirectory)/RNTesterApp-Fabric
222224
contents: AppPackages\**
223225
condition: failed()

packages/e2e-test-app-fabric/jest.config.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module.exports = {
2626
// This environment causes the app to launch and close after testing is complete.
2727
// Temporarily disabling due to breaks in UIA implementation.
2828
// disabled temporarily
29-
// testEnvironment: '@react-native-windows/automation',
29+
testEnvironment: '@react-native-windows/automation',
3030

3131
// The pattern or patterns Jest uses to detect test files
3232
testRegex: ['.*\\.test\\.ts$', '.*\\.test\\.js$'],
@@ -60,9 +60,8 @@ module.exports = {
6060
setupFilesAfterEnv: ['react-native-windows/jest/setup', './jest.setup.js'],
6161

6262
testEnvironmentOptions: {
63-
app: `windows\\Debug\\RNTesterApp-Fabric.exe`,
63+
app: `windows\\x64\\Debug\\RNTesterApp-Fabric.exe`,
6464
enableAutomationChannel: true,
65-
winAppDriverBin: 'D:\\WindowsApplicationDriver\\WinAppDriver.exe',
6665
},
6766

6867
moduleFileExtensions: [
Lines changed: 1 addition & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,2 @@
11
const {makeMetroConfig} = require('@rnw-scripts/metro-dev-config');
2-
3-
/**
4-
* Metro configuration for React Native
5-
* https://github.com/facebook/react-native
6-
*
7-
* @format
8-
*/
9-
const fs = require('fs');
10-
const path = require('path');
11-
12-
const rnwPath = fs.realpathSync(
13-
path.dirname(require.resolve('react-native-windows/package.json')),
14-
);
15-
16-
const rnwTesterPath = fs.realpathSync(
17-
path.dirname(require.resolve('@react-native-windows/tester/package.json')),
18-
);
19-
20-
const devPackages = {
21-
'react-native': path.normalize(rnwPath),
22-
'react-native-windows': path.normalize(rnwPath),
23-
'@react-native-windows/tester': path.normalize(rnwTesterPath),
24-
};
25-
26-
function isRelativeImport(filePath) {
27-
return /^[.][.]?(?:[/]|$)/.test(filePath);
28-
}
29-
30-
// Example: devResolve('C:/Repos/react-native-windows/vnext/', './Libraries/Text/Text');
31-
// Returns a full path to the resolved location which would be in the src subdirectory if
32-
// the file exists or the directory root otherwise
33-
function devResolve(packageName, originDir, moduleName) {
34-
const originDirSrc = originDir.replace(
35-
devPackages[packageName],
36-
path.join(devPackages[packageName], 'src'),
37-
);
38-
39-
// redirect the resolution to src if an appropriate file exists there
40-
const extensions = [
41-
'',
42-
'.windows.tsx',
43-
'.windows.ts',
44-
'.windows.jsx',
45-
'.windows.js',
46-
'.tsx',
47-
'.ts',
48-
'.jsx',
49-
'.js',
50-
];
51-
52-
// For each potential extension we need to check for the file in either src and root
53-
for (const extension of extensions) {
54-
// Start with the src folder
55-
let potentialSrcModuleName = path.resolve(originDirSrc, moduleName);
56-
if (fs.existsSync(potentialSrcModuleName) &&
57-
fs.statSync(potentialSrcModuleName).isDirectory()) {
58-
potentialSrcModuleName = path.resolve(potentialSrcModuleName, 'index');
59-
}
60-
potentialSrcModuleName += extension;
61-
62-
if (fs.existsSync(potentialSrcModuleName)) {
63-
return potentialSrcModuleName;
64-
}
65-
66-
// Next check under root folder
67-
let potentialModuleName = path.resolve(originDir, moduleName);
68-
if (fs.existsSync(potentialModuleName) &&
69-
fs.statSync(potentialModuleName).isDirectory()) {
70-
potentialModuleName = path.resolve(potentialModuleName, 'index');
71-
}
72-
potentialModuleName += extension;
73-
74-
if (fs.existsSync(potentialModuleName)) {
75-
return potentialModuleName;
76-
}
77-
}
78-
}
79-
80-
/**
81-
* Allows the usage of live reload in packages in our repo which merges
82-
* Windows-specific over core. These normally work by copying from the "src"
83-
* subdirectory to package root during build time, but this resolver will
84-
* instead prefer the copy in "src" to avoid the need to build.
85-
*/
86-
function devResolveRequest(
87-
context,
88-
moduleName /* string */,
89-
platform /* string */,
90-
) {
91-
const modifiedModuleName =
92-
tryResolveDevPackage(moduleName) ||
93-
tryResolveDevAbsoluteImport(moduleName) ||
94-
tryResolveDevRelativeImport(context.originModulePath, moduleName) ||
95-
moduleName;
96-
return context.resolveRequest(context, modifiedModuleName, platform);
97-
}
98-
99-
function tryResolveDevPackage(moduleName) /*: string | null*/ {
100-
if (devPackages[moduleName]) {
101-
return devResolve(moduleName, devPackages[moduleName], './index');
102-
}
103-
104-
return null;
105-
}
106-
107-
function tryResolveDevAbsoluteImport(moduleName) /*: string | null*/ {
108-
for (const [packageName, packagePath] of Object.entries(devPackages)) {
109-
if (moduleName.startsWith(`${packageName}/`)) {
110-
return devResolve(
111-
packageName,
112-
packagePath,
113-
`./${moduleName.slice(`${packageName}/`.length)}`,
114-
);
115-
}
116-
}
117-
118-
return null;
119-
}
120-
121-
function tryResolveDevRelativeImport(
122-
originModulePath,
123-
moduleName,
124-
) /*: string | null*/ {
125-
for (const [packageName, packagePath] of Object.entries(devPackages)) {
126-
if (
127-
isRelativeImport(moduleName) &&
128-
originModulePath.startsWith(packagePath)
129-
) {
130-
const packageSrcPath = path.join(packagePath, 'src');
131-
const originPathWithoutSrc = originModulePath.replace(
132-
packageSrcPath,
133-
packagePath,
134-
);
135-
136-
return devResolve(
137-
packageName,
138-
path.dirname(originPathWithoutSrc),
139-
moduleName,
140-
);
141-
}
142-
}
143-
144-
return null;
145-
}
146-
147-
module.exports = makeMetroConfig({
148-
resolver: {
149-
resolveRequest: devResolveRequest,
150-
},
151-
});
2+
module.exports = makeMetroConfig();

packages/e2e-test-app-fabric/test/visitAllPages.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,49 @@
44
*
55
* @format
66
*/
7+
//import {goToApiExample, goToComponentExample} from './RNTesterNavigation';
8+
//import {verifyNoErrorLogs} from './Helpers';
9+
10+
type RNTesterExampleModule = {
11+
title: string;
12+
description: string;
13+
};
14+
15+
type RNTesterModuleInfo = {
16+
key: string;
17+
module: RNTesterExampleModule;
18+
};
19+
20+
type RNTesterList = {
21+
APIs: RNTesterModuleInfo[];
22+
Components: RNTesterModuleInfo[];
23+
};
24+
25+
const testerList: RNTesterList = require('@react-native-windows/tester/js/utils/RNTesterList');
26+
27+
const apiExamples = testerList.APIs.map(e => e.module.title);
28+
const componentExamples = testerList.Components.map(e => e.module.title);
29+
730
describe('visitAllPages', () => {
831
test('control', () => {
932
expect(true).toBe(true);
1033
});
34+
35+
for (const component of componentExamples) {
36+
test(component, () => {
37+
expect(true).toBe(true);
38+
});
39+
}
40+
41+
for (const api of apiExamples) {
42+
if (api === 'Transforms')
43+
// disable until either transformExample uses units, or that isn't an error
44+
continue;
45+
46+
test(api, () => {
47+
expect(true).toBe(true);
48+
});
49+
}
1150
});
1251

1352
export {};

packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,14 @@ Global
125125
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x64.Build.0 = Debug|x64
126126
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x86.ActiveCfg = Debug|Win32
127127
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x86.Build.0 = Debug|Win32
128+
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x86.Deploy.0 = Debug|Win32
128129
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|ARM64.ActiveCfg = Release|ARM64
129130
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|ARM64.Build.0 = Release|ARM64
130131
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x64.ActiveCfg = Release|x64
131132
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x64.Build.0 = Release|x64
132133
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.ActiveCfg = Release|Win32
133134
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.Build.0 = Release|Win32
135+
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.Deploy.0 = Release|Win32
134136
EndGlobalSection
135137
GlobalSection(SolutionProperties) = preSolution
136138
HideSolutionNode = FALSE

packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/RNTesterApp-Fabric.cpp

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
#include "RNTesterApp-Fabric.h"
66

77
#include "../../../../vnext/codegen/NativeDeviceInfoSpec.g.h"
8+
#include "winrt/AutomationChannel.h"
89

910
#include <DispatcherQueue.h>
1011
#include <UIAutomation.h>
1112

1213
#include <winrt/Microsoft.ReactNative.Composition.h>
14+
#include <winrt/Windows.Data.Json.h>
15+
#include <winrt/Windows.Foundation.h>
1316
#include <winrt/Windows.UI.Composition.Desktop.h>
1417

1518
#include "NativeModules.h"
@@ -52,7 +55,6 @@ struct CompReactPackageProvider
5255
public: // IReactPackageProvider
5356
void CreatePackage(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept {
5457
AddAttributedModules(packageBuilder, true);
55-
packageBuilder.AddTurboModule(L"DeviceInfo", winrt::Microsoft::ReactNative::MakeModuleProvider<DeviceInfo>());
5658
}
5759
};
5860

@@ -62,6 +64,8 @@ WCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
6264

6365
winrt::Windows::System::DispatcherQueueController g_dispatcherQueueController{nullptr};
6466
winrt::Windows::UI::Composition::Compositor g_compositor{nullptr};
67+
winrt::AutomationChannel::CommandHandler handler;
68+
winrt::AutomationChannel::Server server{nullptr};
6569

6670
constexpr auto WindowDataProperty = L"WindowData";
6771
constexpr PCWSTR c_windowClassName = L"MS_REACTNATIVE_RNTESTER_COMPOSITION";
@@ -70,6 +74,9 @@ constexpr PCWSTR appName = L"RNTesterApp";
7074
// Forward declarations of functions included in this code module:
7175
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
7276
int RunRNTester(int showCmd);
77+
winrt::Windows::Data::Json::JsonObject ListErrors(winrt::Windows::Data::Json::JsonValue payload);
78+
winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json::JsonValue payload);
79+
winrt::Windows::Foundation::IAsyncAction LoopServer(winrt::AutomationChannel::Server &server);
7380

7481
struct WindowData {
7582
static HINSTANCE s_instance;
@@ -136,6 +143,7 @@ struct WindowData {
136143
host.InstanceSettings().UseDeveloperSupport(true);
137144

138145
host.PackageProviders().Append(winrt::make<CompReactPackageProvider>());
146+
host.PackageProviders().Append(winrt::AutomationChannel::ReactPackageProvider());
139147
winrt::Microsoft::ReactNative::ReactCoreInjection::SetTopLevelWindowId(
140148
host.InstanceSettings().Properties(), reinterpret_cast<uint64_t>(hwnd));
141149

@@ -196,7 +204,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
196204

197205
auto hwndHost = windowData->m_CompositionHwndHost;
198206
winrt::com_ptr<IRawElementProviderSimple> spReps;
199-
hwndHost.UiaProvider().as(spReps);
207+
if (!hwndHost.UiaProvider().try_as(spReps)) {
208+
break;
209+
}
200210
LRESULT lResult = UiaReturnRawElementProvider(hWnd, wParam, lParam, spReps.get());
201211
return lResult;
202212
}
@@ -232,6 +242,12 @@ int RunRNTester(int showCmd) {
232242

233243
HACCEL hAccelTable = LoadAccelerators(WindowData::s_instance, MAKEINTRESOURCE(IDC_RNTESTER_COMPOSITION));
234244

245+
// Set Up Servers for E2E Testing
246+
handler.BindOperation(L"DumpVisualTree", DumpVisualTree);
247+
handler.BindOperation(L"ListErrors", ListErrors);
248+
server = winrt::AutomationChannel::Server(handler);
249+
auto asyncAction = LoopServer(server);
250+
235251
MSG msg = {};
236252
while (GetMessage(&msg, nullptr, 0, 0)) {
237253
if (!TranslateAccelerator(hwnd, hAccelTable, &msg)) {
@@ -272,5 +288,31 @@ _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE instance, HINSTANCE, PSTR
272288
winrt::put_abi(g_dispatcherQueueController))));
273289

274290
g_compositor = winrt::Windows::UI::Composition::Compositor();
291+
275292
return RunRNTester(showCmd);
276293
}
294+
295+
winrt::Windows::Data::Json::JsonObject ListErrors(winrt::Windows::Data::Json::JsonValue payload) {
296+
winrt::Windows::Data::Json::JsonObject result;
297+
winrt::Windows::Data::Json::JsonArray jsonErrors;
298+
winrt::Windows::Data::Json::JsonArray jsonWarnings;
299+
// TODO: Add Error and Warnings
300+
result.Insert(L"errors", jsonErrors);
301+
result.Insert(L"warnings", jsonWarnings);
302+
return result;
303+
}
304+
305+
winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json::JsonValue payload) {
306+
winrt::Windows::Data::Json::JsonObject result;
307+
// TODO: Method should return a JSON of the Composition Visual Tree
308+
return result;
309+
}
310+
311+
winrt::Windows::Foundation::IAsyncAction LoopServer(winrt::AutomationChannel::Server &server) {
312+
while (true) {
313+
try {
314+
co_await server.ProcessAllClientRequests(8603, std::chrono::milliseconds(50));
315+
} catch (const std::exception ex) {
316+
}
317+
}
318+
}

packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/pch.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@
3333
// reference additional headers your program requires here
3434
#include <unknwn.h>
3535
#include <winrt/base.h>
36+
37+
#include "winrt/AutomationChannel.h"

0 commit comments

Comments
 (0)