Skip to content

Commit

Permalink
Enable AutomationClient for E2E Testing on Fabric (microsoft#12037)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
chiaramooney authored Aug 21, 2023
1 parent 2ff641e commit 7e83afd
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 161 deletions.
14 changes: 8 additions & 6 deletions .ado/jobs/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,15 @@ jobs:
feature: UseHermes
value: ${{ matrix.UseHermes }}

- template: ../templates/msbuild-sln.yml
- template: ../templates/run-windows-with-certificates.yml
parameters:
solutionDir: packages/e2e-test-app-fabric/windows
solutionName: RNTesterApp-Fabric.sln
buildPlatform: ${{ matrix.BuildPlatform}}
buildEnvironment: ${{ parameters.BuildEnvironment }}
certificateName: reactUWPTestAppEncodedKey
buildConfiguration: Debug
warnAsError: false
buildPlatform: ${{ matrix.BuildPlatform }}
buildLogDirectory: $(BuildLogDirectory)
deployOption: '--no-deploy'
workingDirectory: packages/e2e-test-app-fabric

- script: |
echo ##vso[task.setvariable variable=StartedFabricTests]true
Expand Down Expand Up @@ -217,7 +219,7 @@ jobs:
- task: CopyFiles@2
displayName: Copy RNTesterApp artifacts
inputs:
sourceFolder: $(Build.SourcesDirectory)/packages/e2e-test-app-fabic/windows/RNTesterApp-Fabric
sourceFolder: $(Build.SourcesDirectory)/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric
targetFolder: $(Build.StagingDirectory)/RNTesterApp-Fabric
contents: AppPackages\**
condition: failed()
Expand Down
5 changes: 2 additions & 3 deletions packages/e2e-test-app-fabric/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {
// This environment causes the app to launch and close after testing is complete.
// Temporarily disabling due to breaks in UIA implementation.
// disabled temporarily
// testEnvironment: '@react-native-windows/automation',
testEnvironment: '@react-native-windows/automation',

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

testEnvironmentOptions: {
app: `windows\\Debug\\RNTesterApp-Fabric.exe`,
app: `windows\\x64\\Debug\\RNTesterApp-Fabric.exe`,
enableAutomationChannel: true,
winAppDriverBin: 'D:\\WindowsApplicationDriver\\WinAppDriver.exe',
},

moduleFileExtensions: [
Expand Down
151 changes: 1 addition & 150 deletions packages/e2e-test-app-fabric/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,151 +1,2 @@
const {makeMetroConfig} = require('@rnw-scripts/metro-dev-config');

/**
* Metro configuration for React Native
* https://github.com/facebook/react-native
*
* @format
*/
const fs = require('fs');
const path = require('path');

const rnwPath = fs.realpathSync(
path.dirname(require.resolve('react-native-windows/package.json')),
);

const rnwTesterPath = fs.realpathSync(
path.dirname(require.resolve('@react-native-windows/tester/package.json')),
);

const devPackages = {
'react-native': path.normalize(rnwPath),
'react-native-windows': path.normalize(rnwPath),
'@react-native-windows/tester': path.normalize(rnwTesterPath),
};

function isRelativeImport(filePath) {
return /^[.][.]?(?:[/]|$)/.test(filePath);
}

// Example: devResolve('C:/Repos/react-native-windows/vnext/', './Libraries/Text/Text');
// Returns a full path to the resolved location which would be in the src subdirectory if
// the file exists or the directory root otherwise
function devResolve(packageName, originDir, moduleName) {
const originDirSrc = originDir.replace(
devPackages[packageName],
path.join(devPackages[packageName], 'src'),
);

// redirect the resolution to src if an appropriate file exists there
const extensions = [
'',
'.windows.tsx',
'.windows.ts',
'.windows.jsx',
'.windows.js',
'.tsx',
'.ts',
'.jsx',
'.js',
];

// For each potential extension we need to check for the file in either src and root
for (const extension of extensions) {
// Start with the src folder
let potentialSrcModuleName = path.resolve(originDirSrc, moduleName);
if (fs.existsSync(potentialSrcModuleName) &&
fs.statSync(potentialSrcModuleName).isDirectory()) {
potentialSrcModuleName = path.resolve(potentialSrcModuleName, 'index');
}
potentialSrcModuleName += extension;

if (fs.existsSync(potentialSrcModuleName)) {
return potentialSrcModuleName;
}

// Next check under root folder
let potentialModuleName = path.resolve(originDir, moduleName);
if (fs.existsSync(potentialModuleName) &&
fs.statSync(potentialModuleName).isDirectory()) {
potentialModuleName = path.resolve(potentialModuleName, 'index');
}
potentialModuleName += extension;

if (fs.existsSync(potentialModuleName)) {
return potentialModuleName;
}
}
}

/**
* Allows the usage of live reload in packages in our repo which merges
* Windows-specific over core. These normally work by copying from the "src"
* subdirectory to package root during build time, but this resolver will
* instead prefer the copy in "src" to avoid the need to build.
*/
function devResolveRequest(
context,
moduleName /* string */,
platform /* string */,
) {
const modifiedModuleName =
tryResolveDevPackage(moduleName) ||
tryResolveDevAbsoluteImport(moduleName) ||
tryResolveDevRelativeImport(context.originModulePath, moduleName) ||
moduleName;
return context.resolveRequest(context, modifiedModuleName, platform);
}

function tryResolveDevPackage(moduleName) /*: string | null*/ {
if (devPackages[moduleName]) {
return devResolve(moduleName, devPackages[moduleName], './index');
}

return null;
}

function tryResolveDevAbsoluteImport(moduleName) /*: string | null*/ {
for (const [packageName, packagePath] of Object.entries(devPackages)) {
if (moduleName.startsWith(`${packageName}/`)) {
return devResolve(
packageName,
packagePath,
`./${moduleName.slice(`${packageName}/`.length)}`,
);
}
}

return null;
}

function tryResolveDevRelativeImport(
originModulePath,
moduleName,
) /*: string | null*/ {
for (const [packageName, packagePath] of Object.entries(devPackages)) {
if (
isRelativeImport(moduleName) &&
originModulePath.startsWith(packagePath)
) {
const packageSrcPath = path.join(packagePath, 'src');
const originPathWithoutSrc = originModulePath.replace(
packageSrcPath,
packagePath,
);

return devResolve(
packageName,
path.dirname(originPathWithoutSrc),
moduleName,
);
}
}

return null;
}

module.exports = makeMetroConfig({
resolver: {
resolveRequest: devResolveRequest,
},
});
module.exports = makeMetroConfig();
39 changes: 39 additions & 0 deletions packages/e2e-test-app-fabric/test/visitAllPages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,49 @@
*
* @format
*/
//import {goToApiExample, goToComponentExample} from './RNTesterNavigation';
//import {verifyNoErrorLogs} from './Helpers';

type RNTesterExampleModule = {
title: string;
description: string;
};

type RNTesterModuleInfo = {
key: string;
module: RNTesterExampleModule;
};

type RNTesterList = {
APIs: RNTesterModuleInfo[];
Components: RNTesterModuleInfo[];
};

const testerList: RNTesterList = require('@react-native-windows/tester/js/utils/RNTesterList');

const apiExamples = testerList.APIs.map(e => e.module.title);
const componentExamples = testerList.Components.map(e => e.module.title);

describe('visitAllPages', () => {
test('control', () => {
expect(true).toBe(true);
});

for (const component of componentExamples) {
test(component, () => {
expect(true).toBe(true);
});
}

for (const api of apiExamples) {
if (api === 'Transforms')
// disable until either transformExample uses units, or that isn't an error
continue;

test(api, () => {
expect(true).toBe(true);
});
}
});

export {};
2 changes: 2 additions & 0 deletions packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.sln
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,14 @@ Global
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x64.Build.0 = Debug|x64
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x86.ActiveCfg = Debug|Win32
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x86.Build.0 = Debug|Win32
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Debug|x86.Deploy.0 = Debug|Win32
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|ARM64.ActiveCfg = Release|ARM64
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|ARM64.Build.0 = Release|ARM64
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x64.ActiveCfg = Release|x64
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x64.Build.0 = Release|x64
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.ActiveCfg = Release|Win32
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.Build.0 = Release|Win32
{C0A69310-6119-46DC-A6D6-0BAB7826DC92}.Release|x86.Deploy.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
#include "RNTesterApp-Fabric.h"

#include "../../../../vnext/codegen/NativeDeviceInfoSpec.g.h"
#include "winrt/AutomationChannel.h"

#include <DispatcherQueue.h>
#include <UIAutomation.h>

#include <winrt/Microsoft.ReactNative.Composition.h>
#include <winrt/Windows.Data.Json.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.UI.Composition.Desktop.h>

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

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

winrt::Windows::System::DispatcherQueueController g_dispatcherQueueController{nullptr};
winrt::Windows::UI::Composition::Compositor g_compositor{nullptr};
winrt::AutomationChannel::CommandHandler handler;
winrt::AutomationChannel::Server server{nullptr};

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

struct WindowData {
static HINSTANCE s_instance;
Expand Down Expand Up @@ -136,6 +143,7 @@ struct WindowData {
host.InstanceSettings().UseDeveloperSupport(true);

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

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

auto hwndHost = windowData->m_CompositionHwndHost;
winrt::com_ptr<IRawElementProviderSimple> spReps;
hwndHost.UiaProvider().as(spReps);
if (!hwndHost.UiaProvider().try_as(spReps)) {
break;
}
LRESULT lResult = UiaReturnRawElementProvider(hWnd, wParam, lParam, spReps.get());
return lResult;
}
Expand Down Expand Up @@ -232,6 +242,12 @@ int RunRNTester(int showCmd) {

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

// Set Up Servers for E2E Testing
handler.BindOperation(L"DumpVisualTree", DumpVisualTree);
handler.BindOperation(L"ListErrors", ListErrors);
server = winrt::AutomationChannel::Server(handler);
auto asyncAction = LoopServer(server);

MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) {
if (!TranslateAccelerator(hwnd, hAccelTable, &msg)) {
Expand Down Expand Up @@ -272,5 +288,31 @@ _Use_decl_annotations_ int CALLBACK WinMain(HINSTANCE instance, HINSTANCE, PSTR
winrt::put_abi(g_dispatcherQueueController))));

g_compositor = winrt::Windows::UI::Composition::Compositor();

return RunRNTester(showCmd);
}

winrt::Windows::Data::Json::JsonObject ListErrors(winrt::Windows::Data::Json::JsonValue payload) {
winrt::Windows::Data::Json::JsonObject result;
winrt::Windows::Data::Json::JsonArray jsonErrors;
winrt::Windows::Data::Json::JsonArray jsonWarnings;
// TODO: Add Error and Warnings
result.Insert(L"errors", jsonErrors);
result.Insert(L"warnings", jsonWarnings);
return result;
}

winrt::Windows::Data::Json::JsonObject DumpVisualTree(winrt::Windows::Data::Json::JsonValue payload) {
winrt::Windows::Data::Json::JsonObject result;
// TODO: Method should return a JSON of the Composition Visual Tree
return result;
}

winrt::Windows::Foundation::IAsyncAction LoopServer(winrt::AutomationChannel::Server &server) {
while (true) {
try {
co_await server.ProcessAllClientRequests(8603, std::chrono::milliseconds(50));
} catch (const std::exception ex) {
}
}
}
2 changes: 2 additions & 0 deletions packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@
// reference additional headers your program requires here
#include <unknwn.h>
#include <winrt/base.h>

#include "winrt/AutomationChannel.h"

0 comments on commit 7e83afd

Please sign in to comment.