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

Experimental test selector API #18607

Merged
merged 10 commits into from May 5, 2020
Merged

Experimental test selector API #18607

merged 10 commits into from May 5, 2020

Conversation

bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Apr 14, 2020

Adds several new experimental APIs to aid with automated testing.

This is not an RFC, although the PR includes some basic documentation in a similar format.

Types of selectors

Each of the methods below accepts an array of "selectors" that identifies a path (or paths) through a React tree. There are four basic selector types:

  • Component: Matches Fibers with the specified React component type
  • Role: Matches Host Instances matching the (explicit or implicit) accessibility role.
  • Test name: Matches Host Instances with a data-testname attribute.
  • Text: Matches Host Instances that directly contain the specified text.

There is also a special lookahead selector type that enables further matching within a path (without actually including the path in the result). This selector type was inspired by the :has() CSS pseudo-class. It enables e.g. matching a <section> that contained a specific header text, then finding a like button within that <section>.

API

findAllNodes()

Finds all Host Instances (e.g. HTMLElement) within a host subtree that match the specified selector criteria.

function findAllNodes(
  hostRoot: Instance,
  selector: Array<Selector>
): Array<Instance>

How does it work?

  • This method starts searching at the specified root, which must be either:
    • A host parent node above the React container (e.g. document.body) in which case, React will traverse the native tree until it finds the first React container, or...
    • A React container, or...
    • A React-rendered Host Instance with a data-testname attribute (e.g. a match from an earlier findAllNodes query).
  • Starting from the root (or React container) React traverses the internal Fiber tree ¹ looking for all paths matching the specified selector.
    • While traversing the tree, each node will be compared to the current selector index.
      • If the criteria is satisfied, the selector index is advanced.
      • Otherwise the tree traversal continues.
    • Once all selector criteria have been satisfied, the remaining subtree is searched for the topmost Host Instances (might be several for Fragments).

¹ Traversing the Fiber tree (instead of the host tree) provides several benefits:

  • Easier to implement and share cross-renderer logic.
  • Portals (e.g. tooltips) can be matched even if they are in a different host tree.

What does it return?

  • An array of the topmost Host Instances (e.g. HTMLElements) that match the specified criteria. (This array will be empty if no matches are found.)

getFindAllNodesFailureDescription()

Returns an error string describing the matched and unmatched portions of the selector query.

function getFindAllNodesFailureDescription(
  hostRoot: Instance,
  selector: Array<Selector>
): string | null

How does it work?

  • This methods processes selectors in the same way as findAllNodes.

What does it return?

  • If the full selector query is able to be matched, this method returns null.
  • Otherwise, it returns a string describing how far it was able to match. For example:
findAllNodes was able to match part of the selector:
  <App> > <Navigation>

No matching component was found for:
  <UserProfiler> > [data-testname="edit"]

findBoundingRects()

For all React components within a host subtree that match the specified selector criteria, return a set of bounding boxes that covers the bounds of the nearest (shallowed) Host Instances within those trees.

function findBoundingRects(
  hostRoot: Instance,
  selector: Array<Selector>
): Array<Rect>

How does it work?

  • This methods processes selectors in the same way as findAllNodes.
  • Within each matching path:
    • Find the first Host Instances in the matched component. (Might be several for Fragments.)
    • For each Host Instance, call a getBoundingClientRect Host Config method. Add the result to a list of matched boxes.
    • Optionally merge adjacent boxes and exclude boxes that are fully covered by other boxes. (This ensure less reliance on implementation details like how many nodes make up the tree.)
  • Return the list of matches bounds.

What does it return?

  • An array of bounding boxes relative to the viewport. E.g. { x: number, y: number, width: number, height: number }
  • (This array will be empty if no matches are found.)

What can this API be used for?

  • Clicking or hovering over the center of a Component.
  • Take a snapshot of the Component for screen shot tests.
  • Assert on the maximum size a component is expected to take up.
  • Scrolling the Component into view.

observeVisibleRects()

For all React components within a host subtree that match the specified selector criteria, observe if it’s bounding rect is visible in the viewport and is not occluded.

function observeVisibleRects(
  hostRoot: Instance,
  selector: Array<Selector>,
  callback: (intersectingRects: Array<{ ratio: number, rect: Rect }>) => void,
  options: IntersectionObserverOptions,
): { disconnect(): void }

