@react-native-windows/automation 0.0.0-canary.1016
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/lib-commonjs/AutomationClient.d.ts +70 -0
- package/lib-commonjs/AutomationClient.js +66 -0
- package/lib-commonjs/AutomationClient.js.map +1 -0
- package/lib-commonjs/AutomationEnvironment.d.ts +60 -0
- package/lib-commonjs/AutomationEnvironment.js +234 -0
- package/lib-commonjs/AutomationEnvironment.js.map +1 -0
- package/lib-commonjs/index.d.ts +11 -0
- package/lib-commonjs/index.js +17 -0
- package/lib-commonjs/index.js.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @react-native-windows/automation
|
|
2
|
+
|
|
3
|
+
`@react-native-windows/automation` makes it easy to perform UI tests against
|
|
4
|
+
your `react-native-windows` application using Jest.
|
|
5
|
+
|
|
6
|
+
**This package is a work in progress**
|
|
7
|
+
|
|
8
|
+
## System Requirements
|
|
9
|
+
|
|
10
|
+
`@react-native-windows/automation` relies on [WinAppDriver 1.2.1](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1)
|
|
11
|
+
or later to manipulate and inspect your application via [Windows UI Automation](https://docs.microsoft.com/en-us/dotnet/framework/ui-automation/ui-automation-overview).
|
|
12
|
+
This is already installed in common CI environments like GitHub Actions or
|
|
13
|
+
Azure Pipelines hosted images.
|
|
14
|
+
|
|
15
|
+
## Configuring Jest
|
|
16
|
+
|
|
17
|
+
First, ensure that `@react-native-windows/automation` is included as a
|
|
18
|
+
dependency in your package:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@react-native-windows/automation": "<version>",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Next, edit your [Jest configuration](https://jestjs.io/docs/configuration) to
|
|
29
|
+
use `@react-native-windows/automation` as your test environment. Add the
|
|
30
|
+
following to your `jest.config.js` or `jest.config.ts` file:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
module.exports = {
|
|
34
|
+
...
|
|
35
|
+
// Set up the automation environment before running tests
|
|
36
|
+
testEnvironment: '@react-native-windows/automation',
|
|
37
|
+
|
|
38
|
+
// Only run a single test at a time
|
|
39
|
+
maxWorkers: 1,
|
|
40
|
+
|
|
41
|
+
// Set up @react-native-windows/automation specific options (see below)
|
|
42
|
+
testEnvironmentOptions: {
|
|
43
|
+
app: '<AppName>',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Environment Options
|
|
49
|
+
|
|
50
|
+
`@react-native-windows/automation` is configured via the
|
|
51
|
+
`testEnvironmentOptions` property in your Jest configuration. The following
|
|
52
|
+
options are valid:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
testEnvironmentOptions: {
|
|
56
|
+
// Required: Your application to launch, as either an AppX package identity,
|
|
57
|
+
// or path to an .exe for an unpackaged application.
|
|
58
|
+
app: 'Microsoft.WindowsAlarms',
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Optional: Arguments to be passed to your application when launched
|
|
62
|
+
*/
|
|
63
|
+
appArguments: '--bundle testBundle.js';
|
|
64
|
+
|
|
65
|
+
// Optional: Explicit path to WinAppDriver. By default,
|
|
66
|
+
// `@react-native-windows/automation` tries to use
|
|
67
|
+
// "%PROGRAMFILES(X86)%\Windows Application Driver\WinAppDriver.exe"
|
|
68
|
+
winAppDriverBin: 'D:\\WinAppDriver.exe',
|
|
69
|
+
|
|
70
|
+
// Optional: Whether to break on app launch, before starting tests
|
|
71
|
+
breakOnStart: false,
|
|
72
|
+
|
|
73
|
+
// Optional: Options to be passed to WebDriverIO
|
|
74
|
+
// See https://webdriver.io/docs/options/
|
|
75
|
+
webdriverOptions: {
|
|
76
|
+
// Port to use for WebDriver server (Default 4723)
|
|
77
|
+
port: 4444
|
|
78
|
+
|
|
79
|
+
// Level of logging verbosity: trace | debug | info | warn | error
|
|
80
|
+
logLevel: 'error',
|
|
81
|
+
|
|
82
|
+
// Default timeout for all waitFor* commands.
|
|
83
|
+
waitforTimeout: 60000,
|
|
84
|
+
|
|
85
|
+
// Default timeout in milliseconds for request
|
|
86
|
+
connectionRetryTimeout: 90000,
|
|
87
|
+
|
|
88
|
+
// Default request retries count
|
|
89
|
+
connectionRetryCount: 10,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Writing Tests
|
|
95
|
+
|
|
96
|
+
`@react-native-windows/automation` exports an `app` member with functions to
|
|
97
|
+
perform globally scoped WebDriver commands, such as locating an element or
|
|
98
|
+
waiting for a condition. Several strategies are present to locate an element,
|
|
99
|
+
with the reccomended being to use React Native's `testID` property.
|
|
100
|
+
|
|
101
|
+
A located `AutomationElement` exposes methods to allow application interaction
|
|
102
|
+
and introspection, such as clicking or typing.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import {app} from '@react-native-windows/automation';
|
|
106
|
+
|
|
107
|
+
beforeAll(async () => {
|
|
108
|
+
const appHeader = await app.findElementByTestID('app-header');
|
|
109
|
+
|
|
110
|
+
await app.waitUntil(async () => {
|
|
111
|
+
const headerText = await appHeader.getText();
|
|
112
|
+
return headerText.includes('Welcome');
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('Type abc', async () => {
|
|
117
|
+
const textInput = await app.findElementByTestID('textinput-field');
|
|
118
|
+
await textInput.setValue('abc');
|
|
119
|
+
expect(await textInput.getText()).toBe('abc');
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Performing Additional Actions
|
|
124
|
+
|
|
125
|
+
**WIP Not yet exposed**
|
|
126
|
+
|
|
127
|
+
Sometimes it is useful to exmaine or manipulate the application in ways that
|
|
128
|
+
are not exposed to Windows UI Automation. An additional package,
|
|
129
|
+
`@react-native-windows/automation-channel` can be included in your application
|
|
130
|
+
as a native module to allow writing custom commands performed by your
|
|
131
|
+
application in-process. Some pre-built commands can be included using the
|
|
132
|
+
`@react-native-windows/automation-commands` package.
|
|
133
|
+
|
|
134
|
+
## Visual Comparison
|
|
135
|
+
|
|
136
|
+
**WIP Not yet exposed**
|
|
137
|
+
|
|
138
|
+
A common use-case of UI testing is to ensure your components look the way you
|
|
139
|
+
expect. Tools like [enzyme](https://github.com/enzymejs/enzyme) allow
|
|
140
|
+
inspecting your React component tree, but are oblivious to the native UI tree.
|
|
141
|
+
|
|
142
|
+
`@react-native-windows/automation-commands` exposes a `dumpVisualTree` command
|
|
143
|
+
which creates a JSON object corresponding to the XAML tree under a given
|
|
144
|
+
testID. This can be used in conjunction with snapshot testing to validate
|
|
145
|
+
native component logic stays consistent.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import {dumpVisualTree} from '@react-native-windows/automation-commands';
|
|
149
|
+
|
|
150
|
+
test('Widget', async () => {
|
|
151
|
+
const dump = await dumpVisualTree('widget-test-id');
|
|
152
|
+
expect(dump).toMatchSnapshot();
|
|
153
|
+
});
|
|
154
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*
|
|
5
|
+
* @format
|
|
6
|
+
*/
|
|
7
|
+
/// <reference types="webdriverio/webdriverio-core" />
|
|
8
|
+
/**
|
|
9
|
+
* Projection of a WebDriver Element, with functions corresponding to supported
|
|
10
|
+
* WinAppDriver APIs.
|
|
11
|
+
*
|
|
12
|
+
* See https://github.com/microsoft/WinAppDriver/blob/master/Docs/SupportedAPIs.md
|
|
13
|
+
*/
|
|
14
|
+
export type AutomationElement = Pick<WebdriverIO.Element, 'addValue' | 'clearValue' | 'click' | 'doubleClick' | 'findElements' | 'getAttribute' | 'getLocation' | 'getSize' | 'getText' | 'getValue' | 'isDisplayed' | 'isDisplayedInViewport' | 'isEnabled' | 'isEqual' | 'isSelected' | 'moveTo' | 'saveScreenshot' | 'sendKeys' | 'selectByVisibleText' | 'setValue' | 'touchAction' | 'waitForDisplayed' | 'waitForExist'>;
|
|
15
|
+
/**
|
|
16
|
+
* A subset of WebDriver functionality that will work with Windows applications
|
|
17
|
+
*/
|
|
18
|
+
export declare const app: {
|
|
19
|
+
/**
|
|
20
|
+
* Find an element by testID property
|
|
21
|
+
*/
|
|
22
|
+
findElementByTestID: (id: string) => Promise<AutomationElement>;
|
|
23
|
+
/**
|
|
24
|
+
* Find an element by Automation ID
|
|
25
|
+
*
|
|
26
|
+
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.automationid?view=net-6.0
|
|
27
|
+
*/
|
|
28
|
+
findElementByAutomationID: (id: string) => Promise<AutomationElement>;
|
|
29
|
+
/**
|
|
30
|
+
* Finds an element by the name of its class name (e.g. ListViewItem)
|
|
31
|
+
*
|
|
32
|
+
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.classname?view=net-6.0
|
|
33
|
+
*/
|
|
34
|
+
findElementByClassName: (className: string) => Promise<AutomationElement>;
|
|
35
|
+
/**
|
|
36
|
+
* Find element by ControlType (e.g. Button, CheckBox)
|
|
37
|
+
*
|
|
38
|
+
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.controltype?view=net-6.0
|
|
39
|
+
*/
|
|
40
|
+
findElementByControlType: (controlType: string) => Promise<AutomationElement>;
|
|
41
|
+
/**
|
|
42
|
+
* Find element by a WinAppDriver compatible XPath Selector (e.g. '//Button[@AutomationId=\"MoreButton\"]')
|
|
43
|
+
*/
|
|
44
|
+
findElementByXPath: (xpath: string) => Promise<AutomationElement>;
|
|
45
|
+
/**
|
|
46
|
+
* Resizes app window outer size according to provided width and height.
|
|
47
|
+
*/
|
|
48
|
+
setWindowSize: (width: number, height: number) => Promise<object | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Change the position of the current focussed window.
|
|
51
|
+
*/
|
|
52
|
+
setWindowPosition: (x: number, y: number) => Promise<WebDriver.ProtocolCommandResponse>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the position of the current focussed window.
|
|
55
|
+
*/
|
|
56
|
+
getWindowPosition: () => Promise<WebDriver.ProtocolCommandResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Returns app window size.
|
|
59
|
+
*/
|
|
60
|
+
getWindowSize: () => Promise<WebDriver.RectReturn>;
|
|
61
|
+
/**
|
|
62
|
+
* Switch focus to a particular window.
|
|
63
|
+
*/
|
|
64
|
+
switchWindow: (titleToMatch: string | RegExp) => Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* This wait command is your universal weapon if you want to wait on something. It expects a condition
|
|
67
|
+
* and waits until that condition is fulfilled with a truthy value.
|
|
68
|
+
*/
|
|
69
|
+
waitUntil: (condition: () => Promise<boolean>, options?: WebdriverIO.WaitUntilOptions) => Promise<boolean>;
|
|
70
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*
|
|
6
|
+
* @format
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.app = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* A subset of WebDriver functionality that will work with Windows applications
|
|
12
|
+
*/
|
|
13
|
+
exports.app = {
|
|
14
|
+
/**
|
|
15
|
+
* Find an element by testID property
|
|
16
|
+
*/
|
|
17
|
+
findElementByTestID: (id) => $(`~${id}`),
|
|
18
|
+
/**
|
|
19
|
+
* Find an element by Automation ID
|
|
20
|
+
*
|
|
21
|
+
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.automationid?view=net-6.0
|
|
22
|
+
*/
|
|
23
|
+
findElementByAutomationID: (id) => $(`~${id}`),
|
|
24
|
+
/**
|
|
25
|
+
* Finds an element by the name of its class name (e.g. ListViewItem)
|
|
26
|
+
*
|
|
27
|
+
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.classname?view=net-6.0
|
|
28
|
+
*/
|
|
29
|
+
findElementByClassName: (className) => $(className),
|
|
30
|
+
/**
|
|
31
|
+
* Find element by ControlType (e.g. Button, CheckBox)
|
|
32
|
+
*
|
|
33
|
+
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.controltype?view=net-6.0
|
|
34
|
+
*/
|
|
35
|
+
findElementByControlType: (controlType) => $(`<${controlType} />`),
|
|
36
|
+
/**
|
|
37
|
+
* Find element by a WinAppDriver compatible XPath Selector (e.g. '//Button[@AutomationId=\"MoreButton\"]')
|
|
38
|
+
*/
|
|
39
|
+
findElementByXPath: (xpath) => $(xpath),
|
|
40
|
+
/**
|
|
41
|
+
* Resizes app window outer size according to provided width and height.
|
|
42
|
+
*/
|
|
43
|
+
setWindowSize: (width, height) => browser.setWindowSize(width, height),
|
|
44
|
+
/**
|
|
45
|
+
* Change the position of the current focussed window.
|
|
46
|
+
*/
|
|
47
|
+
setWindowPosition: (x, y) => browser.setWindowPosition(x, y),
|
|
48
|
+
/**
|
|
49
|
+
* Get the position of the current focussed window.
|
|
50
|
+
*/
|
|
51
|
+
getWindowPosition: () => browser.getWindowPosition(),
|
|
52
|
+
/**
|
|
53
|
+
* Returns app window size.
|
|
54
|
+
*/
|
|
55
|
+
getWindowSize: () => browser.getWindowSize(),
|
|
56
|
+
/**
|
|
57
|
+
* Switch focus to a particular window.
|
|
58
|
+
*/
|
|
59
|
+
switchWindow: (titleToMatch) => browser.switchWindow(titleToMatch),
|
|
60
|
+
/**
|
|
61
|
+
* This wait command is your universal weapon if you want to wait on something. It expects a condition
|
|
62
|
+
* and waits until that condition is fulfilled with a truthy value.
|
|
63
|
+
*/
|
|
64
|
+
waitUntil: (condition, options) => browser.waitUntil(condition, options),
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=AutomationClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AutomationClient.js","sourceRoot":"","sources":["../src/AutomationClient.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAqCH;;GAEG;AACU,QAAA,GAAG,GAAG;IACjB;;OAEG;IACH,mBAAmB,EAAE,CAAC,EAAU,EAA8B,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;IAE5E;;;;OAIG;IACH,yBAAyB,EAAE,CAAC,EAAU,EAA8B,EAAE,CACpE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;IAEb;;;;OAIG;IACH,sBAAsB,EAAE,CAAC,SAAiB,EAA8B,EAAE,CACxE,CAAC,CAAC,SAAS,CAAC;IAEd;;;;OAIG;IACH,wBAAwB,EAAE,CAAC,WAAmB,EAA8B,EAAE,CAC5E,CAAC,CAAC,IAAI,WAAW,KAAK,CAAC;IAEzB;;OAEG;IACH,kBAAkB,EAAE,CAAC,KAAa,EAA8B,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IAE3E;;OAEG;IACH,aAAa,EAAE,CAAC,KAAa,EAAE,MAAc,EAAE,EAAE,CAC/C,OAAO,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC;IAEtC;;OAEG;IACH,iBAAiB,EAAE,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC;IAE5E;;OAEG;IACH,iBAAiB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,iBAAiB,EAAE;IAEpD;;OAEG;IACH,aAAa,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE;IAE5C;;OAEG;IACH,YAAY,EAAE,CAAC,YAA6B,EAAE,EAAE,CAC9C,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC;IAEpC;;;OAGG;IACH,SAAS,EAAE,CACT,SAAiC,EACjC,OAAsC,EACtC,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC;CAC3C,CAAC","sourcesContent":["/**\n * Copyright (c) Microsoft Corporation.\n * Licensed under the MIT License.\n *\n * @format\n */\n\n/* global $:false, browser:false */\n\n/**\n * Projection of a WebDriver Element, with functions corresponding to supported\n * WinAppDriver APIs.\n *\n * See https://github.com/microsoft/WinAppDriver/blob/master/Docs/SupportedAPIs.md\n */\nexport type AutomationElement = Pick<\n WebdriverIO.Element,\n | 'addValue'\n | 'clearValue'\n | 'click'\n | 'doubleClick'\n | 'findElements'\n | 'getAttribute'\n | 'getLocation'\n | 'getSize'\n | 'getText'\n | 'getValue'\n | 'isDisplayed'\n | 'isDisplayedInViewport'\n | 'isEnabled'\n | 'isEqual'\n | 'isSelected'\n | 'moveTo'\n | 'saveScreenshot'\n | 'sendKeys'\n | 'selectByVisibleText'\n | 'setValue'\n | 'touchAction'\n | 'waitForDisplayed'\n | 'waitForExist'\n>;\n\n/**\n * A subset of WebDriver functionality that will work with Windows applications\n */\nexport const app = {\n /**\n * Find an element by testID property\n */\n findElementByTestID: (id: string): Promise<AutomationElement> => $(`~${id}`),\n\n /**\n * Find an element by Automation ID\n *\n * https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.automationid?view=net-6.0\n */\n findElementByAutomationID: (id: string): Promise<AutomationElement> =>\n $(`~${id}`),\n\n /**\n * Finds an element by the name of its class name (e.g. ListViewItem)\n *\n * https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.classname?view=net-6.0\n */\n findElementByClassName: (className: string): Promise<AutomationElement> =>\n $(className),\n\n /**\n * Find element by ControlType (e.g. Button, CheckBox)\n *\n * https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.controltype?view=net-6.0\n */\n findElementByControlType: (controlType: string): Promise<AutomationElement> =>\n $(`<${controlType} />`),\n\n /**\n * Find element by a WinAppDriver compatible XPath Selector (e.g. '//Button[@AutomationId=\\\"MoreButton\\\"]')\n */\n findElementByXPath: (xpath: string): Promise<AutomationElement> => $(xpath),\n\n /**\n * Resizes app window outer size according to provided width and height.\n */\n setWindowSize: (width: number, height: number) =>\n browser.setWindowSize(width, height),\n\n /**\n * Change the position of the current focussed window.\n */\n setWindowPosition: (x: number, y: number) => browser.setWindowPosition(x, y),\n\n /**\n * Get the position of the current focussed window.\n */\n getWindowPosition: () => browser.getWindowPosition(),\n\n /**\n * Returns app window size.\n */\n getWindowSize: () => browser.getWindowSize(),\n\n /**\n * Switch focus to a particular window.\n */\n switchWindow: (titleToMatch: string | RegExp) =>\n browser.switchWindow(titleToMatch),\n\n /**\n * This wait command is your universal weapon if you want to wait on something. It expects a condition\n * and waits until that condition is fulfilled with a truthy value.\n */\n waitUntil: (\n condition: () => Promise<boolean>,\n options?: WebdriverIO.WaitUntilOptions,\n ) => browser.waitUntil(condition, options),\n};\n"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*
|
|
5
|
+
* @format
|
|
6
|
+
*/
|
|
7
|
+
/// <reference types="webdriverio/webdriverio-core" />
|
|
8
|
+
import NodeEnvironment from 'jest-environment-node';
|
|
9
|
+
import { RemoteOptions } from 'webdriverio';
|
|
10
|
+
import { JestEnvironmentConfig } from '@jest/environment';
|
|
11
|
+
import type { EnvironmentContext } from '@jest/environment';
|
|
12
|
+
export type EnvironmentOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* The application to launch. Can be a path to an exe, or a package identity
|
|
15
|
+
* name (e.g. Microsoft.WindowsAlarms)
|
|
16
|
+
*/
|
|
17
|
+
app?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Instead of letting WinAppDriver launch and attach to the app directly,
|
|
20
|
+
* create a Root (Desktop) session and search for the app's window.
|
|
21
|
+
*
|
|
22
|
+
* Note: This is only really necessary to correctly attach to packaged
|
|
23
|
+
* WinAppSDK apps.
|
|
24
|
+
*/
|
|
25
|
+
useRootSession?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* When using a Root (Desktop) session, still launch the test app during setup
|
|
28
|
+
* and close it during cleanup.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to true when using `useRootSession` to mimic the expected test
|
|
31
|
+
* behavior, but can be disabled if you're trying to test an already
|
|
32
|
+
* running app instance.
|
|
33
|
+
*/
|
|
34
|
+
rootLaunchApp?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Arguments to be passed to your application when launched
|
|
37
|
+
*/
|
|
38
|
+
appArguments?: string;
|
|
39
|
+
appWorkingDir?: string;
|
|
40
|
+
enableAutomationChannel?: boolean;
|
|
41
|
+
automationChannelPort?: number;
|
|
42
|
+
winAppDriverBin?: string;
|
|
43
|
+
breakOnStart?: boolean;
|
|
44
|
+
webdriverOptions?: RemoteOptions;
|
|
45
|
+
};
|
|
46
|
+
export default class AutomationEnvironment extends NodeEnvironment {
|
|
47
|
+
private readonly rootWebDriverOptions?;
|
|
48
|
+
private readonly webDriverOptions;
|
|
49
|
+
private readonly channelOptions;
|
|
50
|
+
private readonly winappdriverBin;
|
|
51
|
+
private readonly breakOnStart;
|
|
52
|
+
private readonly useRootSession;
|
|
53
|
+
private readonly rootLaunchApp;
|
|
54
|
+
private winAppDriverProcess;
|
|
55
|
+
private browser;
|
|
56
|
+
private automationClient;
|
|
57
|
+
constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
|
|
58
|
+
setup(): Promise<void>;
|
|
59
|
+
teardown(): Promise<void>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*
|
|
6
|
+
* @format
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
32
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
36
|
+
const child_process_1 = require("child_process");
|
|
37
|
+
const fs_1 = __importDefault(require("@react-native-windows/fs"));
|
|
38
|
+
const path_1 = __importDefault(require("path"));
|
|
39
|
+
const readline_sync_1 = __importDefault(require("readline-sync"));
|
|
40
|
+
const jest_environment_node_1 = __importDefault(require("jest-environment-node"));
|
|
41
|
+
const webdriverio = __importStar(require("webdriverio"));
|
|
42
|
+
const automation_channel_1 = require("@react-native-windows/automation-channel");
|
|
43
|
+
class AutomationEnvironment extends jest_environment_node_1.default {
|
|
44
|
+
constructor(config, context) {
|
|
45
|
+
var _a;
|
|
46
|
+
super(config, context);
|
|
47
|
+
const passedOptions = config.projectConfig.testEnvironmentOptions;
|
|
48
|
+
if (!passedOptions.app) {
|
|
49
|
+
throw new Error('"app" must be specified in testEnvironmentOptions');
|
|
50
|
+
}
|
|
51
|
+
this.winappdriverBin =
|
|
52
|
+
passedOptions.winAppDriverBin ||
|
|
53
|
+
path_1.default.join(process.env['PROGRAMFILES(X86)'], 'Windows Application Driver\\WinAppDriver.exe');
|
|
54
|
+
if (!fs_1.default.existsSync(this.winappdriverBin)) {
|
|
55
|
+
throw new Error(`Could not find WinAppDriver at searched location: "${this.winappdriverBin}"`);
|
|
56
|
+
}
|
|
57
|
+
const baseOptions = {
|
|
58
|
+
hostname: '127.0.0.1',
|
|
59
|
+
port: 4723,
|
|
60
|
+
// Level of logging verbosity: trace | debug | info | warn | error
|
|
61
|
+
logLevel: 'error',
|
|
62
|
+
// Default timeout for all waitFor* commands.
|
|
63
|
+
waitforTimeout: 30000,
|
|
64
|
+
// Default timeout in milliseconds for request
|
|
65
|
+
connectionRetryTimeout: 30000,
|
|
66
|
+
// Default request retries count
|
|
67
|
+
connectionRetryCount: 5,
|
|
68
|
+
};
|
|
69
|
+
this.useRootSession = !!passedOptions.useRootSession;
|
|
70
|
+
this.rootLaunchApp =
|
|
71
|
+
passedOptions.rootLaunchApp === undefined
|
|
72
|
+
? this.useRootSession
|
|
73
|
+
: !!passedOptions.rootLaunchApp;
|
|
74
|
+
if (this.useRootSession) {
|
|
75
|
+
this.rootWebDriverOptions = Object.assign({}, baseOptions, {
|
|
76
|
+
capabilities: {
|
|
77
|
+
app: 'Root',
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
'ms:experimental-webdriver': true,
|
|
80
|
+
},
|
|
81
|
+
}, passedOptions.webdriverOptions);
|
|
82
|
+
this.webDriverOptions = Object.assign({}, baseOptions, {
|
|
83
|
+
capabilities: {
|
|
84
|
+
// Save the name for now, we'll get the handle later
|
|
85
|
+
appTopLevelWindow: passedOptions.app,
|
|
86
|
+
// @ts-ignore
|
|
87
|
+
'ms:experimental-webdriver': true,
|
|
88
|
+
},
|
|
89
|
+
}, passedOptions.webdriverOptions);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.webDriverOptions = Object.assign({}, baseOptions, {
|
|
93
|
+
capabilities: {
|
|
94
|
+
app: resolveAppName(passedOptions.app),
|
|
95
|
+
...(passedOptions.appWorkingDir && {
|
|
96
|
+
appWorkingDir: passedOptions.appWorkingDir,
|
|
97
|
+
}),
|
|
98
|
+
...(passedOptions.appArguments && {
|
|
99
|
+
appArguments: passedOptions.appArguments,
|
|
100
|
+
}),
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
'ms:experimental-webdriver': true,
|
|
103
|
+
},
|
|
104
|
+
}, passedOptions.webdriverOptions);
|
|
105
|
+
}
|
|
106
|
+
this.webDriverOptions.capabilities = Object.assign(this.webDriverOptions.capabilities, (_a = passedOptions.webdriverOptions) === null || _a === void 0 ? void 0 : _a.capabilities);
|
|
107
|
+
this.channelOptions = {
|
|
108
|
+
enable: passedOptions.enableAutomationChannel === true,
|
|
109
|
+
port: passedOptions.automationChannelPort || 8603,
|
|
110
|
+
};
|
|
111
|
+
this.breakOnStart = passedOptions.breakOnStart === true;
|
|
112
|
+
}
|
|
113
|
+
async setup() {
|
|
114
|
+
await super.setup();
|
|
115
|
+
this.winAppDriverProcess = await spawnWinAppDriver(this.winappdriverBin, this.webDriverOptions.port);
|
|
116
|
+
if (this.useRootSession) {
|
|
117
|
+
// Extract out the saved window name
|
|
118
|
+
const appName = this.webDriverOptions.capabilities
|
|
119
|
+
.appTopLevelWindow;
|
|
120
|
+
if (this.rootLaunchApp) {
|
|
121
|
+
const appPackageName = resolveAppName(appName);
|
|
122
|
+
(0, child_process_1.spawnSync)('cmd', [
|
|
123
|
+
'/c',
|
|
124
|
+
'start',
|
|
125
|
+
`shell:AppsFolder\\${appPackageName}`,
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
// Set up the "Desktop" or Root session
|
|
129
|
+
const rootBrowser = await webdriverio.remote(this.rootWebDriverOptions);
|
|
130
|
+
// Get the list of windows
|
|
131
|
+
const allWindows = await rootBrowser.$$('//Window');
|
|
132
|
+
// Find our target window
|
|
133
|
+
let appWindow;
|
|
134
|
+
for (const window of allWindows) {
|
|
135
|
+
if ((await window.getAttribute('Name')) === appName) {
|
|
136
|
+
appWindow = window;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!appWindow) {
|
|
141
|
+
throw new Error(`Unable to find window with Name === '${appName}'.`);
|
|
142
|
+
}
|
|
143
|
+
// Swap the the window handle for WinAppDriver
|
|
144
|
+
const appWindowHandle = parseInt(await appWindow.getAttribute('NativeWindowHandle'), 10);
|
|
145
|
+
this.webDriverOptions.capabilities.appTopLevelWindow =
|
|
146
|
+
'0x' + appWindowHandle.toString(16);
|
|
147
|
+
await rootBrowser.deleteSession();
|
|
148
|
+
}
|
|
149
|
+
this.browser = await webdriverio.remote(this.webDriverOptions);
|
|
150
|
+
if (this.breakOnStart) {
|
|
151
|
+
readline_sync_1.default.question(chalk_1.default.bold.yellow('Breaking before tests start\n') +
|
|
152
|
+
'Press Enter to resume...');
|
|
153
|
+
}
|
|
154
|
+
if (this.channelOptions.enable) {
|
|
155
|
+
this.automationClient = await (0, automation_channel_1.waitForConnection)({
|
|
156
|
+
port: this.channelOptions.port,
|
|
157
|
+
});
|
|
158
|
+
this.global.automationClient = this.automationClient;
|
|
159
|
+
}
|
|
160
|
+
this.global.remote = webdriverio.remote;
|
|
161
|
+
this.global.browser = this.browser;
|
|
162
|
+
this.global.$ = this.browser.$.bind(this.browser);
|
|
163
|
+
this.global.$$ = this.browser.$$.bind(this.browser);
|
|
164
|
+
}
|
|
165
|
+
async teardown() {
|
|
166
|
+
var _a;
|
|
167
|
+
if (this.automationClient) {
|
|
168
|
+
this.automationClient.close();
|
|
169
|
+
}
|
|
170
|
+
if (this.browser) {
|
|
171
|
+
if (this.rootLaunchApp) {
|
|
172
|
+
// We started the app, so let's close it too
|
|
173
|
+
await this.browser.closeWindow();
|
|
174
|
+
}
|
|
175
|
+
await this.browser.deleteSession();
|
|
176
|
+
}
|
|
177
|
+
(_a = this.winAppDriverProcess) === null || _a === void 0 ? void 0 : _a.kill('SIGINT');
|
|
178
|
+
await super.teardown();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.default = AutomationEnvironment;
|
|
182
|
+
/**
|
|
183
|
+
* Starts a WinAppdriver process and resolves a promise with the process once
|
|
184
|
+
* it is ready to accept commands
|
|
185
|
+
*
|
|
186
|
+
* Inspired-by/stolen from https://github.com/licanhua/wdio-winappdriver-service
|
|
187
|
+
*/
|
|
188
|
+
async function spawnWinAppDriver(winappdriverBin, port) {
|
|
189
|
+
if (!fs_1.default.existsSync(winappdriverBin)) {
|
|
190
|
+
throw new Error(`Could not locate WinAppDriver binary at "${winappdriverBin}"`);
|
|
191
|
+
}
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const process = (0, child_process_1.spawn)(winappdriverBin, [port.toString()], { stdio: 'pipe' });
|
|
194
|
+
process.stdout.on('data', data => {
|
|
195
|
+
const s = data.toString('utf16le');
|
|
196
|
+
if (s.includes('Press ENTER to exit.')) {
|
|
197
|
+
resolve(process);
|
|
198
|
+
}
|
|
199
|
+
else if (s.includes('Failed to initialize')) {
|
|
200
|
+
reject(new Error('Failed to start WinAppDriver: ' + s));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
process.stderr.once('data', err => {
|
|
204
|
+
console.warn(err);
|
|
205
|
+
});
|
|
206
|
+
process.once('exit', exitCode => {
|
|
207
|
+
reject(new Error(`WinAppDriver CLI exited before timeout (exit code: ${exitCode})`));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Convert a package identity or path to exe to the form expected by a WinAppDriver capability
|
|
213
|
+
*/
|
|
214
|
+
function resolveAppName(appName) {
|
|
215
|
+
if (appName.endsWith('.exe')) {
|
|
216
|
+
return appName;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const packageFamilyName = (0, child_process_1.spawnSync)('powershell', [
|
|
220
|
+
`(Get-AppxPackage -Name ${appName}).PackageFamilyName`,
|
|
221
|
+
])
|
|
222
|
+
.stdout.toString()
|
|
223
|
+
.trim();
|
|
224
|
+
if (packageFamilyName.length === 0) {
|
|
225
|
+
// Rethrown below
|
|
226
|
+
throw new Error();
|
|
227
|
+
}
|
|
228
|
+
return `${packageFamilyName}!App`;
|
|
229
|
+
}
|
|
230
|
+
catch (_a) {
|
|
231
|
+
throw new Error(`Could not locate a package with identity "${appName}"`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=AutomationEnvironment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AutomationEnvironment.js","sourceRoot":"","sources":["../src/AutomationEnvironment.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Copyright (c) Microsoft Corporation.\n * Licensed under the MIT License.\n *\n * @format\n */\n\nimport chalk from 'chalk';\nimport {spawnSync, spawn, ChildProcess} from 'child_process';\nimport fs from '@react-native-windows/fs';\nimport path from 'path';\nimport readlineSync from 'readline-sync';\n\nimport NodeEnvironment from 'jest-environment-node';\nimport * as webdriverio from 'webdriverio';\nimport {BrowserObject, RemoteOptions} from 'webdriverio';\nimport {JestEnvironmentConfig} from '@jest/environment';\nimport type {EnvironmentContext} from '@jest/environment';\nimport {\n waitForConnection,\n AutomationClient,\n} from '@react-native-windows/automation-channel';\n\nexport type EnvironmentOptions = {\n /**\n * The application to launch. Can be a path to an exe, or a package identity\n * name (e.g. Microsoft.WindowsAlarms)\n */\n app?: string;\n\n /**\n * Instead of letting WinAppDriver launch and attach to the app directly,\n * create a Root (Desktop) session and search for the app's window.\n *\n * Note: This is only really necessary to correctly attach to packaged\n * WinAppSDK apps.\n */\n useRootSession?: boolean;\n\n /**\n * When using a Root (Desktop) session, still launch the test app during setup\n * and close it during cleanup.\n *\n * Defaults to true when using `useRootSession` to mimic the expected test\n * behavior, but can be disabled if you're trying to test an already\n * running app instance.\n */\n rootLaunchApp?: boolean;\n\n /**\n * Arguments to be passed to your application when launched\n */\n appArguments?: string;\n\n appWorkingDir?: string;\n\n enableAutomationChannel?: boolean;\n automationChannelPort?: number;\n winAppDriverBin?: string;\n breakOnStart?: boolean;\n webdriverOptions?: RemoteOptions;\n};\n\ntype AutomationChannelOptions = {\n enable: boolean;\n port: number;\n};\n\nexport default class AutomationEnvironment extends NodeEnvironment {\n private readonly rootWebDriverOptions?: RemoteOptions;\n private readonly webDriverOptions: RemoteOptions;\n private readonly channelOptions: AutomationChannelOptions;\n private readonly winappdriverBin: string;\n private readonly breakOnStart: boolean;\n private readonly useRootSession: boolean;\n private readonly rootLaunchApp: boolean;\n private winAppDriverProcess: ChildProcess | undefined;\n private browser: BrowserObject | undefined;\n private automationClient: AutomationClient | undefined;\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n super(config, context);\n const passedOptions: EnvironmentOptions =\n config.projectConfig.testEnvironmentOptions;\n\n if (!passedOptions.app) {\n throw new Error('\"app\" must be specified in testEnvironmentOptions');\n }\n\n this.winappdriverBin =\n passedOptions.winAppDriverBin ||\n path.join(\n process.env['PROGRAMFILES(X86)']!,\n 'Windows Application Driver\\\\WinAppDriver.exe',\n );\n\n if (!fs.existsSync(this.winappdriverBin)) {\n throw new Error(\n `Could not find WinAppDriver at searched location: \"${this.winappdriverBin}\"`,\n );\n }\n\n const baseOptions: RemoteOptions = {\n hostname: '127.0.0.1',\n port: 4723,\n\n // Level of logging verbosity: trace | debug | info | warn | error\n logLevel: 'error',\n\n // Default timeout for all waitFor* commands.\n waitforTimeout: 30000,\n\n // Default timeout in milliseconds for request\n connectionRetryTimeout: 30000,\n\n // Default request retries count\n connectionRetryCount: 5,\n };\n\n this.useRootSession = !!passedOptions.useRootSession;\n this.rootLaunchApp =\n passedOptions.rootLaunchApp === undefined\n ? this.useRootSession\n : !!passedOptions.rootLaunchApp;\n\n if (this.useRootSession) {\n this.rootWebDriverOptions = Object.assign(\n {},\n baseOptions,\n {\n capabilities: {\n app: 'Root',\n // @ts-ignore\n 'ms:experimental-webdriver': true,\n },\n },\n passedOptions.webdriverOptions,\n );\n\n this.webDriverOptions = Object.assign(\n {},\n baseOptions,\n {\n capabilities: {\n // Save the name for now, we'll get the handle later\n appTopLevelWindow: passedOptions.app,\n // @ts-ignore\n 'ms:experimental-webdriver': true,\n },\n },\n passedOptions.webdriverOptions,\n );\n } else {\n this.webDriverOptions = Object.assign(\n {},\n baseOptions,\n {\n capabilities: {\n app: resolveAppName(passedOptions.app),\n ...(passedOptions.appWorkingDir && {\n appWorkingDir: passedOptions.appWorkingDir,\n }),\n ...(passedOptions.appArguments && {\n appArguments: passedOptions.appArguments,\n }),\n\n // @ts-ignore\n 'ms:experimental-webdriver': true,\n },\n },\n passedOptions.webdriverOptions,\n );\n }\n\n this.webDriverOptions.capabilities = Object.assign(\n this.webDriverOptions.capabilities!,\n passedOptions.webdriverOptions?.capabilities,\n );\n\n this.channelOptions = {\n enable: passedOptions.enableAutomationChannel === true,\n port: passedOptions.automationChannelPort || 8603,\n };\n\n this.breakOnStart = passedOptions.breakOnStart === true;\n }\n\n async setup() {\n await super.setup();\n this.winAppDriverProcess = await spawnWinAppDriver(\n this.winappdriverBin,\n this.webDriverOptions.port!,\n );\n\n if (this.useRootSession) {\n // Extract out the saved window name\n const appName = (this.webDriverOptions.capabilities! as any)\n .appTopLevelWindow;\n\n if (this.rootLaunchApp) {\n const appPackageName = resolveAppName(appName);\n spawnSync('cmd', [\n '/c',\n 'start',\n `shell:AppsFolder\\\\${appPackageName}`,\n ]);\n }\n\n // Set up the \"Desktop\" or Root session\n const rootBrowser = await webdriverio.remote(this.rootWebDriverOptions);\n\n // Get the list of windows\n const allWindows = await rootBrowser.$$('//Window');\n\n // Find our target window\n let appWindow: webdriverio.Element | undefined;\n for (const window of allWindows) {\n if ((await window.getAttribute('Name')) === appName) {\n appWindow = window;\n break;\n }\n }\n\n if (!appWindow) {\n throw new Error(`Unable to find window with Name === '${appName}'.`);\n }\n\n // Swap the the window handle for WinAppDriver\n const appWindowHandle = parseInt(\n await appWindow!.getAttribute('NativeWindowHandle'),\n 10,\n );\n\n (this.webDriverOptions.capabilities as any).appTopLevelWindow =\n '0x' + appWindowHandle.toString(16);\n\n await rootBrowser.deleteSession();\n }\n\n this.browser = await webdriverio.remote(this.webDriverOptions);\n\n if (this.breakOnStart) {\n readlineSync.question(\n chalk.bold.yellow('Breaking before tests start\\n') +\n 'Press Enter to resume...',\n );\n }\n\n if (this.channelOptions.enable) {\n this.automationClient = await waitForConnection({\n port: this.channelOptions.port,\n });\n this.global.automationClient = this.automationClient;\n }\n\n this.global.remote = webdriverio.remote;\n this.global.browser = this.browser;\n this.global.$ = this.browser.$.bind(this.browser);\n this.global.$$ = this.browser.$$.bind(this.browser);\n }\n\n async teardown() {\n if (this.automationClient) {\n this.automationClient.close();\n }\n\n if (this.browser) {\n if (this.rootLaunchApp) {\n // We started the app, so let's close it too\n await this.browser.closeWindow();\n }\n await this.browser.deleteSession();\n }\n this.winAppDriverProcess?.kill('SIGINT');\n await super.teardown();\n }\n}\n\n/**\n * Starts a WinAppdriver process and resolves a promise with the process once\n * it is ready to accept commands\n *\n * Inspired-by/stolen from https://github.com/licanhua/wdio-winappdriver-service\n */\nasync function spawnWinAppDriver(\n winappdriverBin: string,\n port: number,\n): Promise<ChildProcess> {\n if (!fs.existsSync(winappdriverBin)) {\n throw new Error(\n `Could not locate WinAppDriver binary at \"${winappdriverBin}\"`,\n );\n }\n\n return new Promise((resolve, reject) => {\n const process = spawn(winappdriverBin, [port.toString()], {stdio: 'pipe'});\n\n process.stdout.on('data', data => {\n const s = data.toString('utf16le');\n if (s.includes('Press ENTER to exit.')) {\n resolve(process);\n } else if (s.includes('Failed to initialize')) {\n reject(new Error('Failed to start WinAppDriver: ' + s));\n }\n });\n\n process.stderr.once('data', err => {\n console.warn(err);\n });\n\n process.once('exit', exitCode => {\n reject(\n new Error(\n `WinAppDriver CLI exited before timeout (exit code: ${exitCode})`,\n ),\n );\n });\n });\n}\n\n/**\n * Convert a package identity or path to exe to the form expected by a WinAppDriver capability\n */\nfunction resolveAppName(appName: string): string {\n if (appName.endsWith('.exe')) {\n return appName;\n }\n\n try {\n const packageFamilyName = spawnSync('powershell', [\n `(Get-AppxPackage -Name ${appName}).PackageFamilyName`,\n ])\n .stdout.toString()\n .trim();\n\n if (packageFamilyName.length === 0) {\n // Rethrown below\n throw new Error();\n }\n\n return `${packageFamilyName}!App`;\n } catch {\n throw new Error(`Could not locate a package with identity \"${appName}\"`);\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*
|
|
5
|
+
* @format
|
|
6
|
+
*/
|
|
7
|
+
import AutomationEnvironment from './AutomationEnvironment';
|
|
8
|
+
import { app, AutomationElement } from './AutomationClient';
|
|
9
|
+
export { app };
|
|
10
|
+
export type { AutomationElement };
|
|
11
|
+
export default AutomationEnvironment;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*
|
|
6
|
+
* @format
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.app = void 0;
|
|
13
|
+
const AutomationEnvironment_1 = __importDefault(require("./AutomationEnvironment"));
|
|
14
|
+
const AutomationClient_1 = require("./AutomationClient");
|
|
15
|
+
Object.defineProperty(exports, "app", { enumerable: true, get: function () { return AutomationClient_1.app; } });
|
|
16
|
+
exports.default = AutomationEnvironment_1.default;
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;AAEH,oFAA4D;AAC5D,yDAA0D;AAElD,oFAFA,sBAAG,OAEA;AAEX,kBAAe,+BAAqB,CAAC","sourcesContent":["/**\n * Copyright (c) Microsoft Corporation.\n * Licensed under the MIT License.\n *\n * @format\n */\n\nimport AutomationEnvironment from './AutomationEnvironment';\nimport {app, AutomationElement} from './AutomationClient';\n\nexport {app};\nexport type {AutomationElement}\nexport default AutomationEnvironment;\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@react-native-windows/automation",
|
|
3
|
+
"version": "0.0.0-canary.1016",
|
|
4
|
+
"description": "UI Automation Suite for React Native Windows Applications",
|
|
5
|
+
"main": "lib-commonjs/index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/microsoft/react-native-windows",
|
|
9
|
+
"directory": "packages/@react-native-windows/automation"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"private": false,
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rnw-scripts build",
|
|
15
|
+
"clean": "rnw-scripts clean",
|
|
16
|
+
"lint": "rnw-scripts lint",
|
|
17
|
+
"lint:fix": "rnw-scripts lint:fix",
|
|
18
|
+
"watch": "rnw-scripts watch"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@react-native-windows/automation-channel": "0.0.0-canary.1016",
|
|
22
|
+
"@react-native-windows/fs": "^0.0.0-canary.70",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
|
24
|
+
"@typescript-eslint/parser": "^7.1.1",
|
|
25
|
+
"chalk": "^4.1.2",
|
|
26
|
+
"readline-sync": "1.4.10",
|
|
27
|
+
"webdriverio": "^6.9.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@jest/create-cache-key-function": "^29.2.1",
|
|
31
|
+
"@jest/environment": "^29.3.0",
|
|
32
|
+
"@jest/types": "^29.2.1",
|
|
33
|
+
"@rnw-scripts/eslint-config": "1.2.38",
|
|
34
|
+
"@rnw-scripts/just-task": "2.3.58",
|
|
35
|
+
"@rnw-scripts/ts-config": "2.0.6",
|
|
36
|
+
"@types/jest": "^29.2.2",
|
|
37
|
+
"@types/node": "^22.14.0",
|
|
38
|
+
"@types/readline-sync": "^1.4.4",
|
|
39
|
+
"eslint": "^8.19.0",
|
|
40
|
+
"prettier": "2.8.8",
|
|
41
|
+
"typescript": "5.0.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"jest": ">=29.0.3",
|
|
45
|
+
"jest-environment-node": ">=29.2.2"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"lib-commonjs",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"beachball": {
|
|
52
|
+
"defaultNpmTag": "canary",
|
|
53
|
+
"disallowedChangeTypes": [
|
|
54
|
+
"major",
|
|
55
|
+
"minor",
|
|
56
|
+
"patch",
|
|
57
|
+
"premajor",
|
|
58
|
+
"preminor",
|
|
59
|
+
"prepatch"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"promoteRelease": true,
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">= 22"
|
|
65
|
+
}
|
|
66
|
+
}
|