# UI testing Test your Stripe app UI with a set of utilities and helpers. The Extension SDK includes a set of tools to write unit tests for your app’s user interface. We recommend running tests with [Jest](https://jestjs.io) and we include [Jest custom matchers](https://docs.stripe.com/stripe-apps/ui-testing.md#matchers) to help with writing assertions. ## Conceptual overview When testing your Stripe app’s UI, you’re testing a remote engine that renders your app, not the Document Object Model (DOM) tree directly. For security purposes, the React code in your Stripe app repository is serialized, sent through an extension loader using an iframe, and translated into a DOM tree within the Stripe Dashboard. The testing tools provided by the Extension SDK work with the remote rendering engine. ## Example This example tests a [Button](https://docs.stripe.com/stripe-apps/components/button.md) UI component that changes text when clicked. In the test, we render the button, confirm that the initial button text is correct, click the button, and confirm that the text of the button has changed. ```ts // App.tsx import {useState} from 'react'; import {ContextView, Button} from '@stripe/ui-extension-sdk/ui'; const App = () => { const [isPressed, setIsPressed] = useState(false); return ( ); }; export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; describe('App', () => { it('changes button text when pressed', async () => { const {wrapper, update} = render(); // Expect that the initial text is correct expect(wrapper.find(Button)).toContainText('Press me'); // Press the button wrapper.find(Button)!.trigger('onPress'); // This is needed if the "onPress" handler involves something asynchronous // like a promise or a React useEffect hook await update(); // Expect that the text changed expect(wrapper.find(Button)).toContainText('You pressed me!'); }); }); ``` ## Rendering a component ### `render(element: React.ReactElement)` The `render` method accepts a React element and returns an object with the following properties: - `wrapper`: The root element of the component passed to `render`. - `update`: A function that returns a promise that resolves after the JavaScript event stack has been cleared. This is useful when mocking APIs, dealing with promises, employing React hooks such as `useEffect`, or ensuring asynchronous rendering completes before running subsequent test cases. ```ts import {render} from '@stripe/ui-extension-sdk/testing'; import App from './App'; it('contains a Button', async () => { const {wrapper, update} = render(); await update(); // Continue testing... }); ``` ## Element properties and methods When working with the wrapper or any element within it, use the following properties and methods to assess state and interact with your app: ### children: Element[] Returns an array of the direct children of the element. ### descendants: Element[] Returns an array of all elements below the element in the tree. ### debug(options?: {all?: boolean, depth?: number, verbosity?: number}): string Returns a text representation of the element. You can modify `debug()` output using the `options` parameter. - `all` overrides the default props filtering behavior and instead includes all props in the output. `debug()` omits `className`, `aria-*`, and `data-*` props by default. - `depth` defines the number of children printed. All children are printed by default. - `verbosity` defines the level of expansion for non-scalar props. The default value of `1` expands objects one level deep. ### act(action: () => T): T Performs an action in the context of a React [act() block](https://reactjs.org/docs/test-utils.html#act). Normally, you can use `update()` (which uses `act()` internally) to handle asynchronous events. However, in some cases you might need to call `act()` directly, such as when your code uses timers (`setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`), and you want to test using [timer mocks](https://jestjs.io/docs/timer-mocks). When using timer mocks, you need to reset or cleanup mocks between tests (in jest this means calling `runOnlyPendingTimers()` and `useRealTimers()`), otherwise library code that uses timers won’t work properly. ### find(type: Type, props?: Partial>): Element> | null Finds a descendant element that matches `type`, where `type` is a component. If it doesn’t find a matching element, it returns null. If it finds a match, the returned element has the correct prop typing, which provides excellent type safety while navigating the React tree. If the second `props` argument is passed, it finds the first element of `type` with matching `props`. ```ts // App.tsx import {Button, ContextView} from '@stripe/ui-extension-sdk/ui'; const App = () => ( ); export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; it('contains a Button with text', () => { const {wrapper} = render(); const button = wrapper.find(Button, {href: 'http://example.com'}); expect(button).toContainText('Press me'); }); ``` Be aware that when using any of the `findX` methods, saved results are immediately stale and future updates to the component aren’t reflected. For example: ```ts // Bad - this will not work const button = wrapper.find(Button); expect(button).toContainText('Press me'); button!.trigger('onPress'); expect(button).toContainText('You pressed me!'); // button still contains 'Press me' // Good - this will work expect(wrapper.find(Button)).toContainText('Press me'); wrapper.find(Button)!.trigger('onPress'); expect(wrapper.find(Button)).toContainText('You pressed me!'); ``` ### findAll(type: Type, props?: Partial>): Element>[] Like `find`, but returns all matches as an array. ### findWhere(predicate: (element: Element) => boolean): Element> | null Finds the first descendant component matching the passed function. The function is called with each element from `descendants` until it finds a match. If it doesn’t find a match, it returns `null`. `findWhere` accepts an optional TypeScript argument that you can use to specify the type of the returned element. If you omit the generic argument, the returned element has unknown props, so calling `.props` and `.trigger` on it causes type errors, as those functions don’t know what props are valid on your element: ```ts // App.tsx import {Button, ContextView} from '@stripe/ui-extension-sdk/ui'; const App = () => ( ); export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; it('contains a Button with a href', () => { const {wrapper} = render(); const button = wrapper.findWhere( (node) => node.is(Button) && node.prop('href').startsWith('http://example'), ); expect(button).toContainText('Press me'); }); ``` ### findAllWhere(predicate: (element: Element) => boolean): Element>[] Like `findWhere`, but returns all matches as an array. ### is(type: Type): boolean Returns a boolean indicating whether the component type matches the passed type. This function also serves as a type guard, so subsequent calls to values like `props` are typed as the prop type of the passed component. ```ts import {Button} from '@stripe/ui-extension-sdk/ui'; // If we omit element.is here, we would not know whether 'href' was a valid prop and Typescript // would throw an error. if (element.is(Button) && element.prop('href') === 'http://example.com') { // ... } ``` ### prop(key: K): Props[K] Returns the current value of the passed prop name. ### props: Props All props of the element. ### text: string The text content of the element (that is, the string you would get by calling `textContent`). ### trigger>(prop: K, …args: Arguments>): ReturnType> Simulates a function prop being called on your component. This is usually the key to effective testing. After you mount your component, you simulate a change in a subcomponent and assert that the resulting tree is in the expected state. Optionally, each additional argument passed to `trigger` is passed to the function. This is useful for testing components in isolation. ```ts // App.tsx import {useState} from 'react'; import {ContextView, Button} from '@stripe/ui-extension-sdk/ui'; const App = () => { const [buttonText, setButtonText] = useState('Press me'); return ( ); }; export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; describe('App', () => { it('changes button text when pressed', () => { const {wrapper} = render(); expect(wrapper.find(Button)).toContainText('Press me'); // Press the button wrapper.find(Button)!.trigger('onPress', 'You pressed me!'); // Expect that the text changed expect(wrapper.find(Button)).toContainText('You pressed me!'); }); }); ``` ### triggerKeypath(keypath: string, …args: any[]): T Like `trigger()`, but allows you to provide a keypath referencing nested objects. Be aware that limitations in TypeScript prevent the same kind of type-safety that `trigger` guarantees. ```ts const App = ({action}: {action: {onAction(): void; label: string}}) => ( ); const spy = jest.fn(); const app = mount( , ); app.triggerKeypath('action.onAction'); expect(spy).toHaveBeenCalled(); ``` ## Matchers The Extension SDK provides [Jest custom matchers](https://jestjs.io/docs/using-matchers). These are imported automatically when you import `@stripe/ui-extension-sdk/testing`. ### toContainComponent(type: RemoteComponentType, props?: object) Asserts that at least one component matching `type` is in the descendants of the passed node. If the second `props` argument is passed, it further filters the matches by components whose props are equal to the passed object. Jest’s asymmetric matchers, like `expect.objectContaining`, are fully supported. ```ts // App.tsx import {Button, ContextView} from '@stripe/ui-extension-sdk/ui'; const App = () => ( ); export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; it('contains a Button', () => { const {wrapper} = render(); expect(wrapper).toContainComponent(Button, { onPress: expect.any(Function), }); }); ``` ### toContainComponentTimes(type: RemoteComponentType, times: number, props?: object) Identical to `.toContainComponent`, but asserts that there are exactly `times` matches within the passed node. ### toHaveProps(props: object) Checks whether the node has the specified props. ```ts // App.tsx import {Button, ContextView} from '@stripe/ui-extension-sdk/ui'; const App = () => ( ); export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; it('contains a Button with an onPress function', () => { const {wrapper} = render(); expect(wrapper.find(Button)).toHaveProps({ onPress: expect.any(Function), }); }); ``` ### toContainText(text: string) Checks that the rendered output of the component contains the passed string as text content (that is, the text is included in what you would get by calling `textContent` on all DOM nodes rendered by the component). ```ts // App.tsx import {Button, ContextView} from '@stripe/ui-extension-sdk/ui'; const App = () => ( ); export default App; // App.test.tsx import {render} from '@stripe/ui-extension-sdk/testing'; import {Button} from '@stripe/ui-extension-sdk/ui'; import App from './App'; it('contains a Button with an onPress function', () => { const {wrapper} = render(); expect(wrapper.find(Button)).toContainText('Press me'); }); ``` ## Mock context props App views are passed [context props](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md#props) in the Stripe Dashboard. You can generate a mock context props object for testing purposes using the `getMockContextProps` function. ```ts import {getMockContextProps} from '@stripe/ui-extension-sdk/testing'; const context = getMockContextProps(); const {wrapper} = render(); ``` By default, the mock context props are standard test values like `id: 'usr_1234'` and `email: 'user@example.com'`. You can override these values by passing in a partial object. The object you pass in is deep-merged with the default object, so you only need to pass in the values you want to override. ```ts import {getMockContextProps} from '@stripe/ui-extension-sdk/testing'; const context = getMockContextProps({ environment: { objectContext: { id: 'inv_1234', object: 'invoice', }, }, }); const {wrapper} = render(); ``` ## See also - [How UI extensions work](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md) - [UI extension SDK reference](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md) - [UI components](https://docs.stripe.com/stripe-apps/components.md)