How does it work?

  • Create a new IntersectionObserver or equivalent HostConfig specific API, passing a callback and options argument to it.
    • Note: Host Config implementation needs to convert each entry into an object containing only { ratio, rect }, otherwise this callback works the same as the DOM version.
  • This methods processes selectors in the same way as findAllNodes.
  • Within each matching path:
    • Find the first Host Instances in the matched component. (Might be several for Fragments.)
    • For each Host Instance, call the observe(intersectionObserver, instance) Host Config method to add it to the list to be observed.
  • Attach an internal (for test builds only) callback to the React reconciler that is called every time React commits a new update.
    • When this callback is invoked, disconnect the Observer and start over from the top to attach a new one. This makes this observation live updating. This prevents accidentally relying on implementation details such as if a DOM node is reused or remounted.

What does it return?

  • An object containing a disconnect() method. When this method is called, we disconnect the IntersectionObserver and disconnect the callback in the React reconciler.

What can this API be used for?

  • Waiting for a Component to become visible before doing further operations.
  • Asserting that a Component is already visible.

Why is this API necessary?

  • Ideally findBoundingRects would be enough for this but these APIs don’t give access to where in the DOM this component is, nor really all the other things that might obscuring the bounding rect. This lets us solve this use case without exposing more implementation details and we can build in details such as if it’s obscured by something virtual (e.g. if an ART component is covered by another ART component within a canvas).

focusWithin()

For all React components within a host subtree that match the specified selector criteria, set focus within the first focusable Host Instance (as if you started before this component in the tree and moved focus forwards one step).

function focusWithin(
  hostRoot: Instance,
  selector: Array<Selector>
): boolean

The internal structure of a node is an implementation detail. However, you can start from the outside of a component and move forward (e.g. hit tab) to focus within a component. This has behavior that is defined, and so it can be thought of as part of the public API fo the component.

With the Focus Selectors API used for a11y we might be able to even expose more specific capabilities for the outside of a component move focus into a child.

How does it work?

  • This methods processes selectors in the same way as findAllNodes.
  • Within each matching path:
    • Find the first Host Instances in the matched component. (Might be several for Fragments.)
    • For each Host Instance:
      • Focus the Host Instance IIF it is considered Focusable by the Host Config.
      • Return true once we’ve found a focusable Host Instance.
  • If we didn’t find a focusable Host Instance, return false.

What does it return?

  • A boolean indicating if something focusable was found and focus was set on it.

What can this API be used for?

  • Setting focus on the canonical part of a Component such as text input.

Usage examples

Coming soon. (For now, refer to unit tests.)

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Apr 14, 2020
@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 14, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 6f2096e:

Sandbox Source
exciting-violet-msq48 Configuration

@sizebot
Copy link

sizebot commented Apr 14, 2020

Details of bundled changes.

