Test de l'interface utilisateur
Testez l'interface utilisateur de votre application avec un ensemble d'utilitaires et d'outils d'aide.
Le SDK Extension inclut un ensemble d’outils pour écrire des tests unitaires pour l’interface utilisateur de votre application. Nous recommandons d’exécuter les tests avec Jest et d’inclure les comparateurs Jest personnalisés pour faciliter l’écriture des assertions.
Présentation du concept
Lorsque vous testez l’interface utilisateur de votre application Stripe, pour le rendu, vous utilisez un moteur distant et non pas directement l’arbre Document Object Model (DOM).
Pour des questions de sécurité, le code React qui se trouve dans le dépôt de votre application Stripe est sérialisé, envoyé via un chargeur d’extension à l’aide d’un iframe et traduit en un arbre DOM dans le Dashboard Stripe. Les outils de test fournis par le SDK fonctionnent avec le moteur de rendu distant.
Exemple
Cet exemple teste un composant d’interface utilisateur de type bouton dont le texte change après un clic. Pour le test, nous affichons le bouton, vérifions que son texte initial est correct, cliquons dessus et vérifions que son texte a changé.
// 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!'); }); });
Rendu d’un composant
render(element: React. ReactElement)
La méthode render
accepte un élément React et renvoie un objet avec les propriétés suivantes :
wrapper
: l’élément racine du composant transmis àrender
.update
: fonction qui renvoie une promesse résolue une fois que la suite d’événements JavaScript est exécutée. Elle est utile pour la simulation d’API, pour le traitement de promesses, pour l’utilisation de hooks React tels queuseEffect
, ou encore pour s’assurer que l’affichage asynchrone soit terminé avant d’exécuter les cas de test ultérieurs.
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... });
Propriétés et méthodes d’élément
Lorsque vous travaillez avec le wrapper ou n’importe quel élément inclus dans celui-ci, utilisez les propriétés et méthodes suivantes pour déterminer l’état et interagir avec votre application :
children: Element<unknown>[]
Renvoie un tableau contenant les enfants directs de l’élément.
descendants: Element<unknown>[]
Renvoie un tableau contenant tous les éléments se trouvant sous l’élément dans l’arbre.
debug(options?: {all?: boolean, depth?: number, verbosity?: number}): string
Renvoie une représentation textuelle de l’élément. Vous pouvez modifier le résultat de debug()
à l’aide du paramètre options
.
all
remplace le comportement de filtrage des propriétés par défaut et inclut à la place toutes les propriétés dans le résultat. Par défaut, les propriétésclassName
,aria-*
etdata-*
sont omises pardebug()
.depth
définit le nombre d’enfants affichés. Par défaut, tous les enfants sont affichés.verbosity
définit le niveau d’expansion des propriétés non-scalaires. La valeur par défaut de1
étend les objets à une profondeur d’un niveau.
act<T>(action: () => T): T
Effectue une action dans le contexte d’un bloc act() React. En règle générale, vous pouvez utiliser update()
(qui utilise act()
en interne) pour gérer les événements asynchrones. Toutefois, il peut arriver que vous deviez appeler directement act()
. Cela peut être le cas lorsque votre code utilise un système de décompte (setTimeout
, setInterval
, clearTimeout
, clearInterval
) et que vous souhaitez le tester à l’aide de chronomètres fictifs. Lorsque vous utilisez ces chronomètres, vous devez les réinitialiser ou les nettoyer entre chaque test (dans jest, cela consiste à appeler runOnlyPendingTimers()
et useRealTimers()
), faute de quoi le code de la bibliothèque qui utilise des chronomètres ne pourra pas fonctionner correctement.
find(type: Type, props?: Partial<PropsForComponent<Type>>): Element<PropsForComponent<Type>> | null
Trouve un élément descendant qui correspond à type
, où type
est un composant. Si aucun élément correspondant n’est trouvé, null est renvoyé. Si une correspondance est trouvée, l’élément renvoyé prend le typage de propriété correct, ce qui est un excellent moyen de garantir la sûreté du typage lors de la navigation dans l’arbre React.
Si le deuxième argument props
est transmis, il trouve le premier élément type
avec les props
correspondantes.
// 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'); });
N’oubliez pas que lorsque vous utilisez l’une des méthodes findX
les résultats enregistrés sont immédiatement obsolètes, et les mises à jour ultérieures du composant ne sont pas prises en compte. Par exemple :
// 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>>[]
Comme find
, mais renvoie toutes les correspondances dans un tableau.
findWhere<Type = unknown>(predicate: (element: Element<unknown>) => boolean): Element<PropsForComponent<Type>> | null
Trouve le premier composant descendant correspondant à la fonction transmise. La fonction est appelée avec chaque élément depuis descendants
jusqu’à ce qu’une correspondance soit trouvée. Si aucune correspondance n’est trouvée, null
est renvoyé.
findWhere
accepte un argument TypeScript facultatif que vous pouvez utiliser pour spécifier le type de l’élément renvoyé. Si vous omettez l’argument générique, l’élément renvoyé a des propriétés inconnues. Par conséquent, appeler .
et .
sur cet élément génère des erreurs de type, puisque ces fonctions ne savent pas quelles propriétés sont valides sur votre élément :
// 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>>[]
Comme findWhere
, mais renvoie toutes les correspondances dans un tableau.
is(type: Type): boolean
Renvoie un booléen indiquant si le type du composant correspond au type transmis. Cette fonction sert également de garde de type afin que les appels suivants à des valeurs telles que props
soient du même type que la propriété du composant transmis.
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]
Renvoie la valeur actuelle du nom de propriété transmis.
props: Props
Toutes les propriétés de l’élément.
text: string
Le contenu textuel de l’élément (c’est-à-dire la chaîne que vous obtiendriez en appelant textContent
).
trigger<K extends FunctionKeys<Props>>(prop: K, …args: Arguments<Props<K>>): ReturnType<Props<K>>
Simule une propriété de fonction appelée sur votre composant. Généralement, c’est la clé d’un test efficace. Une fois que vous avez monté votre composant, vous simulez une modification dans un sous-composant et assertez que l’arbre qui en résulte est à l’état attendu.
De manière facultative, chaque argument supplémentaire transmis à trigger
est transmis à la fonction. Utile pour tester des composants isolés.
// 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
Semblable à trigger()
, mais vous permet de fournir un keypath référençant des objets imbriqués. Sachez qu’en raison de limitations propres à TypeScript, il n’est pas possible d’obtenir une sûreté de typage du même ordre que celle garantie par 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();
Comparateurs
Le SDK Extension fournit des comparateurs Jest personnalisés. Ils sont automatiquement importés lorsque vous importez @stripe/ui-extension-sdk/testing
.
toContainComponent(type: RemoteComponentType, props?: object)
Asserte qu’au moins un composant correspondant à type
figure dans les descendants du nœud transmis. Si le deuxième argument props
est transmis, filtre également les correspondances en fonction des composants dont les propriétés sont égales à l’objet transmis. Les comparateurs Jest asymétriques tels que expect.
sont entièrement pris en charge.
// 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)
Identique à .
, mais asserte qu’il y a exactement times
correspondance dans le nœud transmis.
toHaveProps(props: object)
Vérifie si le nœud contient les propriétés spécifiées.
// 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)
Vérifie que le résultat affiché du composant contient la chaîne transmise en tant que contenu textuel (autrement dit que le texte est inclus dans le résultat que vous obtiendriez en appelant textContent
sur tous les nœuds DOM affichés par le composant).
// 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'); });
Propriétés de contexte fictives
Les vues de l’application sont transmises en tant que propriétés de contexte dans le Dashboard Stripe. Vous pouvez générer un objet de propriétés de contexte fictif pour vos tests avec la fonction getMockContextProps
.
import {getMockContextProps} from '@stripe/ui-extension-sdk/testing'; const context = getMockContextProps(); const {wrapper} = render(<App {...context} />);
Par défaut, les propriétés de contexte fictives sont des valeurs de test standard comme id: 'usr_
et email: 'user@example.
. Vous pouvez remplacer ces valeurs en transmettant un objet partiel. L’objet que vous transmettez est fusionné avec l’objet par défaut (en fusion profonde). Ainsi, vous avez seulement besoin de transmettre les valeurs que vous souhaitez remplacer.
import {getMockContextProps} from '@stripe/ui-extension-sdk/testing'; const context = getMockContextProps({ environment: { objectContext: { id: 'inv_1234', object: 'invoice', }, }, }); const {wrapper} = render(<App {...context} />);