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"