Comparing: 7992ca1...6f2096e

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.1% +0.1% 638.57 KB 638.96 KB 137.05 KB 137.19 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +0.1% +0.1% 657.53 KB 657.92 KB 141.61 KB 141.75 KB RN_OSS_DEV
ReactFabric-prod.js 0.0% -0.0% 264.07 KB 264.07 KB 45.46 KB 45.46 KB RN_OSS_PROD

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.3% 1.16 KB 1.16 KB 660 B 658 B NODE_PROD
react-dom-server.browser.production.min.js 0.0% 0.0% 22.97 KB 22.97 KB 8.53 KB 8.54 KB UMD_PROD
ReactDOMForked-dev.js +0.1% +0.1% 1.01 MB 1.01 MB 232.46 KB 232.68 KB FB_WWW_DEV
ReactDOMForked-prod.js 0.0% -0.0% 429.5 KB 429.5 KB 77.26 KB 77.26 KB FB_WWW_PROD
react-dom.development.js +0.1% +0.1% 907.99 KB 908.66 KB 199.7 KB 199.93 KB UMD_DEV
react-dom.production.min.js 0.0% 0.0% 119.84 KB 119.84 KB 38.42 KB 38.42 KB UMD_PROD
react-dom.profiling.min.js 0.0% -0.0% 123.61 KB 123.61 KB 39.63 KB 39.62 KB UMD_PROFILING
ReactDOMTesting-dev.js +1.9% +2.1% 936.05 KB 954.21 KB 208.74 KB 213.23 KB FB_WWW_DEV
react-dom.development.js +0.1% +0.1% 864.29 KB 864.91 KB 197.17 KB 197.39 KB NODE_DEV
ReactDOMTesting-prod.js 🔺+3.6% 🔺+4.9% 390.83 KB 404.71 KB 71.18 KB 74.69 KB FB_WWW_PROD
react-dom.production.min.js 0.0% 0.0% 119.96 KB 119.96 KB 37.58 KB 37.58 KB NODE_PROD
react-dom-test-utils.development.js 0.0% 0.0% 75.28 KB 75.28 KB 20.18 KB 20.18 KB UMD_DEV
react-dom.profiling.min.js 0.0% 0.0% 123.85 KB 123.85 KB 38.8 KB 38.8 KB NODE_PROFILING
ReactDOM-dev.js +0.1% +0.1% 1.02 MB 1.02 MB 231.63 KB 231.84 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% 0.0% 70.11 KB 70.11 KB 19.67 KB 19.67 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% +0.1% 5.6 KB 5.6 KB 1.85 KB 1.86 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.2% 5.35 KB 5.35 KB 1.79 KB 1.8 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.19 KB 1.19 KB 698 B 697 B UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1 KB 1 KB 610 B 609 B NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.1% +0.1% 609.34 KB 609.73 KB 128.33 KB 128.47 KB FB_WWW_DEV
react-art.development.js +0.1% +0.1% 636.09 KB 636.51 KB 134.44 KB 134.58 KB UMD_DEV
react-art.production.min.js 0.0% -0.0% 107.53 KB 107.53 KB 32.63 KB 32.63 KB UMD_PROD
react-art.development.js +0.1% +0.1% 540.31 KB 540.7 KB 116.88 KB 117.01 KB NODE_DEV
react-art.production.min.js 0.0% -0.0% 72.53 KB 72.53 KB 21.8 KB 21.79 KB NODE_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactTestRenderer-dev.js +0.1% +0.1% 573.79 KB 574.17 KB 121.58 KB 121.75 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% 0.0% 39.16 KB 39.16 KB 9.57 KB 9.57 KB UMD_DEV
react-test-renderer.development.js +0.1% +0.1% 572.98 KB 573.39 KB 119.6 KB 119.77 KB UMD_DEV
react-test-renderer.development.js +0.1% +0.1% 546.31 KB 546.7 KB 118.22 KB 118.36 KB NODE_DEV
react-test-renderer.production.min.js 0.0% -0.0% 74.43 KB 74.43 KB 22.43 KB 22.43 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +2.3% +2.3% 581.44 KB 594.66 KB 123.4 KB 126.27 KB NODE_DEV
react-reconciler.production.min.js 🔺+4.9% 🔺+5.7% 77.74 KB 81.58 KB 23.08 KB 24.4 KB NODE_PROD

ReactDOM: size: 0.0%, gzip: 0.0%

Size changes (stable)

Generated by 🚫 dangerJS against 6f2096e

@sizebot
Copy link

sizebot commented Apr 14, 2020

Details of bundled changes.

