diff --git a/website/versioned_docs/version-20.x/api/actions.md b/website/versioned_docs/version-20.x/api/actions.md index d1658adf6b..ee68d15ab2 100644 --- a/website/versioned_docs/version-20.x/api/actions.md +++ b/website/versioned_docs/version-20.x/api/actions.md @@ -8,7 +8,7 @@ Use [expectations](expect.md) to verify element states. - [`.tap()`](#tappoint) - [`.multiTap()`](#multitaptimes) -- [`.longPress()`](#longpressduration) +- [`.longPress()`](#longpresspoint-duration) - [`.longPressAndDrag()`](#longpressanddragduration-normalizedpositionx-normalizedpositiony-targetelement-normalizedtargetpositionx-normalizedtargetpositiony-speed-holdduration) - [`.swipe()`](#swipedirection-speed-normalizedoffset-normalizedstartingpointx-normalizedstartingpointy) - [`.pinch()`](#pinchscale-speed-angle--ios-only) **iOS only** @@ -51,17 +51,27 @@ Simulates multiple taps on the element at its activation point. All taps are app await element(by.id('tappable')).multiTap(3); ``` -### `longPress(duration)` +### `longPress(point, duration)` -Simulates a long press on the element at its activation point. +Simulates a long press on the element at its activation point or at the specified point. -`duration` (iOS only) — press during time, in milliseconds. Optional (default is 1000 ms). +`point` — a point in the element’s coordinate space (optional, object with `x` and `y` numerical values, default is `null`). +`duration` — press during time, in milliseconds. Optional (defaults to the standard long-press duration for the platform). ```js await element(by.id('tappable')).longPress(); +await element(by.id('tappable')).longPress({x:5, y:10}); await element(by.id('tappable')).longPress(1500); +await element(by.id('tappable')).longPress({x:5, y:10}, 1500); ``` +:::note Important + +Custom durations should be used cautiously, as they can affect test consistency and user experience expectations. +They are typically necessary when testing components that behave differently from the platform's defaults or when simulating unique user interactions. + +::: + ### `longPressAndDrag(duration, normalizedPositionX, normalizedPositionY, targetElement, normalizedTargetPositionX, normalizedTargetPositionY, speed, holdDuration)` Simulates a long press on the element and then drag it to a position of another element. @@ -378,7 +388,7 @@ Simulates a pinch on the element with the provided options. await element(by.id('PinchableScrollView')).pinchWithAngle('outward', 'slow', 0); ``` -[`testID`]: ../guide/test-id.mdx +[`testID`]: ../guide/test-id.md [`by.type`]: ../api/matchers.md#bytypeclassname diff --git a/website/versioned_docs/version-20.x/guide/investigating-test-failure.mdx b/website/versioned_docs/version-20.x/guide/investigating-test-failure.mdx index cf7521a4a2..be216c26c2 100644 --- a/website/versioned_docs/version-20.x/guide/investigating-test-failure.mdx +++ b/website/versioned_docs/version-20.x/guide/investigating-test-failure.mdx @@ -29,7 +29,7 @@ In most cases we recommend using `debug` log level to understand the failed test ## Missing elements -If your tests are failing due to non-existent or invisible elements, you can [inspect the native view hierarchy](test-id.mdx#find-your-testid) to understand better the failure reason. +If your tests are failing due to non-existent or invisible elements, you can [inspect the native view hierarchy](test-id.md#finding-your-test-id) to understand better the failure reason. ## More recipes diff --git a/website/versioned_docs/version-20.x/guide/partials/_find-test-id-android.mdx b/website/versioned_docs/version-20.x/guide/partials/_find-test-id-android.mdx deleted file mode 100644 index b071870e0d..0000000000 --- a/website/versioned_docs/version-20.x/guide/partials/_find-test-id-android.mdx +++ /dev/null @@ -1,14 +0,0 @@ -1. Make sure that React Native packager is already running. If not, you can start it with: - ```bash - npx react-native start - ``` -1. Launch Android Studio. -1. Open `Tools > Layout Inspector` tool: - ![tag attribute with testID value](../../img/android/layoutInspector.png) -1. Build your application from Android Studio. -1. After you run your app from Android Studio, the `Layout Inspector` should automatically attach to the process and show the hierarchy of your screen. You will see the snapshot of your screen, where you can focus on any component with a click. - :::tip - If `Layout Inspector` doesn't attach to process from Android Studio, or you build it in a different way – you can attach to your app process manually using `Select Process` dropdown. - ::: -1. Select the component you need, and you will see your actual testID value under the `tag` attribute. - ![tag attribute with testID value](../../img/android/tagAttributeAndroid.png) diff --git a/website/versioned_docs/version-20.x/guide/partials/_find-test-id-ios.mdx b/website/versioned_docs/version-20.x/guide/partials/_find-test-id-ios.mdx deleted file mode 100644 index 68013fa0a2..0000000000 --- a/website/versioned_docs/version-20.x/guide/partials/_find-test-id-ios.mdx +++ /dev/null @@ -1,16 +0,0 @@ -1. Build and start your app in _debug_ mode as you usually do, e.g.: - ```sh - detox build -c ios.sim.debug - npx react-native start - npx react-native run-ios - ``` -1. Open your iOS project in Xcode, e.g. `YourProject/ios/YourProject.xcworkspace`. -1. Go to `Debug > Attach to Process` and select your app process (it is usually on top of the list). - ![attach to debug process ios](../../img/ios/attachToProcess.png) - You will see a new device started with your app. -1. Open the `AppDelegate` file: - ![AppDelegate file](../../img/ios/appDelegate.png) -1. Click `Debug View Hierarchy` button on the bottom panel: - ![Debug Hierarchy button](../../img/ios/debugHierarchyButton.png) -1. Select the component you need, and you will see your actual `testID` value under the `Accessibility > Identifier` attribute. - ![accessibility attribute with testID value](../../img/ios/accessibilityAttributeIOS.png) diff --git a/website/versioned_docs/version-20.x/guide/test-id.md b/website/versioned_docs/version-20.x/guide/test-id.md new file mode 100644 index 0000000000..c27ee99ac0 --- /dev/null +++ b/website/versioned_docs/version-20.x/guide/test-id.md @@ -0,0 +1,202 @@ +# Adding test ID's to your components + +:::info Note + +This guide was written primarily for React Native apps, but it can be generalized for testing any app, including native apps. + +::: + +While [view-element matching](../api/matchers.md) can be done in numerous ways, it is always the best idea to match based on something unique and decoupled, as it ensures that the test code is clear, stable and sustainable over time. + +We recommend assigning unique test ID's to the elements you're aiming to interact with in your tests, and prefering matching based on those rather than on anything else. Test ID's are the least likely to change over time (compared with raw text, for example), and are locale-agnostic. Furthermore, utilizing unique test ID's across the app not only simplifies the identification and interaction with specific elements but also enhances code navigability, making it easier to locate elements when traversing the codebase. + +In React Native applications, `View` components have a dedicated [test ID property](https://reactnative.dev/docs/view#testid) that can be utilized: + +```jsx + + + Next + + +``` + +For native apps, test ID's can be assigned by setting a value for the following properties: + +- **iOS:** [`accessibilityIdentifier`](https://developer.apple.com/documentation/uikit/uiaccessibilityidentification/1623132-accessibilityidentifier) +- **Android:** Default [viewTag](https://developer.android.com/reference/android/view/View#tags) + +## Pass testID to your native components + +Passing a `testID` to your custom component props has no effect until you forward it down to a native component like `` or `` +that implements rendering it as an accessibility identifier in the native component hierarchy: + +![Pass testID to native component](../img/test-id/passTestID.png) + +For example, you have `` and you pass a `testID` to it: + +```jsx title="YourScreen.jsx" +function YourScreen() { + return ( + + ); +} +``` + +Make sure that your implementation passes `testID` to some React Native component that supports it: + +```jsx title="YourCustomComponent.jsx" +function YourCustomComponent(props) { + return ( +// highlight-next-line + + Some text + + ); +} +``` + +### Child elements + +If your component has several useful child elements, it is even a better idea to assign them some derived test IDs, e.g.: + +```jsx title="YourCustomComponent.jsx" +function YourCustomComponent(props) { + return ( +// highlight-next-line + + Some text + + ); +} +``` + +That way, you could refer to specific elements in Detox tests via the most basic and least ambiguous `by.id` matchers, e.g.: + +```js +expect(element(by.id('YourCustomComponent'))).toBeVisible(); +expect(element(by.id('YourCustomComponent.label'))).toHaveText('Some text'); +``` + +### Repetitive components + +It is highly not recommended to use non-unique `testID`, e.g. when your components get rendered in any sort of repeater or virtualized list: + +```jsx title="YourScreen.jsx" +const ITEMS = [ + { title: 'First Item' }, + { title: 'Second Item' }, + { title: 'Third Item' }, +]; + +function YourScreen() { + const renderItem = ({ item }) => ( +// highlight-next-line + + ); + + return ( + + ); +} +``` + +This would be a violation of accessibility guidelines and unnecessary complication for your test matchers. +You’d also have to use extra matchers and `.atIndex` clarification: + +```js +expect(element(by.id('listItem')).atIndex(2)).toHaveText('Third Item'); +``` + +Instead, you could generate a unique `testID` for every list item with the `index` property: + +```jsx + const renderItem = ({ item, index }) => ( + + ); +``` + +That way, your assertion would become simpler and more deterministic: + +```js +expect(element(by.id('listItem.3'))).toHaveText('Third Item'); +``` + +![testID for repetitive components](../img/test-id/repetitiveComponentTestID.png) + +## Finding your test ID + +:::note + +Incorrect or absent `testID` is a common cause for test failure. +If your test can't find your `testID` and you can't see it either using tools described below, that usually means you haven't passed it down to this component. +Make sure you keep forwarding it down until it reaches a native component. + +::: + +To make sure your `testID` is indeed rendered in your app, you can use such tools as MacOS' built-in [accessibility inspector](https://developer.apple.com/documentation/accessibility/inspecting-the-accessibility-of-screens) for iOS, and [Detox Layout-inspector](https://github.com/wix-incubator/detox-inspector) (setup required) for Android. + +## Test ID naming - Best practices + +Test ID's work best when they are unique, simple and concise. Here are our recommendations regarding what rules to follow in terms of naming. + +### Use a consistent naming system + +Decide upon a system by which test ID's are named, and stick with it. + +1. Use a consistent naming convention. An `ITEM_NAME_ALL_CAPS` convention and an `ItemNameUpperCamelCase` are both ok, but **don't use them either intermittently nor in conjunction:** + + - `SITE_LIST_ROOT` & `SITE_LIST_ITEM_1` - :white\_check\_mark: + - `SITE_LIST_ROOT` & `SiteList_Item1` - :x: + - `SITE_LIST_Item1` - :x: +1. Consistently apply notations for special items. For example: + - A `_ROOT` postfix for screen-root or list-root items (e.g. `SITE_LIST_ROOT`) + - A `_BTN` for buttons / touchable CTA elements +1. Apply consistent prefixes as categories in order to introduce a top-level context to the test ID, distinguishing it from similar ones in various places in the app. The name of the associated screen can be useful in that sense. For example: `EDIT_PROFILE_SCREEN.DONE_BTN` is better than just `DONE_BTN` for a button that is inside a user profile editing screen. Also, things such as `NAV_TABS.`, `TOP_TABS.` and `SIDE_MENU.` can be used as good context providers. +1. As explained in the section on passing test ID's to _child_ elements, drill down to the details of elements via a _chain of contexts_. Given the parent element-group of an element (for example, a card in a feed), use its own test ID as a prefix for the sub-items (e.g. an options "meatballs" / "kebab" CTA or an _edit_ button). For example: + - `SITE_LIST_ITEM1` ⇒ + - `SITE_LIST_ITEM1.OPTIONS` + - `SITE_LIST_ITEM1.EDIT_BTN` + - `SITE_LIST_ITEM1.TITLE` +1. In a large-scale, multi-module environment, apply a consistent module identifier as the module's test ID's prefix. For example: + - `AUTH.LOGIN_SCREEN.EDIT_PASSWORD` - the `AUTH.` prefix suggests that were are under the context of a module handling Authentication matters. + +:::tip + +Don't hesitate to articulate a well defined conventions manifest that all teams should adhere to. + +::: + +### Use simple names + +Stick to simple alpha-numeric characters, and simple separators. When it comes to test ID's, there's usually no reason to use special characters or emojis. + +In addition, use test ID that clearly describe the associated element, but are also concise. For example: + +- `SITE_LIST_ROOT` - :white\_check\_mark: +- `MAIN_SITE_LIST_WRAPPER_ELEMENT` - :x: +- `SITE_LIST@ITEM$1` - :x: + +### Dissociate test ID names + +Make sure the names you give test ID's are completely decoupled and dissociated from everything else in the system. In particular - + +:::warning Attention + +By all means, **never utilize the element's text / label in the naming of a test ID!** +Namely, a test ID should never use `text` or `label` props passed to a React Native component. + +::: + +There are at least 2 reasons why this is a very important rule: + +1. Alternation of test ID's can lead to broken tests (test-ID based matchers become obsolete), and on-screen text can change frequently. +1. In apps supporting multiple languages, the on-screen text is likely to be different in each language. You want the same test code to be compatible with any language set into the test device, and you therefore need it have as little awareness to it as possible. Using test ID's is the best means to keep it that way. + +### Examples + +Based on the `ALL_CAPS` convention, here is an example of a screen which test ID's illustrate the principles of this discussion: + +![Test ID: Naming example](../img/test-id/naming-example.png) diff --git a/website/versioned_docs/version-20.x/guide/test-id.mdx b/website/versioned_docs/version-20.x/guide/test-id.mdx deleted file mode 100644 index 6a26deba73..0000000000 --- a/website/versioned_docs/version-20.x/guide/test-id.mdx +++ /dev/null @@ -1,146 +0,0 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import Android from './partials/_find-test-id-android.mdx'; -import IOS from './partials/_find-test-id-ios.mdx'; - -# Adding testID to your components - -:::info Note - -This guide is applicable only for React Native applications. - -::: - -It is always the best idea to match your element by something unique. We recommend using [`testID` property](https://reactnative.dev/docs/view#testid) supported by most React Native components: - -```jsx - - - Some text - - -``` - -## Pass testID to your native components - -Passing a `testID` to your custom component props has no effect until you forward it down to a native component like `` or `` -that implements rendering it as an accessibility identifier in the native component hierarchy: - -![Pass testID to native component](../img/passTestID.png) - -For example, you have `` and you pass a `testID` to it: - -```jsx title="YourScreen.jsx" -function YourScreen() { - return ( - - ); -} -``` - -Make sure that your implementation passes `testID` to some React Native component that supports it: - -```jsx title="YourCustomComponent.jsx" -function YourCustomComponent(props) { - return ( -// highlight-next-line - - Some text - - ); -} -``` - -### Child elements - -If your component has several useful child elements, it is even a better idea to assign them some derived test IDs, e.g.: - -```jsx title="YourCustomComponent.jsx" -function YourCustomComponent(props) { - return ( -// highlight-next-line - - Some text - - ); -} -``` - -That way, you could refer to specific elements in Detox tests via the most basic and least ambiguous `by.id` matchers, e.g.: - -```js -expect(element(by.id('YourCustomComponent'))).toBeVisible(); // the view is visible -expect(element(by.id('YourCustomComponent.label'))).toHaveText('Some text'); // the label has some text -``` - -### Repetitive components - -It is highly not recommended to use non-unique `testID`, e.g. when your components get rendered in any sort of repeater or virtualized list: - -```jsx title="YourScreen.jsx" -const ITEMS = [ - { title: 'First Item' }, - { title: 'Second Item' }, - { title: 'Third Item' }, -]; - -function YourScreen() { - const renderItem = ({ item }) => ( -// highlight-next-line - - ); - - return ( - - ); -} -``` - -This would be a violation of accessibility guidelines and unnecessary complication for your test matchers. -You’d also have to use extra matchers and `.atIndex` clarification: - -```js -expect(element(by.id('listItem')).atIndex(2)).toHaveText('Third Item'); -``` - -Instead, you could generate a unique `testID` for every list item with the `index` property: - -```jsx - const renderItem = ({ item, index }) => ( - - ); -``` - -That way, your assertion would become simpler and more deterministic: - -```js -expect(element(by.id('listItem.3'))).toHaveText('Third Item'); -``` - - -![testID for repetitive components](../img/repetitiveComponentTestID.png) - -## Find your testID - -:::note - -Incorrect or absent `testID` is a common cause for test failure. -If your test can't find your `testID` and you can't see it either using tools described below, that usually means you haven't passed it down to this component. -Make sure you keep forwarding it down until it reaches a native component. - -::: - -To make sure your `testID` is indeed rendered in your app, you can use such tools as "View Hierarchy" for iOS and "Layout Inspector" for Android. - - - - - - - - - - diff --git a/website/versioned_docs/version-20.x/img/android/layoutInspector.png b/website/versioned_docs/version-20.x/img/android/layoutInspector.png deleted file mode 100644 index 96d81712cc..0000000000 Binary files a/website/versioned_docs/version-20.x/img/android/layoutInspector.png and /dev/null differ diff --git a/website/versioned_docs/version-20.x/img/android/tagAttributeAndroid.png b/website/versioned_docs/version-20.x/img/android/tagAttributeAndroid.png deleted file mode 100644 index 9be4089ad8..0000000000 Binary files a/website/versioned_docs/version-20.x/img/android/tagAttributeAndroid.png and /dev/null differ diff --git a/website/versioned_docs/version-20.x/img/ios/accessibilityAttributeIOS.png b/website/versioned_docs/version-20.x/img/ios/accessibilityAttributeIOS.png deleted file mode 100644 index 5195d511f5..0000000000 Binary files a/website/versioned_docs/version-20.x/img/ios/accessibilityAttributeIOS.png and /dev/null differ diff --git a/website/versioned_docs/version-20.x/img/ios/appDelegate.png b/website/versioned_docs/version-20.x/img/ios/appDelegate.png deleted file mode 100644 index 3519a3b91a..0000000000 Binary files a/website/versioned_docs/version-20.x/img/ios/appDelegate.png and /dev/null differ diff --git a/website/versioned_docs/version-20.x/img/ios/attachToProcess.png b/website/versioned_docs/version-20.x/img/ios/attachToProcess.png deleted file mode 100644 index ff9e3abe08..0000000000 Binary files a/website/versioned_docs/version-20.x/img/ios/attachToProcess.png and /dev/null differ diff --git a/website/versioned_docs/version-20.x/img/ios/debugHierarchyButton.png b/website/versioned_docs/version-20.x/img/ios/debugHierarchyButton.png deleted file mode 100644 index 5c9299b3a0..0000000000 Binary files a/website/versioned_docs/version-20.x/img/ios/debugHierarchyButton.png and /dev/null differ diff --git a/website/versioned_docs/version-20.x/img/test-id/naming-example.png b/website/versioned_docs/version-20.x/img/test-id/naming-example.png new file mode 100644 index 0000000000..190b5c85eb Binary files /dev/null and b/website/versioned_docs/version-20.x/img/test-id/naming-example.png differ diff --git a/website/versioned_docs/version-20.x/img/passTestID.png b/website/versioned_docs/version-20.x/img/test-id/passTestID.png similarity index 100% rename from website/versioned_docs/version-20.x/img/passTestID.png rename to website/versioned_docs/version-20.x/img/test-id/passTestID.png diff --git a/website/versioned_docs/version-20.x/img/repetitiveComponentTestID.png b/website/versioned_docs/version-20.x/img/test-id/repetitiveComponentTestID.png similarity index 100% rename from website/versioned_docs/version-20.x/img/repetitiveComponentTestID.png rename to website/versioned_docs/version-20.x/img/test-id/repetitiveComponentTestID.png diff --git a/website/versioned_docs/version-20.x/introduction/your-first-test.mdx b/website/versioned_docs/version-20.x/introduction/your-first-test.mdx index ded7913760..93867c22aa 100644 --- a/website/versioned_docs/version-20.x/introduction/your-first-test.mdx +++ b/website/versioned_docs/version-20.x/introduction/your-first-test.mdx @@ -97,7 +97,7 @@ element(by.text('Element text')); :::info Best practice Try to use [`by.id()`](../api/matchers.md#byidid) matcher wherever possible. -[Explore our guide](../guide/test-id.mdx) to learn how to work with `testID` props. +[Explore our guide](../guide/test-id.md) to learn how to work with `testID` props. :::