Add AutomationClient and AutomationElement Facade (#8517)
* Add AutomationClient and AutomationElement Facade WebDriverIO exposes a whole webdriver client, with loads of functionality that doesn't work or make sense on a desktop app. This facade exposes the bits that are supported by WinAppDriver, in an easier API. It also lets us potentially decouple from WebDriverIO if we want to move to a differnt WebDriver client. Currently being done in the test app, but will be extracted to a separate package after to allow use in apps outside the repo. * update docs * Keep dumpVisualTree outside of AutomationClient for now * fix lint * raw example
This commit is contained in:
Родитель
7d3e48c60d
Коммит
46fc2070e9
|
@ -116,37 +116,13 @@ describe('FancyWidget', () => {
|
|||
|
||||
test('FancyWidget is populated with placeholder', async () => {
|
||||
// Query for an element with accessibilityId of "foo" (see "locators" below)
|
||||
const field = await $('~foo');
|
||||
const field = await app.findElementByTestID('foo');
|
||||
expect(await field.getText()).toBe('placeholder');
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
### Locators to find UI Element
|
||||
|
||||
No matter what JavaScript framework you choose for native app testing, you have to use one of the locators which is described in [mobile JSON wire protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md#locator-strategies). Since locators are implemented significant different on iOS, Android and Windows, as below I only talk about the locators for Windows.
|
||||
|
||||
[Locators WinAppDriver supports](https://github.com/microsoft/WinAppDriver/blob/master/Docs/AuthoringTestScripts.md#supported-locators-to-find-ui-elements)
|
||||
|
||||
WinAppDriver provides rich API to help locate the UI element. If [testID](https://facebook.github.io/react-native/docs/picker-item#testid) is specified in React Native app for Windows, the locator strategy should choose `accessibility id`.
|
||||
|
||||
| **Client API** | **Locator Strategy** | **Matched Attribute in inspect.exe** | **Example** |
|
||||
| --- | --- | --- | --- |
|
||||
| FindElementByAccessibilityId | accessibility id | AutomationId | AppNameTitle |
|
||||
| FindElementByClassName | class name | ClassName | TextBlock |
|
||||
| FindElementById | Id | RuntimeId (decimal) | 42.333896.3.1 |
|
||||
| FindElementByName | Name | Name | Calculator |
|
||||
| FindElementByTagName | tag name | LocalizedControlType (upper camel case) | Text |
|
||||
| FindElementByXPath | Xpath | Any | //Button[0] |
|
||||
|
||||
[Selectors WebDriverIO supports](https://webdriver.io/docs/selectors.html#mobile-selectors)
|
||||
|
||||
| **Client API by Example** | **Locator Strategy** |
|
||||
| --- | --- |
|
||||
| $('~AppNameTitle') | accessibility id |
|
||||
| $('TextBlock') | class name |
|
||||
|
||||
### Adding a custom RNTester page
|
||||
|
||||
Before adding a custom page, consider whether the change can be made to an existing RNTester page and upstreamed. If needed, new examples may be integrated into the Windows fork of RNTester, [`@react-native-windows/tester`](../packages/@react-native-windows/tester).
|
||||
|
|
|
@ -2,8 +2,6 @@ module.exports = {
|
|||
extends: ['@rnw-scripts'],
|
||||
parserOptions: { tsconfigRootDir: __dirname },
|
||||
globals: {
|
||||
$: 'readonly',
|
||||
browser: 'readonly',
|
||||
expect: 'readonly',
|
||||
fail: 'readonly',
|
||||
rpcClient: 'readonly',
|
||||
|
|
|
@ -23,7 +23,7 @@ global.jasmine.getEnv().addReporter({
|
|||
);
|
||||
|
||||
const filename = path.join(screenshotDir, friendlySpecName);
|
||||
await browser.saveScreenshot(filename);
|
||||
await global.browser.saveScreenshot(filename);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {goToComponentExample, dumpVisualTree} from './framework';
|
||||
import {app, goToComponentExample, dumpVisualTree} from './framework';
|
||||
|
||||
beforeAll(async () => {
|
||||
await goToComponentExample('Display:none Style');
|
||||
|
@ -27,6 +27,8 @@ describe('DisplayNoneTest', () => {
|
|||
});
|
||||
|
||||
async function toggleDisplayNone() {
|
||||
const showDisplayNoneToggle = await $('~toggle-display:none');
|
||||
const showDisplayNoneToggle = await app.findElementByTestID(
|
||||
'toggle-display:none',
|
||||
);
|
||||
await showDisplayNoneToggle.click();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {goToComponentExample, dumpVisualTree} from './framework';
|
||||
import {app, goToComponentExample, dumpVisualTree} from './framework';
|
||||
|
||||
beforeAll(async () => {
|
||||
await goToComponentExample('LegacyControlStyleTest');
|
||||
|
@ -34,6 +34,6 @@ describe('LegacyControlStyleTest', () => {
|
|||
});
|
||||
|
||||
async function toggleControlBorder() {
|
||||
const showBorderToggle = await $('~show-border-toggle');
|
||||
const showBorderToggle = await app.findElementByTestID('show-border-toggle');
|
||||
await showBorderToggle.click();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {goToComponentExample, dumpVisualTree} from './framework';
|
||||
import {app, goToComponentExample, dumpVisualTree} from './framework';
|
||||
|
||||
beforeAll(async () => {
|
||||
await goToComponentExample('LegacyImageTest');
|
||||
|
@ -40,11 +40,13 @@ describe('LegacyImageTest', () => {
|
|||
});
|
||||
|
||||
async function toggleImageBorder() {
|
||||
const imageBorderToggle = await $('~toggle-border-button');
|
||||
const imageBorderToggle = await app.findElementByTestID(
|
||||
'toggle-border-button',
|
||||
);
|
||||
await imageBorderToggle.click();
|
||||
}
|
||||
|
||||
async function toggleRTLMode() {
|
||||
const rtlToggleButton = await $('~set-rtl-button');
|
||||
const rtlToggleButton = await app.findElementByTestID('set-rtl-button');
|
||||
await rtlToggleButton.click();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {goToComponentExample} from './framework';
|
||||
import {goToComponentExample, app} from './framework';
|
||||
|
||||
beforeAll(async () => {
|
||||
await goToComponentExample('LegacyLoginTest');
|
||||
|
@ -67,32 +67,34 @@ describe('LegacyLoginTest', () => {
|
|||
});
|
||||
|
||||
async function setUsername(username: string) {
|
||||
const usernameField = await $('~username-field');
|
||||
const usernameField = await app.findElementByTestID('username-field');
|
||||
await usernameField.setValue(username);
|
||||
}
|
||||
|
||||
async function setPassword(password: string) {
|
||||
const passwordField = await $('~password-field');
|
||||
const passwordField = await app.findElementByTestID('password-field');
|
||||
await passwordField.setValue(password);
|
||||
}
|
||||
|
||||
async function appendPassword(password: string) {
|
||||
const passwordField = await $('~password-field');
|
||||
const passwordField = await app.findElementByTestID('password-field');
|
||||
await passwordField.addValue('End');
|
||||
await passwordField.addValue(password);
|
||||
}
|
||||
|
||||
async function toggleShowPassword() {
|
||||
const showPasswordToggle = await $('~show-password-toggle');
|
||||
const showPasswordToggle = await app.findElementByTestID(
|
||||
'show-password-toggle',
|
||||
);
|
||||
await showPasswordToggle.click();
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
const submitButton = await $('~submit-button');
|
||||
const submitButton = await app.findElementByTestID('submit-button');
|
||||
await submitButton.click();
|
||||
}
|
||||
|
||||
async function getLoginResult(): Promise<string> {
|
||||
const loginResult = await $('~result-text');
|
||||
const loginResult = await app.findElementByTestID('result-text');
|
||||
return await loginResult.getText();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {goToComponentExample} from './framework';
|
||||
import {goToComponentExample, app} from './framework';
|
||||
|
||||
beforeAll(async () => {
|
||||
await goToComponentExample('LegacyTextInputTest');
|
||||
|
@ -86,21 +86,21 @@ describe('LegacyTextInputTest', () => {
|
|||
});
|
||||
|
||||
async function textInputField() {
|
||||
return await $('~textinput-field');
|
||||
return await app.findElementByTestID('textinput-field');
|
||||
}
|
||||
|
||||
async function autoCapsTextInputField() {
|
||||
return await $('~auto-caps-textinput-field');
|
||||
return await app.findElementByTestID('auto-caps-textinput-field');
|
||||
}
|
||||
|
||||
async function multiLineTextInputField() {
|
||||
return await $('~multi-line-textinput-field');
|
||||
return await app.findElementByTestID('multi-line-textinput-field');
|
||||
}
|
||||
|
||||
async function assertLogContains(text: string) {
|
||||
const textLogComponent = await $('~textinput-log');
|
||||
const textLogComponent = await app.findElementByTestID('textinput-log');
|
||||
|
||||
await browser.waitUntil(
|
||||
await app.waitUntil(
|
||||
async () => {
|
||||
const loggedText = await textLogComponent.getText();
|
||||
return loggedText.split('\n').includes(text);
|
||||
|
@ -112,9 +112,9 @@ async function assertLogContains(text: string) {
|
|||
}
|
||||
|
||||
async function assertLogContainsInOrder(expectedLines: string[]) {
|
||||
const textLogComponent = await $('~textinput-log');
|
||||
const textLogComponent = await app.findElementByTestID('textinput-log');
|
||||
|
||||
await browser.waitUntil(
|
||||
await app.waitUntil(
|
||||
async () => {
|
||||
const loggedText = await textLogComponent.getText();
|
||||
const actualLines = loggedText.split('\n');
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT License.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
/* global $:false, browser:false */
|
||||
|
||||
/**
|
||||
* Projection of a WebDriver Element, with functions corresponding to supported
|
||||
* WinAppDriver APIs.
|
||||
*
|
||||
* See https://github.com/microsoft/WinAppDriver/blob/master/Docs/SupportedAPIs.md
|
||||
*/
|
||||
export type AutomationElement = Pick<
|
||||
WebdriverIO.Element,
|
||||
| 'addValue'
|
||||
| 'clearValue'
|
||||
| 'click'
|
||||
| 'doubleClick'
|
||||
| 'getAttribute'
|
||||
| 'getLocation'
|
||||
| 'getSize'
|
||||
| 'getText'
|
||||
| 'getValue'
|
||||
| 'isDisplayed'
|
||||
| 'isDisplayedInViewport'
|
||||
| 'isEnabled'
|
||||
| 'isEqual'
|
||||
| 'isSelected'
|
||||
| 'moveTo'
|
||||
| 'saveScreenshot'
|
||||
| 'setValue'
|
||||
| 'touchAction'
|
||||
| 'waitForDisplayed'
|
||||
| 'waitForExist'
|
||||
>;
|
||||
|
||||
/**
|
||||
* WebDriverIO exposes a whole webdriver client, with loads of functionality
|
||||
* that doesn't work or make sense on a desktop app. This facade exposes the
|
||||
* bits that consistently work, in an easier API.
|
||||
*
|
||||
* This may not be 100% complete
|
||||
*/
|
||||
export const app = {
|
||||
/**
|
||||
* Find an element by testID property
|
||||
*/
|
||||
findElementByTestID: (id: string): Promise<AutomationElement> => $(`~${id}`),
|
||||
|
||||
/**
|
||||
* Find an element by Automation ID
|
||||
*
|
||||
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.automationid?view=net-5.0
|
||||
*/
|
||||
findElementByAutomationID: (id: string): Promise<AutomationElement> =>
|
||||
$(`~${id}`),
|
||||
|
||||
/**
|
||||
* Finds an element by the name of its class name (e.g. ListViewItem)
|
||||
*
|
||||
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.automationelementinformation.classname?view=net-5.0
|
||||
*/
|
||||
findElementByClassName: (className: string): Promise<AutomationElement> =>
|
||||
$(className),
|
||||
|
||||
/**
|
||||
* Find element by ControlType (e.g. Button, CheckBox)
|
||||
*
|
||||
* https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.controltype?view=net-5.0
|
||||
*/
|
||||
findElementByControlType: (controlType: string): Promise<AutomationElement> =>
|
||||
$(`<${controlType} />`),
|
||||
|
||||
/**
|
||||
* Find element by a WinAppDriver compatible XPath Selector (e.g. '//Button[@AutomationId=\"MoreButton\"]')
|
||||
*/
|
||||
findElementByXPath: (xpath: string): Promise<AutomationElement> => $(xpath),
|
||||
|
||||
setWindowSize: browser.setWindowSize.bind(browser),
|
||||
setWindowPosition: browser.setWindowPosition.bind(browser),
|
||||
getWindowPosition: browser.getWindowPosition.bind(browser),
|
||||
getWindowSize: browser.getWindowSize.bind(browser),
|
||||
switchWindow: browser.switchWindow.bind(browser),
|
||||
|
||||
waitUntil: browser.waitUntil.bind(browser),
|
||||
};
|
|
@ -5,11 +5,13 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {app} from './AutomationClient';
|
||||
|
||||
/**
|
||||
* Visit an example on the RNTester Components tab
|
||||
*/
|
||||
export async function goToComponentExample(example: string) {
|
||||
const componentsTabButton = await $('~components-tab');
|
||||
const componentsTabButton = await app.findElementByTestID('components-tab');
|
||||
await componentsTabButton.click();
|
||||
await goToExample(example);
|
||||
}
|
||||
|
@ -18,24 +20,24 @@ export async function goToComponentExample(example: string) {
|
|||
* Visit an example on the RNTester APIs tab
|
||||
*/
|
||||
export async function goToApiExample(example: string) {
|
||||
const componentsTabButton = await $('~apis-tab');
|
||||
const componentsTabButton = await app.findElementByTestID('apis-tab');
|
||||
await componentsTabButton.click();
|
||||
await goToExample(example);
|
||||
}
|
||||
|
||||
async function goToExample(example: string) {
|
||||
// Filter the list down to the one test, to improve the stability of selectors
|
||||
const searchBox = await $('~explorer_search');
|
||||
const searchBox = await app.findElementByTestID('explorer_search');
|
||||
await searchBox.setValue(regexEscape(example));
|
||||
|
||||
const exampleButton = await $(`~${example}`);
|
||||
const exampleButton = await app.findElementByTestID(example);
|
||||
await exampleButton.click();
|
||||
|
||||
// Make sure we've launched the example by waiting until the search box is
|
||||
// no longer present, but make sure we haven't crashed by checking that nav
|
||||
// buttons are still visible
|
||||
await browser.waitUntil(async () => !(await exampleButton.isDisplayed()));
|
||||
const componentsTab = await $('~components-tab');
|
||||
await app.waitUntil(async () => !(await exampleButton.isDisplayed()));
|
||||
const componentsTab = await app.findElementByTestID('components-tab');
|
||||
expect(await componentsTab.isDisplayed()).toBe(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
export * from './AutomationClient';
|
||||
export * from './Navigation';
|
||||
export * from './TreeDump';
|
||||
|
|
Загрузка…
Ссылка в новой задаче