Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable AutomationClient for E2E Testing on Fabric #12037

Merged
merged 17 commits into from
Aug 21, 2023
Merged
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
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) {
}
}
}
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"