From 4e995590db4be26b2ef7d3fc35df59eee06ba4df Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Tue, 28 Aug 2018 14:43:26 -0400 Subject: [PATCH] Store: Add product API + state handlers (https://github.com/woocommerce/woocommerce-admin/pull/321) * Add product state * Add products store tests --- .../woocommerce-admin/client/store/index.js | 5 + .../client/store/products/actions.js | 18 +++ .../client/store/products/index.js | 15 +++ .../client/store/products/reducer.js | 43 ++++++++ .../client/store/products/resolvers.js | 19 ++++ .../client/store/products/selectors.js | 50 +++++++++ .../client/store/products/test/reducer.js | 96 ++++++++++++++++ .../client/store/products/test/resolvers.js | 38 +++++++ .../client/store/products/test/selectors.js | 104 ++++++++++++++++++ .../woocommerce-admin/client/store/util.js | 1 - 10 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/client/store/products/actions.js create mode 100644 plugins/woocommerce-admin/client/store/products/index.js create mode 100644 plugins/woocommerce-admin/client/store/products/reducer.js create mode 100644 plugins/woocommerce-admin/client/store/products/resolvers.js create mode 100644 plugins/woocommerce-admin/client/store/products/selectors.js create mode 100644 plugins/woocommerce-admin/client/store/products/test/reducer.js create mode 100644 plugins/woocommerce-admin/client/store/products/test/resolvers.js create mode 100644 plugins/woocommerce-admin/client/store/products/test/selectors.js diff --git a/plugins/woocommerce-admin/client/store/index.js b/plugins/woocommerce-admin/client/store/index.js index 71187fb11a5..72ae0708d8d 100644 --- a/plugins/woocommerce-admin/client/store/index.js +++ b/plugins/woocommerce-admin/client/store/index.js @@ -10,26 +10,31 @@ import { combineReducers } from 'redux'; */ import { applyMiddleware, addThunks } from './middleware'; import orders from 'store/orders'; +import products from 'store/products'; import reports from 'store/reports'; const store = registerStore( 'wc-admin', { reducer: combineReducers( { orders: orders.reducer, + products: products.reducer, reports: reports.reducer, } ), actions: { ...orders.actions, + ...products.actions, ...reports.actions, }, selectors: { ...orders.selectors, + ...products.selectors, ...reports.selectors, }, resolvers: { ...orders.resolvers, + ...products.resolvers, ...reports.resolvers, }, } ); diff --git a/plugins/woocommerce-admin/client/store/products/actions.js b/plugins/woocommerce-admin/client/store/products/actions.js new file mode 100644 index 00000000000..3af0b82d100 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/actions.js @@ -0,0 +1,18 @@ +/** @format */ + +export default { + setProducts( products, query ) { + return { + type: 'SET_PRODUCTS', + products, + query: query || {}, + }; + }, + + setProductsError( query ) { + return { + type: 'SET_PRODUCTS_ERROR', + query: query || {}, + }; + }, +}; diff --git a/plugins/woocommerce-admin/client/store/products/index.js b/plugins/woocommerce-admin/client/store/products/index.js new file mode 100644 index 00000000000..5d2b89160cc --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/index.js @@ -0,0 +1,15 @@ +/** @format */ +/** + * Internal dependencies + */ +import actions from './actions'; +import reducer from './reducer'; +import resolvers from './resolvers'; +import selectors from './selectors'; + +export default { + actions, + reducer, + resolvers, + selectors, +}; diff --git a/plugins/woocommerce-admin/client/store/products/reducer.js b/plugins/woocommerce-admin/client/store/products/reducer.js new file mode 100644 index 00000000000..1134e651232 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/reducer.js @@ -0,0 +1,43 @@ +/** @format */ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Internal dependencies + */ +import { ERROR } from 'store/constants'; +import { getJsonString } from 'store/util'; + +export const DEFAULT_STATE = { + queries: {}, +}; + +export default function productsReducer( state = DEFAULT_STATE, action ) { + if ( 'SET_PRODUCTS' === action.type ) { + const prevQueries = get( state, 'queries', {} ); + const queryKey = getJsonString( action.query ); + const queries = { + ...prevQueries, + [ queryKey ]: [ ...action.products ], + }; + return { + ...state, + queries, + }; + } + if ( 'SET_PRODUCTS_ERROR' === action.type ) { + const prevQueries = get( state, 'queries', {} ); + const queryKey = getJsonString( action.query ); + const queries = { + ...prevQueries, + [ queryKey ]: ERROR, + }; + return { + ...state, + queries, + }; + } + return state; +} diff --git a/plugins/woocommerce-admin/client/store/products/resolvers.js b/plugins/woocommerce-admin/client/store/products/resolvers.js new file mode 100644 index 00000000000..f5bbb806531 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/resolvers.js @@ -0,0 +1,19 @@ +/** @format */ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { dispatch } from '@wordpress/data'; +import { stringify } from 'qs'; + +export default { + async getProducts( state, query ) { + try { + const params = query ? '?' + stringify( query ) : ''; + const products = await apiFetch( { path: '/wc/v3/products' + params } ); + dispatch( 'wc-admin' ).setProducts( products, query ); + } catch ( error ) { + dispatch( 'wc-admin' ).setProductsError( query ); + } + }, +}; diff --git a/plugins/woocommerce-admin/client/store/products/selectors.js b/plugins/woocommerce-admin/client/store/products/selectors.js new file mode 100644 index 00000000000..51dd3aab551 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/selectors.js @@ -0,0 +1,50 @@ +/** @format */ + +/** + * External dependencies + */ +import { get } from 'lodash'; +import { select } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { ERROR } from 'store/constants'; +import { getJsonString } from 'store/util'; + +/** + * Returns revenue report details for a specific report query. + * + * @param {Object} state Current state + * @param {Object} query Report query paremters + * @return {Object} Report details + */ +function getProducts( state, query = {} ) { + const queries = get( state, 'products.queries', {} ); + return queries[ getJsonString( query ) ]; +} + +export default { + getProducts, + + /** + * Returns true if a products request is pending. + * + * @param {Object} state Current state + * @return {Object} True if the `getProducts` request is pending, false otherwise + */ + isProductsRequesting( state, ...args ) { + return select( 'core/data' ).isResolving( 'wc-admin', 'getProducts', args ); + }, + + /** + * Returns true if a products request has returned an error. + * + * @param {Object} state Current state + * @param {Object} query Report query paremters + * @return {Object} True if the `getProducts` request has failed, false otherwise + */ + isProductsError( state, query ) { + return ERROR === getProducts( state, query ); + }, +}; diff --git a/plugins/woocommerce-admin/client/store/products/test/reducer.js b/plugins/woocommerce-admin/client/store/products/test/reducer.js new file mode 100644 index 00000000000..043f2359fb9 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/test/reducer.js @@ -0,0 +1,96 @@ +/** + * @format + */ + +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { ERROR } from 'store/constants'; +import productsReducer, { DEFAULT_STATE } from '../reducer'; +import { getJsonString } from 'store/util'; + +describe( 'productsReducer', () => { + it( 'returns default state by default', () => { + const state = productsReducer( undefined, {} ); + expect( state ).toEqual( DEFAULT_STATE ); + } ); + + it( 'returns received product data', () => { + const originalState = deepFreeze( { ...DEFAULT_STATE } ); + const query = { + page: 3, + per_page: 5, + }; + const products = [ + { + id: 1, + name: 'my-product', + }, + ]; + const state = productsReducer( originalState, { + type: 'SET_PRODUCTS', + query, + products, + } ); + const queryKey = getJsonString( query ); + expect( state.queries[ queryKey ] ).toEqual( products ); + } ); + + it( 'returns received product data for multiple queries', () => { + const originalState = deepFreeze( { ...DEFAULT_STATE } ); + const query1 = { + page: 3, + per_page: 5, + }; + const products1 = [ + { + id: 1, + name: 'my-product', + }, + ]; + const intermediateState = productsReducer( originalState, { + type: 'SET_PRODUCTS', + query: query1, + products: products1, + } ); + const query2 = { + page: 6232, + per_page: 978, + }; + const products2 = [ + { + id: 2, + name: 'my-other-product', + }, + ]; + const finalState = productsReducer( intermediateState, { + type: 'SET_PRODUCTS', + query: query2, + products: products2, + } ); + + const queryKey1 = getJsonString( query1 ); + const queryKey2 = getJsonString( query2 ); + expect( finalState.queries[ queryKey1 ] ).toEqual( products1 ); + expect( finalState.queries[ queryKey2 ] ).toEqual( products2 ); + } ); + + it( 'returns error appropriately', () => { + const originalState = deepFreeze( { ...DEFAULT_STATE } ); + const query = { + page: 4, + per_page: 5, + }; + const state = productsReducer( originalState, { + type: 'SET_PRODUCTS_ERROR', + query, + } ); + const queryKey = getJsonString( query ); + expect( state.queries[ queryKey ] ).toEqual( ERROR ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/store/products/test/resolvers.js b/plugins/woocommerce-admin/client/store/products/test/resolvers.js new file mode 100644 index 00000000000..9d98d32a761 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/test/resolvers.js @@ -0,0 +1,38 @@ +/* + * @format + */ + +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import resolvers from '../resolvers'; + +const { getProducts } = resolvers; + +jest.mock( '@wordpress/api-fetch', () => jest.fn() ); + +describe( 'getProducts', () => { + const products = [ + { + id: 1, + name: 'my-product', + }, + ]; + + beforeAll( () => { + apiFetch.mockImplementation( options => { + if ( options.path === '/wc/v3/products?search=abc' ) { + return Promise.resolve( products ); + } + } ); + } ); + + it( 'returns requested products', async () => { + getProducts( {}, { search: 'abc' } ).then( data => expect( data ).toEqual( products ) ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/store/products/test/selectors.js b/plugins/woocommerce-admin/client/store/products/test/selectors.js new file mode 100644 index 00000000000..0a6ef06dc95 --- /dev/null +++ b/plugins/woocommerce-admin/client/store/products/test/selectors.js @@ -0,0 +1,104 @@ +/* + * @format + */ + +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { ERROR } from 'store/constants'; +import selectors from '../selectors'; +import { select } from '@wordpress/data'; +import { getJsonString } from 'store/util'; + +const { getProducts, isProductsRequesting, isProductsError } = selectors; +jest.mock( '@wordpress/data', () => ( { + ...require.requireActual( '@wordpress/data' ), + select: jest.fn().mockReturnValue( {} ), +} ) ); + +describe( 'getProducts', () => { + it( 'returns undefined when no query matches values in state', () => { + const state = deepFreeze( {} ); + expect( getProducts( state, { search: 'abc' } ) ).toEqual( undefined ); + } ); + + it( 'returns products for a given query', () => { + const products = [ + { + id: 1, + name: 'my-product', + }, + ]; + const query = { search: 'abc' }; + const queryKey = getJsonString( query ); + const state = deepFreeze( { + products: { + queries: { + [ queryKey ]: products, + }, + }, + } ); + expect( getProducts( state, query ) ).toEqual( products ); + } ); +} ); + +describe( 'isProductsRequesting', () => { + beforeAll( () => { + select( 'core/data' ).isResolving = jest.fn().mockReturnValue( false ); + } ); + + afterAll( () => { + select( 'core/data' ).isResolving.mockRestore(); + } ); + + const query = { search: 'abc' }; + + function setIsResolving( isResolving ) { + select( 'core/data' ).isResolving.mockImplementation( + ( reducerKey, selectorName ) => + isResolving && reducerKey === 'wc-admin' && selectorName === 'getProducts' + ); + } + + it( 'returns false if never requested', () => { + const result = isProductsRequesting( query ); + expect( result ).toBe( false ); + } ); + + it( 'returns false if request finished', () => { + setIsResolving( false ); + const result = isProductsRequesting( query ); + expect( result ).toBe( false ); + } ); + + it( 'returns true if requesting', () => { + setIsResolving( true ); + const result = isProductsRequesting( query ); + expect( result ).toBe( true ); + } ); +} ); + +describe( 'isProductsError', () => { + const query = { search: 'abc' }; + + it( 'returns false by default', () => { + const state = deepFreeze( {} ); + expect( isProductsError( state, query ) ).toEqual( false ); + } ); + it( 'returns true if ERROR constant is found', () => { + const queryKey = getJsonString( query ); + const state = deepFreeze( { + products: { + queries: { + [ queryKey ]: ERROR, + }, + }, + } ); + expect( isProductsError( state, query ) ).toEqual( true ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/store/util.js b/plugins/woocommerce-admin/client/store/util.js index f4105e296ee..b2830ab87a9 100644 --- a/plugins/woocommerce-admin/client/store/util.js +++ b/plugins/woocommerce-admin/client/store/util.js @@ -6,7 +6,6 @@ * @param {Object} query Current state * @return {String} Query Key */ - export function getJsonString( query = {} ) { return JSON.stringify( query, Object.keys( query ).sort() ); }