# UI テスト 一連のユーティリティーとヘルパーを使用して Stripe アプリ UI をテストします。 Extension SDK には、アプリのユーザーインターフェイスのユニットテストを作成するための一連のツールが組み込まれています。[Jest](https://jestjs.io) を使用してテストを実行することをお勧めします。また、アサーションを作成できるように、[Jest カスタムマッチャー](https://docs.stripe.com/stripe-apps/ui-testing.md#matchers)が組み込まれています。 ## 概念の概要 Stripe アプリの UI をテストする場合、ドキュメントオブジェクトモデル (DOM) ツリーを直接テストするのではなく、アプリを表示するリモートエンジンをテストします。 セキュリティー上の理由から、Stripe アプリリポジトリーの React コードは、シリアライズされ、iframe を使用して拡張機能ローダーを介して送信され、Stripe ダッシュボード内で DOM ツリーに変換されます。Extension SDK で提供されているテストツールは、リモートレンダリングエンジンと連携します。 ## 例 この例では、クリックしたときにテキストが変わる[ボタン](https://docs.stripe.com/stripe-apps/components/button.md) UI コンポーネントをテストします。このテストでは、ボタンを表示して、ボタンの初期テキストが正しいことを確認し、ボタンをクリックして、ボタンのテキストが変わることを確認します。 ```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!'); }); }); ``` ## コンポーネントの表示 ### `render(element: React.ReactElement)` `render` メソッドは、React エレメントを受け入れ、以下のプロパティーが指定されたオブジェクトを返します。 - `wrapper`: `render` に渡されたコンポーネントのルートエレメント。 - `update`: JavaScript イベントスタックがクリアされた後で解決するプロミスを返す関数。これは、API の模擬実行、プロミスの処理、`useEffect` などの React フックの採用や、後続のテストケースを実行する前に非同期のレンダリングの完了を確認する場合に利用できます。 ```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... }); ``` ## エレメントのプロパティーとメソッド ラッパーまたはその中のエレメントを処理する際は、以下のプロパティーとメソッドを使用して、状態を評価し、アプリと対話します。 ### children: Element[] エレメントの直接の子の配列を返します。 ### descendants: Element[] ツリー内でエレメントの下位にあるすべてのエレメントの配列を返します。 ### debug(options?: {all?: boolean, depth?: number, verbosity?: number}): string エレメントのテキスト表記を返します。`debug()` 出力は、`options` パラメーターを使用して変更できます。 - `all` は、デフォルトのプロパティーのフィルタリング動作を上書きして、出力にすべてのプロパティーを組み込みます。`debug()` では、デフォルトの場合、`className`、`aria-*`、および `data-*` の各プロパティーが省略されます。 - `depth` は、出力される子の数を定義します。デフォルトでは、すべての子が出力されます。 - `verbosity` は、非スカラープロパティーの拡張のレベルを定義します。デフォルト値の `1` は、オブジェクトを 1 レベルの深さで拡張します。 ### act(action: () => T): T React [act() ブロック](https://reactjs.org/docs/test-utils.html#act)のコンテキストでアクションを実行します。通常は、(`act()` を内部的に使用する) `update()` を使用して非同期イベントを処理できます。ただし、コードでタイマー (`setTimeout`、`setInterval`、`clearTimeout`、`clearInterval`) を使用していて、[模擬のタイマー](https://jestjs.io/docs/timer-mocks)を使用する場合などには、`act()` を直接呼び出す必要があります。模擬のタイマーを使用する場合、テスト間で模擬をリセットまたはクリーンアップする必要があります (Jest では、`runOnlyPendingTimers()` と `useRealTimers()` を呼び出します)。そうしないと、タイマーを使用するライブラリコードは適切に機能しません。 ### find(type: Type, props?: Partial>): Element> | null `type` が一致する子孫エレメントを検出します。`type` はコンポーネントです。一致するエレメントが検出されない場合は、null が返されます。一致が検出された場合、返されるエレメントには正しいプロパティーがタイプ付けされます。これは、React ツリーをナビゲートする際のタイプの安全性の保護に非常に優れています。 2 番目の `props` 引数が渡される場合、`props` が一致する `type` の最初のエレメントを検出します。 ```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'); }); ``` `findX` メソッドのいずれかを使用すると、保存されている結果はすぐに無効になり、コンポーネントに対する以降の更新は反映されないことに注意してください。以下に例を示します。 ```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>[] `find` と似ていますが、すべての一致を配列として返します。 ### findWhere(predicate: (element: Element) => boolean): Element> | null 渡された関数と一致する最初の子孫コンポーネントを検出します。関数は、一致が見つかるまで、`descendants` の各エレメントで呼び出されます。一致が見つからない場合は、`null` が返されます。 `findWhere` は、返されるエレメントのタイプの指定に使用できる TypeScript 引数 (省略可能) を受け入れます。汎用の引数を省略すると、返されるエレメントのプロパティーは不明です。このため、`.props` と `.trigger` を呼び出すと、タイプエラーが発生しますが、これはそれらの関数がエレメントで有効なプロパティーを認識できないためです。 ```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>[] `findWhere` と似ていますが、すべての一致を配列として返します。 ### is(type: Type): boolean コンポーネントタイプが渡されたタイプと一致するかどうかを示すブール値を返します。この関数はタイプ保護としても機能するため、`props` などの値に対する後続の呼び出しは、渡されたコンポーネントのプロパティータイプとしてタイプ付けされます。 ```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] 渡されたプロパティー名の現在の値を返します。 ### props: Props エレメントのすべてのプロパティー。 ### text: string エレメントのテキストコンテンツ (`textContent` を呼び出すことで取得するストリング)。 ### trigger>(prop: K, …args: Arguments>): ReturnType> コンポーネントで呼び出される関数プロパティーをシミュレートします。これは通常、効果的にテストするために重要です。コンポーネントをマウントした後、サブコンポーネントの変更をシミュレートし、生成されるツリーが予期した状態であることをアサートします。 オプションで、`trigger` に渡された追加の各引数が関数に渡されます。これは、コンポーネントを個別にテストする場合に役立ちます。 ```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 `trigger()` と似ていますが、ネストされたオブジェクトを参照するキーパスを指定できます。TypeScript の制限事項のため、`trigger` が保証するのと同じ種類のタイプの安全性を得られないことに注意してください。 ```ts const App = ({action}: {action: {onAction(): void; label: string}}) => ( ); const spy = jest.fn(); const app = mount( , ); app.triggerKeypath('action.onAction'); expect(spy).toHaveBeenCalled(); ``` ## マッチャー Extension SDK は、[Jest カスタムマッチャー](https://jestjs.io/docs/using-matchers)を提供します。これは、`@stripe/ui-extension-sdk/testing` のインポート時に自動的にインポートされます。 ### toContainComponent(type: RemoteComponentType, props?: object) 渡されたノードの子孫に、`type` が一致するコンポーネントが 1 つ以上存在することをアサートします。2 番目の `props` 引数が渡される場合は、渡されたオブジェクトとプロパティーが等しいコンポーネントで一致をさらにフィルタリングします。`expect.objectContaining` などの Jest の非対称マッチャーは完全にサポートされています。 ```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) `.toContainComponent` と同じですが、渡されたノード内の `times` が正確に一致していることをアサートします。 ### toHaveProps(props: object) ノードに指定されたプロパティーがあるかどうかを検査します。 ```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) コンポーネントの表示用出力に、渡されたストリングがテキストコンテンツとして含まれていることを確認します (コンポーネントによって表示されるすべての DOM ノードで `textContent` を呼び出して取得する内容にテキストが含まれていること) 。 ```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'); }); ``` ## 模擬のコンテキストプロパティー アプリビューには Stripe ダッシュボードの[コンテキストプロパティー](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md#props)が渡されます。`getMockContextProps` 関数を使用すると、テスト用として模擬のコンテキストプロパティーオブジェクトを生成できます。 ```ts import {getMockContextProps} from '@stripe/ui-extension-sdk/testing'; const context = getMockContextProps(); const {wrapper} = render(); ``` デフォルトの場合、模擬のコンテキストプロパティーは、`id: 'usr_1234'` and `email: 'user@example.com'` のような標準のテスト値です。この値は、部分オブジェクトを渡すことで上書きできます。渡すオブジェクトはデフォルトオブジェクトと深くマージ (deepmerge) されるため、上書きする必要がある値を渡すだけで済みます。 ```ts import {getMockContextProps} from '@stripe/ui-extension-sdk/testing'; const context = getMockContextProps({ environment: { objectContext: { id: 'inv_1234', object: 'invoice', }, }, }); const {wrapper} = render(); ``` ## See also - [UI Extensions の仕組み](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md) - [UI 拡張機能 SDK リファレンス](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md) - [UI コンポーネント](https://docs.stripe.com/stripe-apps/components.md)