From 99d3cfea9a55cc956982a8658e6bbd02e65cbb26 Mon Sep 17 00:00:00 2001 From: ramonjd Date: Tue, 9 Mar 2021 21:59:06 +1100 Subject: [PATCH] Added @testing-library/react-hooks library to unit test custom hooks Importing regenerator runtime to polyfill transpiled generator functions for react hooks testing library Added custom hook to fetch api data Added tests for custom hook and updated tests for edit.js --- .../jetpack/extensions/blocks/gif/edit.js | 49 ++----- .../blocks/gif/hooks/use-fetch-giphy-data.js | 57 ++++++++ .../extensions/blocks/gif/test/edit.js | 114 ++++++++++------ .../blocks/gif/test/use-fetch-giphy-data.js | 127 ++++++++++++++++++ projects/plugins/jetpack/package.json | 1 + .../plugins/jetpack/tests/jest-globals.js | 4 + projects/plugins/jetpack/yarn.lock | 30 +++++ 7 files changed, 300 insertions(+), 82 deletions(-) create mode 100644 projects/plugins/jetpack/extensions/blocks/gif/hooks/use-fetch-giphy-data.js create mode 100644 projects/plugins/jetpack/extensions/blocks/gif/test/use-fetch-giphy-data.js diff --git a/projects/plugins/jetpack/extensions/blocks/gif/edit.js b/projects/plugins/jetpack/extensions/blocks/gif/edit.js index 2a9b6b83e57e5..a879d948b2697 100644 --- a/projects/plugins/jetpack/extensions/blocks/gif/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/gif/edit.js @@ -3,7 +3,7 @@ */ import classNames from 'classnames'; import { __ } from '@wordpress/i18n'; -import { createRef, useState, useEffect, useCallback } from '@wordpress/element'; +import { createRef, useState, useEffect } from '@wordpress/element'; import { Placeholder } from '@wordpress/components'; import { RichText } from '@wordpress/block-editor'; @@ -14,6 +14,7 @@ import { icon, title } from './'; import { getUrl, getPaddingTop, getEmbedUrl } from './utils'; import SearchForm from './components/search-form'; import Controls from './controls'; +import useFetchGiphyData from './hooks/use-fetch-giphy-data'; function GifEdit( { attributes, @@ -24,8 +25,8 @@ function GifEdit( { const { align, caption, giphyUrl, searchText, paddingTop } = attributes; const classes = classNames( className, `align${ align }` ); const [ captionFocus, setCaptionFocus ] = useState( false ); - const [ results, setResults ] = useState( '' ); const searchFormInputRef = createRef(); + const { isFetching, giphyData, fetchGiphyData } = useFetchGiphyData(); const setSelectedGiphy = ( item ) => { setAttributes( { giphyUrl: getEmbedUrl( item ), paddingTop: getPaddingTop( item ) } ); @@ -36,50 +37,20 @@ function GifEdit( { setCaptionFocus( false ); }; - const fetchResults = async ( requestUrl ) => { - const giphyFetch = await fetch( requestUrl ) - .then( ( response ) => { - if ( response.ok ) { - return response; - } - return false; - } ) - .catch( () => { - return false; - } ); - - if ( giphyFetch ) { - const giphyResponse = await giphyFetch.json(); - // If there is only one result, Giphy's API does not return an array. - // The following statement normalizes the data into an array with one member in this case. - const giphyResults = typeof giphyResponse.data.images !== 'undefined' ? [ giphyResponse.data ] : giphyResponse.data; - - // Try to grab the first result. We're going to show this as the main image. - const giphyData = giphyResults[ 0 ]; - - // No results - if ( ! giphyData.images ) { - return false; - } - - setResults( giphyResults ); - } - }; - useEffect( () => { - if ( results && results[ 0 ] ) { - setSelectedGiphy( results[ 0 ] ); + if ( giphyData && giphyData[ 0 ] ) { + setSelectedGiphy( giphyData[ 0 ] ); } - }, [ results ] ); + }, [ giphyData ] ); const onSubmit = ( event ) => { event.preventDefault(); - if ( ! attributes.searchText ) { + if ( ! attributes.searchText || isFetching ) { return; } - fetchResults( getUrl( attributes.searchText ) ); + fetchGiphyData( getUrl( attributes.searchText ) ); }; const onChange = ( event ) => setAttributes( { searchText: event.target.value } ); @@ -111,9 +82,9 @@ function GifEdit( { ref={ searchFormInputRef } /> ) } - { isSelected && results && results.length > 1 && ( + { isSelected && giphyData && giphyData.length > 1 && (
- { results.map( thumbnail => { + { giphyData.map( thumbnail => { const thumbnailStyle = { backgroundImage: `url(${ thumbnail.images.downsized_still.url })`, }; diff --git a/projects/plugins/jetpack/extensions/blocks/gif/hooks/use-fetch-giphy-data.js b/projects/plugins/jetpack/extensions/blocks/gif/hooks/use-fetch-giphy-data.js new file mode 100644 index 0000000000000..4b8a2521f8995 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/gif/hooks/use-fetch-giphy-data.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ + +const useFetchGiphyData = ( initialValue = [] ) => { + const [ isFetching, setIsFetching ] = useState( false ); + const [ giphyData, setGiphyData ] = useState( initialValue ); + const [ fetchUrl, setFetchUrl ] = useState( '' ); + + useEffect( () => { + if ( ! fetchUrl ) { + return; + } + + const fetchResults = async () => { + setIsFetching( true ); + + const giphyFetch = await fetch( fetchUrl ) + .then( ( response ) => { + if ( response.ok ) { + return response; + } + return false; + } ) + .catch( () => { + return false; + } ); + + if ( giphyFetch ) { + const giphyResponse = await giphyFetch.json(); + // If there is only one result, Giphy's API does not return an array. + // The following statement normalizes the data into an array with one member in this case. + const giphyResults = typeof giphyResponse.data.images !== 'undefined' ? [ giphyResponse.data ] : giphyResponse.data; + + // Try to grab the first result. We're going to show this as the main image. + const firstResult = giphyResults[ 0 ]; + + // Check for results. + if ( firstResult.images ) { + setGiphyData( giphyResults ); + } + } + setIsFetching( false ); + }; + + fetchResults(); + }, [ fetchUrl ] ); + + return { isFetching, giphyData, fetchGiphyData: setFetchUrl }; +}; + +export default useFetchGiphyData; diff --git a/projects/plugins/jetpack/extensions/blocks/gif/test/edit.js b/projects/plugins/jetpack/extensions/blocks/gif/test/edit.js index 51410cd293ca3..0323795bf4988 100644 --- a/projects/plugins/jetpack/extensions/blocks/gif/test/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/gif/test/edit.js @@ -5,14 +5,15 @@ /** * External dependencies */ -import { render, act, fireEvent } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; /** * Internal dependencies */ import GifEdit from '../edit'; -import { getUrl } from '../utils'; +import { getUrl, getPaddingTop, getEmbedUrl } from '../utils'; +import useFetchGiphyData from '../hooks/use-fetch-giphy-data'; const setAttributes = jest.fn(); @@ -31,50 +32,54 @@ const defaultProps = { isSelected: false, }; -const originalFetch = window.fetch; - -/** - * Mock return value for a successful fetch JSON return value. - * - * @return {Promise} Mock return value. - */ -const GIPHY_RESPONSE = { - data: [ - { - id: '9', - embed_url: 'pony', - images: { - downsized_still: { - url: 'chips', - }, - original: { - height: 10, - width: 10, - }, - } +const GIPHY_DATA = [ + { + id: '9', + embed_url: 'pony', + images: { + downsized_still: { + url: 'chips', + }, + original: { + height: 10, + width: 10, + }, } - ] -}; -const RESOLVED_FETCH_PROMISE = Promise.resolve( GIPHY_RESPONSE ); -const DEFAULT_FETCH_MOCK_RETURN = Promise.resolve( { - status: 200, - ok: true, - json: () => RESOLVED_FETCH_PROMISE, -} ); + }, + { + id: '99', + embed_url: 'horsey', + images: { + downsized_still: { + url: 'fish', + }, + original: { + height: 12, + width: 12, + }, + } + } +]; + +const fetchGiphyData = jest.fn(); + +jest.mock('./../hooks/use-fetch-giphy-data' ); describe( 'GifEdit', () => { beforeEach( () => { - window.fetch = jest.fn(); - window.fetch.mockReturnValue( DEFAULT_FETCH_MOCK_RETURN ); + useFetchGiphyData.mockImplementation( () => { + return { + fetchGiphyData, + giphyData: [], + isFetching: false, + } + } ); } ); afterEach( async () => { - await act( () => GIPHY_RESPONSE ); + fetchGiphyData.mockReset(); setAttributes.mockReset(); - } ); - - afterAll( () => { - window.fetch = originalFetch; + useFetchGiphyData.mockReset(); } ); test( 'adds class names', () => { @@ -88,16 +93,39 @@ describe( 'GifEdit', () => { expect( container.querySelector( 'figure' ) ).not.toBeInTheDocument(); } ); -/* test( 'calls API and returns giphy images', () => { + test( 'calls API and returns giphy images', async () => { + useFetchGiphyData.mockImplementationOnce( () => { + return { + fetchGiphyData, + giphyData: GIPHY_DATA, + isFetching: false, + } + } ); const newProps = { ...defaultProps, + isSelected: true, attributes: { ...defaultAttributes, - searchText: 'sausage roll', + giphyUrl: 'https://itsalong.way/to/the/top/if/you/want', + searchText: 'a sausage roll', }, }; - const { container } = render( ); + const { container, screen } = render( ); + + expect( container.querySelector( 'form input' ).value ).toEqual( newProps.attributes.searchText ); + fireEvent.submit( container.querySelector( 'form' ) ); - expect( window.fetch ).toHaveBeenCalledWith( getUrl( newProps.attributes.searchText ) ); - } );*/ + + expect( fetchGiphyData ).toHaveBeenCalledWith( getUrl( newProps.attributes.searchText ) ); + expect( setAttributes.mock.calls[0][0] ).toStrictEqual( { + giphyUrl: getEmbedUrl( GIPHY_DATA[0] ), + paddingTop: getPaddingTop( GIPHY_DATA[0] ), + } ); + + expect( container.querySelector( 'figure' ) ).toBeInTheDocument(); + expect( container.querySelector( 'figcaption' ) ).toBeInTheDocument(); + expect( container.querySelector( '.wp-block-jetpack-gif-wrapper iframe' ) ).toBeInTheDocument(); + expect( container.querySelectorAll( '.wp-block-jetpack-gif_thumbnail-container' ) ).toHaveLength( 2 ); + + } ); } ); diff --git a/projects/plugins/jetpack/extensions/blocks/gif/test/use-fetch-giphy-data.js b/projects/plugins/jetpack/extensions/blocks/gif/test/use-fetch-giphy-data.js new file mode 100644 index 0000000000000..bfa919e4699cc --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/gif/test/use-fetch-giphy-data.js @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import { renderHook, act } from '@testing-library/react-hooks'; + +/** + * Internal dependencies + */ +import useFetchGiphyData from '../hooks/use-fetch-giphy-data'; + +const originalFetch = window.fetch; + +const GIPHY_SINGLE_RESPONSE = { + data: { + id: '9', + embed_url: 'pony', + images: { + downsized_still: { + url: 'chips', + }, + original: { + height: 10, + width: 10, + }, + } + }, +}; + +export const GIPHY_MULTIPLE_RESPONSE = { + data: [ + { + id: '9', + embed_url: 'pony', + images: { + downsized_still: { + url: 'chips', + }, + original: { + height: 10, + width: 10, + }, + } + }, + { + id: '99', + embed_url: 'horsey', + images: { + downsized_still: { + url: 'fish', + }, + original: { + height: 12, + width: 12, + }, + } + } + ] +}; + +/** + * Mock return value for a successful fetch JSON return value. + * + * @return {Promise} Mock return value. + */ +function getFetchMockReturnValue( resolvedFetchPromiseResponse ) { + const resolvedFetchPromise = Promise.resolve( resolvedFetchPromiseResponse ); + return Promise.resolve( { + ok: true, + json: () => resolvedFetchPromise, + } ); +} + +describe( 'useFetchGiphyData', () => { + beforeEach( () => { + window.fetch = jest.fn(); + window.fetch.mockReturnValue( getFetchMockReturnValue( GIPHY_SINGLE_RESPONSE ) ); + } ); + + afterAll( () => { + window.fetch = originalFetch; + } ); + + test( 'should return object response data after fetch', async () => { + const { result } = renderHook(() => + useFetchGiphyData() + ); + + await act( async () => { + result.current.fetchGiphyData( 'https://icantbelieve.its/not/butter' ); + } ); + + expect( result.current.giphyData ).toStrictEqual( [ GIPHY_SINGLE_RESPONSE.data ] ); + + expect( result.current.isFetching ).toBe( false ); + } ); + + test( 'should return array data after fetch', async () => { + window.fetch.mockReturnValueOnce( getFetchMockReturnValue( GIPHY_MULTIPLE_RESPONSE ) ) + + const { result } = renderHook(() => + useFetchGiphyData() + ); + + await act( async () => { + result.current.fetchGiphyData( 'https://icantbelieve.its/not/butter' ); + } ); + + expect( result.current.giphyData ).toStrictEqual( GIPHY_MULTIPLE_RESPONSE.data ); + + expect( result.current.isFetching ).toBe( false ); + } ); + + test( 'should not fetch if url is falsy', async () => { + const { result } = renderHook(() => + useFetchGiphyData() + ); + + await act( async () => { + result.current.fetchGiphyData( null ); + } ); + + expect( result.current.giphyData ).toStrictEqual( [] ); + + expect( result.current.isFetching ).toBe( false ); + } ); + +} ); diff --git a/projects/plugins/jetpack/package.json b/projects/plugins/jetpack/package.json index 590e539cd1641..acefe0151b7d3 100644 --- a/projects/plugins/jetpack/package.json +++ b/projects/plugins/jetpack/package.json @@ -160,6 +160,7 @@ "@testing-library/jest-dom": "5.11.9", "@testing-library/preact": "2.0.1", "@testing-library/react": "11.2.3", + "@testing-library/react-hooks": "4.0.0", "@testing-library/user-event": "12.8.1", "@wordpress/components": "9.2.6", "@wordpress/core-data": "2.12.3", diff --git a/projects/plugins/jetpack/tests/jest-globals.js b/projects/plugins/jetpack/tests/jest-globals.js index d4d296c92c86f..c3e9aa5732c03 100644 --- a/projects/plugins/jetpack/tests/jest-globals.js +++ b/projects/plugins/jetpack/tests/jest-globals.js @@ -1,3 +1,7 @@ +// Needed to use transpiled generator functions. +// See: https://babeljs.io/docs/en/babel-polyfill for details. +require( 'regenerator-runtime/runtime' ); + if ( ! window.matchMedia ) { Object.defineProperty( window, 'matchMedia', { writable: true, diff --git a/projects/plugins/jetpack/yarn.lock b/projects/plugins/jetpack/yarn.lock index 10b41bd19f394..00b3df4a25b30 100644 --- a/projects/plugins/jetpack/yarn.lock +++ b/projects/plugins/jetpack/yarn.lock @@ -3029,6 +3029,15 @@ dependencies: "@testing-library/dom" "^7.16.2" +"@testing-library/react-hooks@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-4.0.0.tgz#5bb4caa5814690cfc3e385ffaaf2ca4d54a8d08e" + integrity sha512-AWIR4M1Fz4dYzuKytkWtabcrwpevq7zj9dImuBOcmrpl3VkjOBDa7Q/62fwK/M30ae5XI25mDSpQ29vzC7A5Lw== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + "@testing-library/react@11.2.3": version "11.2.3" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.3.tgz#9971ede1c8465a231d7982eeca3c39fc362d5443" @@ -3193,6 +3202,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@>=16.9.0": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" + integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.9.0": version "16.9.51" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.51.tgz#f8aa51ffa9996f1387f63686696d9b59713d2b60" @@ -3201,6 +3217,20 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@>=16.9.0": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" + integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"