Comparing: 7992ca1...6f2096e

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-test-utils.development.js 0.0% 0.0% 75.29 KB 75.29 KB 20.19 KB 20.19 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.2% 5.36 KB 5.36 KB 1.8 KB 1.8 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 13.18 KB 13.18 KB 4.9 KB 4.9 KB UMD_PROD
ReactDOMTesting-dev.js +2.0% +2.2% 910.26 KB 928.42 KB 203.2 KB 207.72 KB FB_WWW_DEV
react-dom.development.js +0.1% +0.1% 941.78 KB 942.45 KB 206.16 KB 206.38 KB UMD_DEV
react-dom.production.min.js 0.0% 0.0% 124.62 KB 124.62 KB 39.9 KB 39.9 KB UMD_PROD
ReactDOMForked-dev.js +0.1% +0.1% 1010.16 KB 1010.79 KB 226.77 KB 226.99 KB FB_WWW_DEV
react-dom.profiling.min.js 0.0% 0.0% 128.49 KB 128.49 KB 41.05 KB 41.05 KB UMD_PROFILING
react-dom.development.js +0.1% +0.1% 896.63 KB 897.25 KB 203.6 KB 203.82 KB NODE_DEV
react-dom-server.browser.production.min.js 0.0% 0.0% 23.32 KB 23.32 KB 8.6 KB 8.6 KB NODE_PROD
react-dom.profiling.min.js 0.0% 0.0% 128.82 KB 128.82 KB 40.29 KB 40.29 KB NODE_PROFILING
ReactDOM-dev.js +0.1% +0.1% 1014.03 KB 1014.65 KB 226.01 KB 226.22 KB FB_WWW_DEV
ReactDOMServer-dev.js 0.0% -0.0% 160.35 KB 160.35 KB 40.85 KB 40.85 KB FB_WWW_DEV
ReactDOM-profiling.js 0.0% -0.0% 433.23 KB 433.23 KB 76.44 KB 76.44 KB FB_WWW_PROFILING
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 5.61 KB 5.61 KB 1.87 KB 1.86 KB NODE_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.1% 1.17 KB 1.17 KB 668 B 667 B NODE_PROD
ReactDOMTesting-prod.js 🔺+3.7% 🔺+5.0% 378.97 KB 392.86 KB 69.19 KB 72.67 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% 0.0% 70.12 KB 70.12 KB 19.68 KB 19.68 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 4.87 KB 4.87 KB 1.7 KB 1.7 KB NODE_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.02 KB 1.02 KB 618 B 617 B NODE_PROD
react-dom-server.node.production.min.js 0.0% 0.0% 23.73 KB 23.73 KB 8.75 KB 8.75 KB NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% +0.1% 660.42 KB 660.83 KB 139.3 KB 139.45 KB UMD_DEV
react-art.production.min.js 0.0% 0.0% 110.9 KB 110.9 KB 33.62 KB 33.63 KB UMD_PROD
react-art.development.js +0.1% +0.1% 563.63 KB 564.02 KB 121.8 KB 121.93 KB NODE_DEV
ReactART-dev.js +0.1% +0.1% 599.33 KB 599.71 KB 126.29 KB 126.43 KB FB_WWW_DEV
ReactART-prod.js 0.0% -0.0% 242.39 KB 242.39 KB 41.37 KB 41.37 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer-shallow.development.js 0.0% 0.0% 39.17 KB 39.17 KB 9.58 KB 9.58 KB UMD_DEV
ReactTestRenderer-dev.js +0.1% +0.1% 573.8 KB 574.19 KB 121.58 KB 121.76 KB FB_WWW_DEV
react-test-renderer.development.js +0.1% +0.1% 573 KB 573.42 KB 119.61 KB 119.78 KB UMD_DEV
react-test-renderer.production.min.js 0.0% 0.0% 74.65 KB 74.65 KB 22.78 KB 22.79 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 546.34 KB 546.73 KB 118.23 KB 118.37 KB NODE_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +2.2% +2.3% 607.12 KB 620.34 KB 128.79 KB 131.7 KB NODE_DEV
react-reconciler.production.min.js 🔺+4.7% 🔺+5.7% 81.47 KB 85.31 KB 24.13 KB 25.5 KB NODE_PROD
react-reconciler-reflection.development.js 0.0% -0.0% 17.72 KB 17.72 KB 5.24 KB 5.24 KB NODE_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-profiling.js 0.0% -0.0% 275.54 KB 275.54 KB 47.68 KB 47.67 KB RN_OSS_PROFILING
ReactNativeRenderer-dev.js +0.1% +0.1% 661.98 KB 662.37 KB 142.29 KB 142.42 KB RN_FB_DEV
ReactFabric-dev.js +0.1% +0.1% 638.58 KB 638.97 KB 137.06 KB 137.2 KB RN_OSS_DEV
ReactFabric-dev.js +0.1% +0.1% 643.03 KB 643.42 KB 137.71 KB 137.86 KB RN_FB_DEV
ReactFabric-prod.js 0.0% -0.0% 264.05 KB 264.05 KB 45.46 KB 45.46 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.1% +0.1% 657.54 KB 657.93 KB 141.61 KB 141.75 KB RN_OSS_DEV

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (experimental)

Generated by 🚫 dangerJS against 6f2096e

@bvaughn bvaughn force-pushed the test-selector branch 3 times, most recently from da9b613 to d521316 Compare April 16, 2020 20:16
@bvaughn bvaughn marked this pull request as ready for review April 16, 2020 20:16
@bvaughn
Copy link
Contributor Author

bvaughn commented Apr 16, 2020

Okay, ready for a first pass of review and discussion I think.

packages/react-dom/src/client/ReactDOMHostConfig.js Outdated Show resolved Hide resolved

if (element.tabIndex === null || element.tabIndex < 0) {
// The HTML spec says that negative tab index values indicate an element should be,
// "click focusable but not sequentially focusable".
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the function name reflect that? Negative tab indices are also used to programmatically move focus.

@bvaughn bvaughn force-pushed the test-selector branch 3 times, most recently from 272b206 to f116626 Compare April 18, 2020 19:39
@bvaughn
Copy link
Contributor Author

bvaughn commented Apr 27, 2020

Pinging potential reviewers 😄

@bvaughn
Copy link
Contributor Author

bvaughn commented May 4, 2020

Ping @sebmarkbage / @gaearon / @acdlite again.

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a lot to review in this PR but overall I'm happy with things. We can always revise specific mechanics after we have some internal usage and get to use it.

@bvaughn
Copy link
Contributor Author

bvaughn commented May 4, 2020

Thanks a ton for the review and feedback, Dominic!

@bvaughn bvaughn merged commit 3cde22a into facebook:master May 5, 2020
@bvaughn bvaughn deleted the test-selector branch May 5, 2020 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants