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