From 7fd46e4deb429804a0173ab1827ca3503e04d295 Mon Sep 17 00:00:00 2001 From: Andre Costa Date: Mon, 21 Aug 2023 16:18:46 +0100 Subject: [PATCH] feat(client|redux): expose closet endpoints This exposes user closet endpoints --- .../__snapshots__/index.test.ts.snap | 3 + .../exchanges/types/exchangeFilter.types.ts | 2 +- .../deleteUserClosetItem.fixtures.ts | 16 + .../getUserClosetItems.fixtures.ts | 17 + .../__fixtures__/getUserClosets.fixtures.ts | 17 + .../deleteUserClosetItem.test.ts.snap | 11 + .../getUserClosetItems.test.ts.snap | 11 + .../__snapshots__/getUserClosets.test.ts.snap | 11 + .../__tests__/deleteUserClosetItem.test.ts | 44 +++ .../__tests__/getUserClosetItems.test.ts | 42 +++ .../closets/__tests__/getUserClosets.test.ts | 39 +++ .../src/users/closets/deleteUserClosetItem.ts | 39 +++ .../src/users/closets/getUserClosetItems.ts | 34 ++ .../src/users/closets/getUserClosets.ts | 22 ++ packages/client/src/users/closets/index.ts | 4 + .../src/users/closets/types/closet.types.ts | 31 ++ .../closets/types/closetSummary.types.ts | 5 + .../types/deleteUserClosetItem.types.ts | 11 + .../closets/types/getUserClosetItems.types.ts | 39 +++ .../closets/types/getUserClosets.types.ts | 7 + .../client/src/users/closets/types/index.ts | 5 + packages/client/src/users/index.ts | 5 +- .../__snapshots__/index.test.ts.snap | 27 ++ .../createExchangeFilter.test.ts.snap | 8 +- packages/redux/src/users/actionTypes.ts | 2 + packages/redux/src/users/actions.ts | 3 +- .../users/closets/__tests__/reducer.test.ts | 291 ++++++++++++++++++ .../users/closets/__tests__/selectors.test.ts | 135 ++++++++ .../redux/src/users/closets/actionTypes.ts | 54 ++++ .../fetchUserClosets.test.ts.snap | 14 + .../removeUserClosetItem.test.ts.snap | 7 + .../__tests__/fetchUserClosetItems.test.ts | 156 ++++++++++ .../__tests__/fetchUserClosets.test.ts | 76 +++++ .../__tests__/removeUserClosetItem.test.ts | 95 ++++++ .../__tests__/resetUserClosetItems.test.ts | 23 ++ .../__tests__/resetUserClosets.test.ts | 23 ++ .../factories/fetchUserClosetItemsFactory.ts | 77 +++++ .../factories/fetchUserClosetsFactory.ts | 50 +++ .../users/closets/actions/factories/index.ts | 3 + .../factories/removeUserClosetItemFactory.ts | 58 ++++ .../closets/actions/fetchUserClosetItems.ts | 7 + .../users/closets/actions/fetchUserClosets.ts | 7 + .../redux/src/users/closets/actions/index.ts | 7 + .../closets/actions/removeUserClosetItem.ts | 7 + .../closets/actions/resetUserClosetItems.ts | 18 ++ .../users/closets/actions/resetUserClosets.ts | 17 + packages/redux/src/users/closets/reducer.ts | 96 ++++++ packages/redux/src/users/closets/selectors.ts | 102 ++++++ .../src/users/closets/types/actions.types.ts | 87 ++++++ .../redux/src/users/closets/types/index.ts | 2 + .../src/users/closets/types/state.types.ts | 19 ++ packages/redux/src/users/reducer.ts | 7 + packages/redux/src/users/selectors.ts | 1 + packages/redux/src/users/types/state.types.ts | 2 + .../checkout/checkout.fixtures.mts | 10 + .../exchanges/exchanges.fixtures.mts | 8 +- tests/__fixtures__/users/index.mts | 1 + .../users/userCloset.fixtures.mts | 278 +++++++++++++++++ tests/__fixtures__/users/users.fixtures.mts | 10 + 59 files changed, 2191 insertions(+), 12 deletions(-) create mode 100644 packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts create mode 100644 packages/client/src/users/closets/__fixtures__/getUserClosetItems.fixtures.ts create mode 100644 packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts create mode 100644 packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap create mode 100644 packages/client/src/users/closets/__tests__/__snapshots__/getUserClosetItems.test.ts.snap create mode 100644 packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap create mode 100644 packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts create mode 100644 packages/client/src/users/closets/__tests__/getUserClosetItems.test.ts create mode 100644 packages/client/src/users/closets/__tests__/getUserClosets.test.ts create mode 100644 packages/client/src/users/closets/deleteUserClosetItem.ts create mode 100644 packages/client/src/users/closets/getUserClosetItems.ts create mode 100644 packages/client/src/users/closets/getUserClosets.ts create mode 100644 packages/client/src/users/closets/index.ts create mode 100644 packages/client/src/users/closets/types/closet.types.ts create mode 100644 packages/client/src/users/closets/types/closetSummary.types.ts create mode 100644 packages/client/src/users/closets/types/deleteUserClosetItem.types.ts create mode 100644 packages/client/src/users/closets/types/getUserClosetItems.types.ts create mode 100644 packages/client/src/users/closets/types/getUserClosets.types.ts create mode 100644 packages/client/src/users/closets/types/index.ts create mode 100644 packages/redux/src/users/closets/__tests__/reducer.test.ts create mode 100644 packages/redux/src/users/closets/__tests__/selectors.test.ts create mode 100644 packages/redux/src/users/closets/actionTypes.ts create mode 100644 packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap create mode 100644 packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap create mode 100644 packages/redux/src/users/closets/actions/__tests__/fetchUserClosetItems.test.ts create mode 100644 packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts create mode 100644 packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts create mode 100644 packages/redux/src/users/closets/actions/__tests__/resetUserClosetItems.test.ts create mode 100644 packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts create mode 100644 packages/redux/src/users/closets/actions/factories/fetchUserClosetItemsFactory.ts create mode 100644 packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts create mode 100644 packages/redux/src/users/closets/actions/factories/index.ts create mode 100644 packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts create mode 100644 packages/redux/src/users/closets/actions/fetchUserClosetItems.ts create mode 100644 packages/redux/src/users/closets/actions/fetchUserClosets.ts create mode 100644 packages/redux/src/users/closets/actions/index.ts create mode 100644 packages/redux/src/users/closets/actions/removeUserClosetItem.ts create mode 100644 packages/redux/src/users/closets/actions/resetUserClosetItems.ts create mode 100644 packages/redux/src/users/closets/actions/resetUserClosets.ts create mode 100644 packages/redux/src/users/closets/reducer.ts create mode 100644 packages/redux/src/users/closets/selectors.ts create mode 100644 packages/redux/src/users/closets/types/actions.types.ts create mode 100644 packages/redux/src/users/closets/types/index.ts create mode 100644 packages/redux/src/users/closets/types/state.types.ts create mode 100644 tests/__fixtures__/users/userCloset.fixtures.mts diff --git a/packages/client/src/__tests__/__snapshots__/index.test.ts.snap b/packages/client/src/__tests__/__snapshots__/index.test.ts.snap index 4789765ea..25b1af1df 100644 --- a/packages/client/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/client/src/__tests__/__snapshots__/index.test.ts.snap @@ -1033,6 +1033,7 @@ Object { "deleteToken": [Function], "deleteUserAddress": [Function], "deleteUserAttribute": [Function], + "deleteUserClosetItem": [Function], "deleteUserContact": [Function], "deleteUserDefaultContactAddress": [Function], "deleteUserExternalLogin": [Function], @@ -1146,6 +1147,8 @@ Object { "getUserAttribute": [Function], "getUserAttributes": [Function], "getUserBenefits": [Function], + "getUserClosetItems": [Function], + "getUserClosets": [Function], "getUserContact": [Function], "getUserContacts": [Function], "getUserCreditBalance": [Function], diff --git a/packages/client/src/exchanges/types/exchangeFilter.types.ts b/packages/client/src/exchanges/types/exchangeFilter.types.ts index 273b1b06d..44079d7e5 100644 --- a/packages/client/src/exchanges/types/exchangeFilter.types.ts +++ b/packages/client/src/exchanges/types/exchangeFilter.types.ts @@ -31,7 +31,7 @@ export type ExchangeFilterCondition = ExchangeFilterLogicOperatorData; export type ExchangeFilterLogicOperatorData = { criteria: ExchangeFilterLogicOperatorCriteria; comparator: ExchangeFilterLogicOperatorComparator; - value: string | string[]; + values: string | string[]; }; export enum ExchangeFilterLogicOperatorCriteria { diff --git a/packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts b/packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts new file mode 100644 index 000000000..2d812a84e --- /dev/null +++ b/packages/client/src/users/closets/__fixtures__/deleteUserClosetItem.fixtures.ts @@ -0,0 +1,16 @@ +import { rest, type RestHandler } from 'msw'; + +const path = '/api/account/v1/users/:userId/closets/:closetId/items/:itemId'; + +const fixtures = { + success: (response: number): RestHandler => + rest.delete(path, (_req, res, ctx) => + res(ctx.status(200), ctx.json(response)), + ), + failure: (): RestHandler => + rest.delete(path, (_req, res, ctx) => + res(ctx.status(404), ctx.json({ message: 'stub error' })), + ), +}; + +export default fixtures; diff --git a/packages/client/src/users/closets/__fixtures__/getUserClosetItems.fixtures.ts b/packages/client/src/users/closets/__fixtures__/getUserClosetItems.fixtures.ts new file mode 100644 index 000000000..0800362ec --- /dev/null +++ b/packages/client/src/users/closets/__fixtures__/getUserClosetItems.fixtures.ts @@ -0,0 +1,17 @@ +import { rest, type RestHandler } from 'msw'; +import type { Closet } from '../index.js'; + +const path = '/api/account/v1/users/:userId/closets/:closetId/items'; + +const fixtures = { + success: (response: Closet): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(200), ctx.json(response)), + ), + failure: (): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(404), ctx.json({ message: 'stub error' })), + ), +}; + +export default fixtures; diff --git a/packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts b/packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts new file mode 100644 index 000000000..8297c26ac --- /dev/null +++ b/packages/client/src/users/closets/__fixtures__/getUserClosets.fixtures.ts @@ -0,0 +1,17 @@ +import { rest, type RestHandler } from 'msw'; +import type { ClosetSummary } from '../index.js'; + +const path = '/api/account/v1/users/:userId/closets'; + +const fixtures = { + success: (response: ClosetSummary[]): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(200), ctx.json(response)), + ), + failure: (): RestHandler => + rest.get(path, (_req, res, ctx) => + res(ctx.status(404), ctx.json({ message: 'stub error' })), + ), +}; + +export default fixtures; diff --git a/packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap b/packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap new file mode 100644 index 000000000..77ff8507b --- /dev/null +++ b/packages/client/src/users/closets/__tests__/__snapshots__/deleteUserClosetItem.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deleteUserClosetItem should receive a client request error 1`] = ` +Object { + "code": "-1", + "message": "stub error", + "name": "AxiosError", + "status": 404, + "transportLayerErrorCode": "ERR_BAD_REQUEST", +} +`; diff --git a/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosetItems.test.ts.snap b/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosetItems.test.ts.snap new file mode 100644 index 000000000..d0a6e3cee --- /dev/null +++ b/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosetItems.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getUserClosetItems should receive a client request error 1`] = ` +Object { + "code": "-1", + "message": "stub error", + "name": "AxiosError", + "status": 404, + "transportLayerErrorCode": "ERR_BAD_REQUEST", +} +`; diff --git a/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap b/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap new file mode 100644 index 000000000..191a63afd --- /dev/null +++ b/packages/client/src/users/closets/__tests__/__snapshots__/getUserClosets.test.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getUserClosets should receive a client request error 1`] = ` +Object { + "code": "-1", + "message": "stub error", + "name": "AxiosError", + "status": 404, + "transportLayerErrorCode": "ERR_BAD_REQUEST", +} +`; diff --git a/packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts b/packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts new file mode 100644 index 000000000..ba1ffb314 --- /dev/null +++ b/packages/client/src/users/closets/__tests__/deleteUserClosetItem.test.ts @@ -0,0 +1,44 @@ +import { + closetId, + closetItemId, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { deleteUserClosetItem } from '../index.js'; +import client from '../../../helpers/client/index.js'; +import fixtures from '../__fixtures__/deleteUserClosetItem.fixtures.js'; +import mswServer from '../../../../tests/mswServer.js'; + +describe('deleteUserClosetItem', () => { + const expectedConfig = undefined; + const spy = jest.spyOn(client, 'delete'); + + beforeEach(() => jest.clearAllMocks()); + + it('should handle a client request successfully', async () => { + const response = 200; + + mswServer.use(fixtures.success(response)); + + await expect( + deleteUserClosetItem(userId, closetId, closetItemId), + ).resolves.toBe(response); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}/items/${closetItemId}`, + expectedConfig, + ); + }); + + it('should receive a client request error', async () => { + mswServer.use(fixtures.failure()); + + await expect( + deleteUserClosetItem(userId, closetId, closetItemId), + ).rejects.toMatchSnapshot(); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}/items/${closetItemId}`, + expectedConfig, + ); + }); +}); diff --git a/packages/client/src/users/closets/__tests__/getUserClosetItems.test.ts b/packages/client/src/users/closets/__tests__/getUserClosetItems.test.ts new file mode 100644 index 000000000..23e28fd75 --- /dev/null +++ b/packages/client/src/users/closets/__tests__/getUserClosetItems.test.ts @@ -0,0 +1,42 @@ +import { + closetId, + mockGetUserClosetItemsResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { getUserClosetItems } from '../index.js'; +import client from '../../../helpers/client/index.js'; +import fixtures from '../__fixtures__/getUserClosetItems.fixtures.js'; +import mswServer from '../../../../tests/mswServer.js'; + +describe('getUserClosetItems', () => { + const expectedConfig = undefined; + const spy = jest.spyOn(client, 'get'); + + beforeEach(() => jest.clearAllMocks()); + + it('should handle a client request successfully', async () => { + mswServer.use(fixtures.success(mockGetUserClosetItemsResponse)); + + await expect(getUserClosetItems(userId, closetId)).resolves.toStrictEqual( + mockGetUserClosetItemsResponse, + ); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}/items`, + expectedConfig, + ); + }); + + it('should receive a client request error', async () => { + mswServer.use(fixtures.failure()); + + await expect( + getUserClosetItems(userId, closetId), + ).rejects.toMatchSnapshot(); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets/${closetId}/items`, + expectedConfig, + ); + }); +}); diff --git a/packages/client/src/users/closets/__tests__/getUserClosets.test.ts b/packages/client/src/users/closets/__tests__/getUserClosets.test.ts new file mode 100644 index 000000000..fef90a59a --- /dev/null +++ b/packages/client/src/users/closets/__tests__/getUserClosets.test.ts @@ -0,0 +1,39 @@ +import { getUserClosets } from '../index.js'; +import { + mockGetUserClosetsResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import client from '../../../helpers/client/index.js'; +import fixtures from '../__fixtures__/getUserClosets.fixtures.js'; +import mswServer from '../../../../tests/mswServer.js'; + +describe('getUserClosets', () => { + const expectedConfig = undefined; + const spy = jest.spyOn(client, 'get'); + + beforeEach(() => jest.clearAllMocks()); + + it('should handle a client request successfully', async () => { + mswServer.use(fixtures.success(mockGetUserClosetsResponse)); + + await expect(getUserClosets(userId)).resolves.toStrictEqual( + mockGetUserClosetsResponse, + ); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets`, + expectedConfig, + ); + }); + + it('should receive a client request error', async () => { + mswServer.use(fixtures.failure()); + + await expect(getUserClosets(userId)).rejects.toMatchSnapshot(); + + expect(spy).toHaveBeenCalledWith( + `/account/v1/users/${userId}/closets`, + expectedConfig, + ); + }); +}); diff --git a/packages/client/src/users/closets/deleteUserClosetItem.ts b/packages/client/src/users/closets/deleteUserClosetItem.ts new file mode 100644 index 000000000..33fcd3ab3 --- /dev/null +++ b/packages/client/src/users/closets/deleteUserClosetItem.ts @@ -0,0 +1,39 @@ +import { adaptError } from '../../helpers/client/formatError.js'; +import client from '../../helpers/client/index.js'; +import join from 'proper-url-join'; +import type { DeleteUserClosetItem } from './types/index.js'; + +/** + * Method responsible for deleting a user closet. + * + * @param userId - Universal identifier of the user. + * @param closetId - Closet identifier. + * @param itemId - Closet item identifier. + * @param config - Custom configurations to send to the client instance (axios). + * + * @returns Promise that will resolve when the call to the endpoint finishes. + */ +const deleteUserClosetItem: DeleteUserClosetItem = ( + userId, + closetId, + itemId, + config, +) => + client + .delete( + join( + '/account/v1/users', + userId, + '/closets/', + closetId, + '/items/', + itemId, + ), + config, + ) + .then(response => response.status) + .catch(error => { + throw adaptError(error); + }); + +export default deleteUserClosetItem; diff --git a/packages/client/src/users/closets/getUserClosetItems.ts b/packages/client/src/users/closets/getUserClosetItems.ts new file mode 100644 index 000000000..e7dbb021b --- /dev/null +++ b/packages/client/src/users/closets/getUserClosetItems.ts @@ -0,0 +1,34 @@ +import { adaptError } from '../../helpers/client/formatError.js'; +import client from '../../helpers/client/index.js'; +import join from 'proper-url-join'; +import type { GetUserClosetItems } from './types/index.js'; + +/** + * Method responsible for getting the items (paginated) from a specific closet. + * + * @param userId - Universal identifier of the user. + * @param closetId - Closet identifier. + * @param query - Query params. + * @param config - Custom configurations to send to the client instance (axios). + * + * @returns Promise that will resolve when the call to the endpoint finishes. + */ +const getUserClosetItems: GetUserClosetItems = ( + userId, + closetId, + query, + config, +) => + client + .get( + join('/account/v1/users/', userId, '/closets/', closetId, '/items', { + query, + }), + config, + ) + .then(response => response.data) + .catch(error => { + throw adaptError(error); + }); + +export default getUserClosetItems; diff --git a/packages/client/src/users/closets/getUserClosets.ts b/packages/client/src/users/closets/getUserClosets.ts new file mode 100644 index 000000000..026b23a50 --- /dev/null +++ b/packages/client/src/users/closets/getUserClosets.ts @@ -0,0 +1,22 @@ +import { adaptError } from '../../helpers/client/formatError.js'; +import client from '../../helpers/client/index.js'; +import join from 'proper-url-join'; +import type { GetUserClosets } from './types/index.js'; + +/** + * Method responsible for getting user closets. + * + * @param userId - Universal identifier of the user. + * @param config - Custom configurations to send to the client instance (axios). + * + * @returns Promise that will resolve when the call to the endpoint finishes. + */ +const getUserClosets: GetUserClosets = (userId, config) => + client + .get(join('/account/v1/users/', userId, '/closets'), config) + .then(response => response.data) + .catch(error => { + throw adaptError(error); + }); + +export default getUserClosets; diff --git a/packages/client/src/users/closets/index.ts b/packages/client/src/users/closets/index.ts new file mode 100644 index 000000000..c44498a38 --- /dev/null +++ b/packages/client/src/users/closets/index.ts @@ -0,0 +1,4 @@ +export { default as deleteUserClosetItem } from './deleteUserClosetItem.js'; +export { default as getUserClosetItems } from './getUserClosetItems.js'; +export { default as getUserClosets } from './getUserClosets.js'; +export * from './types/index.js'; diff --git a/packages/client/src/users/closets/types/closet.types.ts b/packages/client/src/users/closets/types/closet.types.ts new file mode 100644 index 000000000..50237185a --- /dev/null +++ b/packages/client/src/users/closets/types/closet.types.ts @@ -0,0 +1,31 @@ +import type { Brand } from '../../../brands/index.js'; +import type { + Color, + PagedResponse, + ProductImageGroup, + ProductVariantAttribute, +} from '../../../types/index.js'; +import type { Order } from '../../../orders/index.js'; +import type { Price, Product } from '../../../products/index.js'; +import type { ProductCategory } from '../../../categories/index.js'; + +export type Closet = PagedResponse; + +export type ClosetItem = { + id: string; + orderId: Order['id']; + merchantId: number; + productId: Product['result']['id']; + productName: string; + productDescription: string; + attributes: ProductVariantAttribute[]; + images: ProductImageGroup; + categories: ProductCategory[]; + colors: Color[]; + price: Price; + purchasePrice: Price; + brand: Brand; + customAttributes: string; + isAvailable: boolean; + createdDate: string; +}; diff --git a/packages/client/src/users/closets/types/closetSummary.types.ts b/packages/client/src/users/closets/types/closetSummary.types.ts new file mode 100644 index 000000000..6d4fa3ab1 --- /dev/null +++ b/packages/client/src/users/closets/types/closetSummary.types.ts @@ -0,0 +1,5 @@ +export type ClosetSummary = { + id: string; + createdDate: string; + totalItems: number; +}; diff --git a/packages/client/src/users/closets/types/deleteUserClosetItem.types.ts b/packages/client/src/users/closets/types/deleteUserClosetItem.types.ts new file mode 100644 index 000000000..17058c64e --- /dev/null +++ b/packages/client/src/users/closets/types/deleteUserClosetItem.types.ts @@ -0,0 +1,11 @@ +import type { ClosetItem } from './closet.types.js'; +import type { ClosetSummary } from './closetSummary.types.js'; +import type { Config } from '../../../types/index.js'; +import type { User } from '../../authentication/types/user.types.js'; + +export type DeleteUserClosetItem = ( + userId: User['id'], + closetId: ClosetSummary['id'], + itemId: ClosetItem['id'], + config?: Config, +) => Promise; diff --git a/packages/client/src/users/closets/types/getUserClosetItems.types.ts b/packages/client/src/users/closets/types/getUserClosetItems.types.ts new file mode 100644 index 000000000..ea87d10c6 --- /dev/null +++ b/packages/client/src/users/closets/types/getUserClosetItems.types.ts @@ -0,0 +1,39 @@ +import type { Closet } from './closet.types.js'; +import type { ClosetSummary } from './closetSummary.types.js'; +import type { Config, User } from '../../../index.js'; + +export type GetUserClosetItemsQuerySort = + | 'createdDate' + | 'name' + | 'isAvailable' + | 'brand' + | 'price' + | 'purchasedPrice'; + +export type GetUserClosetItemsQuerySortDirection = 'desc' | 'asc'; + +export type GetUserClosetItemsQuery = { + /** Number of the page to get, starting at 1. The default is 1. */ + page?: number; + /** Size of each page, as a number between 1 and 180. The default is 60. */ + pageSize?: number; + /** The product category id to filter. */ + categoryId?: number; + /** The product color id to filter. */ + colorId?: number; + /** The brand id to filter. */ + brandId?: number; + /** Get products of some sizes, specified by their numeric identifiers, separated by pipes. */ + sizes?: string; + /** Sort by specified value. The default is to sort by created date. */ + sort?: GetUserClosetItemsQuerySort; + /** Sorts in ascending (asc) or descending (desc) order. Default value is desc */ + sortDirection?: GetUserClosetItemsQuerySortDirection; +}; + +export type GetUserClosetItems = ( + userId: User['id'], + closetId: ClosetSummary['id'], + query?: GetUserClosetItemsQuery, + config?: Config, +) => Promise; diff --git a/packages/client/src/users/closets/types/getUserClosets.types.ts b/packages/client/src/users/closets/types/getUserClosets.types.ts new file mode 100644 index 000000000..c35704395 --- /dev/null +++ b/packages/client/src/users/closets/types/getUserClosets.types.ts @@ -0,0 +1,7 @@ +import type { ClosetSummary } from './closetSummary.types.js'; +import type { Config, User } from '../../../index.js'; + +export type GetUserClosets = ( + userId: User['id'], + config?: Config, +) => Promise; diff --git a/packages/client/src/users/closets/types/index.ts b/packages/client/src/users/closets/types/index.ts new file mode 100644 index 000000000..58611de7e --- /dev/null +++ b/packages/client/src/users/closets/types/index.ts @@ -0,0 +1,5 @@ +export * from './closet.types.js'; +export * from './closetSummary.types.js'; +export * from './deleteUserClosetItem.types.js'; +export * from './getUserClosetItems.types.js'; +export * from './getUserClosets.types.js'; diff --git a/packages/client/src/users/index.ts b/packages/client/src/users/index.ts index ee989cacc..b529430d0 100644 --- a/packages/client/src/users/index.ts +++ b/packages/client/src/users/index.ts @@ -3,12 +3,13 @@ */ export * from './addresses/index.js'; -export * from './authentication/index.js'; export * from './attributes/index.js'; +export * from './authentication/index.js'; export * from './benefits/index.js'; +export * from './closets/index.js'; export * from './contacts/index.js'; export * from './credits/index.js'; export * from './personalIds/index.js'; export * from './preferences/index.js'; -export * from './titles/index.js'; export * from './returns/index.js'; +export * from './titles/index.js'; diff --git a/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap b/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap index 2e7f0c226..4e8101add 100644 --- a/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/redux/src/__tests__/__snapshots__/index.test.ts.snap @@ -129,6 +129,10 @@ Object { "areUserAddressesLoading": [Function], "areUserAttributesLoading": [Function], "areUserBenefitsLoading": [Function], + "areUserClosetItemsFetched": [Function], + "areUserClosetItemsLoading": [Function], + "areUserClosetsFetched": [Function], + "areUserClosetsLoading": [Function], "areUserContactsFetched": [Function], "areUserContactsLoading": [Function], "areUserCreditMovementsLoading": [Function], @@ -782,6 +786,10 @@ Object { "fetchUserAttributesFactory": [Function], "fetchUserBenefits": [Function], "fetchUserBenefitsFactory": [Function], + "fetchUserClosetItems": [Function], + "fetchUserClosetItemsFactory": [Function], + "fetchUserClosets": [Function], + "fetchUserClosetsFactory": [Function], "fetchUserContact": [Function], "fetchUserContactFactory": [Function], "fetchUserContacts": [Function], @@ -1148,6 +1156,10 @@ Object { "getUserBagId": [Function], "getUserBenefits": [Function], "getUserBenefitsError": [Function], + "getUserClosetItemsError": [Function], + "getUserClosetItemsResult": [Function], + "getUserClosets": [Function], + "getUserClosetsError": [Function], "getUserContacts": [Function], "getUserContactsError": [Function], "getUserCreditBalanceError": [Function], @@ -1603,6 +1615,8 @@ Object { "removeUserAddressFactory": [Function], "removeUserAttribute": [Function], "removeUserAttributeFactory": [Function], + "removeUserClosetItem": [Function], + "removeUserClosetItemFactory": [Function], "removeUserContact": [Function], "removeUserContactFactory": [Function], "removeUserDefaultContactAddress": [Function], @@ -1672,6 +1686,8 @@ Object { "resetUpdateCheckoutOrderItem": [Function], "resetUser": [Function], "resetUserAddresses": [Function], + "resetUserClosetItems": [Function], + "resetUserClosets": [Function], "resetUserCreditBalance": [Function], "resetUserSubscriptions": [Function], "resetWishlist": [Function], @@ -1899,6 +1915,12 @@ Object { "FETCH_USER_BENEFITS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_BENEFITS_FAILURE", "FETCH_USER_BENEFITS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_BENEFITS_REQUEST", "FETCH_USER_BENEFITS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_BENEFITS_SUCCESS", + "FETCH_USER_CLOSETS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_FAILURE", + "FETCH_USER_CLOSETS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_REQUEST", + "FETCH_USER_CLOSETS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_SUCCESS", + "FETCH_USER_CLOSET_ITEMS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_CLOSET_ITEMS_FAILURE", + "FETCH_USER_CLOSET_ITEMS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_CLOSET_ITEMS_REQUEST", + "FETCH_USER_CLOSET_ITEMS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_CLOSET_ITEMS_SUCCESS", "FETCH_USER_CONTACTS_FAILURE": "@farfetch/blackout-redux/FETCH_USER_CONTACTS_FAILURE", "FETCH_USER_CONTACTS_REQUEST": "@farfetch/blackout-redux/FETCH_USER_CONTACTS_REQUEST", "FETCH_USER_CONTACTS_SUCCESS": "@farfetch/blackout-redux/FETCH_USER_CONTACTS_SUCCESS", @@ -1965,6 +1987,9 @@ Object { "REMOVE_USER_ATTRIBUTE_FAILURE": "@farfetch/blackout-redux/REMOVE_USER_ATTRIBUTE_FAILURE", "REMOVE_USER_ATTRIBUTE_REQUEST": "@farfetch/blackout-redux/REMOVE_USER_ATTRIBUTE_REQUEST", "REMOVE_USER_ATTRIBUTE_SUCCESS": "@farfetch/blackout-redux/REMOVE_USER_ATTRIBUTE_SUCCESS", + "REMOVE_USER_CLOSET_ITEM_FAILURE": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_FAILURE", + "REMOVE_USER_CLOSET_ITEM_REQUEST": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_REQUEST", + "REMOVE_USER_CLOSET_ITEM_SUCCESS": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_SUCCESS", "REMOVE_USER_CONTACT_FAILURE": "@farfetch/blackout-redux/REMOVE_USER_CONTACT_FAILURE", "REMOVE_USER_CONTACT_REQUEST": "@farfetch/blackout-redux/REMOVE_USER_CONTACT_REQUEST", "REMOVE_USER_CONTACT_SUCCESS": "@farfetch/blackout-redux/REMOVE_USER_CONTACT_SUCCESS", @@ -1989,6 +2014,8 @@ Object { "RESET_REGISTER": "@farfetch/blackout-redux/RESET_REGISTER", "RESET_REMOVE_TOKEN": "@farfetch/blackout-redux/RESET_REMOVE_TOKEN", "RESET_USER_ADDRESSES": "@farfetch/blackout-redux/RESET_USER_ADDRESSES", + "RESET_USER_CLOSETS": "@farfetch/blackout-redux/RESET_USER_CLOSETS", + "RESET_USER_CLOSET_ITEMS_STATE": "@farfetch/blackout-redux/RESET_USER_CLOSET_ITEMS_STATE", "RESET_USER_ENTITIES": "@farfetch/blackout-redux/RESET_USER_ENTITIES", "RESET_USER_PERSONAL_IDS": "@farfetch/blackout-redux/RESET_USER_PERSONAL_IDS", "RESET_USER_STATE": "@farfetch/blackout-redux/RESET_USER_STATE", diff --git a/packages/redux/src/exchanges/actions/__tests__/__snapshots__/createExchangeFilter.test.ts.snap b/packages/redux/src/exchanges/actions/__tests__/__snapshots__/createExchangeFilter.test.ts.snap index 62d925f6f..ad19b9f1e 100644 --- a/packages/redux/src/exchanges/actions/__tests__/__snapshots__/createExchangeFilter.test.ts.snap +++ b/packages/redux/src/exchanges/actions/__tests__/__snapshots__/createExchangeFilter.test.ts.snap @@ -13,12 +13,12 @@ Object { Object { "comparator": "Equals", "criteria": "ProductId", - "value": "18061196", + "values": "18061196", }, Object { "comparator": "LessThanOrEqual", "criteria": "Price", - "value": "1.0", + "values": "1.0", }, ], "id": "25301c32-d738-4315-8e9c-d43d2b57f009", @@ -28,12 +28,12 @@ Object { Object { "comparator": "Equals", "criteria": "ProductId", - "value": "18061196", + "values": "18061196", }, Object { "comparator": "LessThanOrEqual", "criteria": "Price", - "value": "1.0", + "values": "1.0", }, ], }, diff --git a/packages/redux/src/users/actionTypes.ts b/packages/redux/src/users/actionTypes.ts index 82a2f7346..ac6b806c3 100644 --- a/packages/redux/src/users/actionTypes.ts +++ b/packages/redux/src/users/actionTypes.ts @@ -2,6 +2,8 @@ export * from './addresses/actionTypes.js'; export * from './attributes/actionTypes.js'; export * from './authentication/actionTypes.js'; export * from './benefits/actionTypes.js'; +export * from './closets/actionTypes.js'; +export * from './closets/actionTypes.js'; export * from './contacts/actionTypes.js'; export * from './credits/actionTypes.js'; export * from './personalIds/actionTypes.js'; diff --git a/packages/redux/src/users/actions.ts b/packages/redux/src/users/actions.ts index af885b907..24e0ca8d9 100644 --- a/packages/redux/src/users/actions.ts +++ b/packages/redux/src/users/actions.ts @@ -2,9 +2,10 @@ export * from './addresses/actions/index.js'; export * from './attributes/actions/index.js'; export * from './authentication/actions/index.js'; export * from './benefits/actions/index.js'; +export * from './closets/actions/index.js'; export * from './contacts/actions/index.js'; export * from './credits/actions/index.js'; export * from './personalIds/actions/index.js'; export * from './preferences/actions/index.js'; -export * from './titles/actions/index.js'; export * from './returns/actions/index.js'; +export * from './titles/actions/index.js'; diff --git a/packages/redux/src/users/closets/__tests__/reducer.test.ts b/packages/redux/src/users/closets/__tests__/reducer.test.ts new file mode 100644 index 000000000..9f100b7cf --- /dev/null +++ b/packages/redux/src/users/closets/__tests__/reducer.test.ts @@ -0,0 +1,291 @@ +import * as actionTypes from '../actionTypes.js'; +import { + mockGetUserClosetItemsResponse, + mockGetUserClosetsResponse, + mockState, +} from 'tests/__fixtures__/users/index.mjs'; +import { toBlackoutError } from '@farfetch/blackout-client'; +import reducer, * as fromReducer from '../reducer.js'; +import type { UserClosetsState } from '../types/index.js'; + +let initialState: UserClosetsState; +const randomAction = { type: 'this_is_a_random_action' }; + +describe('closets reducer', () => { + beforeEach(() => { + initialState = fromReducer.INITIAL_STATE; + }); + + describe('reset handling', () => { + it('RESET_USER_CLOSETS should return the initial state', () => { + expect( + reducer(mockState.closets, { + type: actionTypes.RESET_USER_CLOSETS, + }), + ).toEqual(initialState); + }); + }); + + describe('error() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).error; + + expect(state).toBe(initialState.error); + expect(state).toBeNull(); + }); + + it.each([actionTypes.FETCH_USER_CLOSETS_REQUEST])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + error: toBlackoutError(new Error('error')), + }; + + expect( + reducer(state, { + type: actionType, + }).error, + ).toBe(initialState.error); + }, + ); + + it.each([actionTypes.FETCH_USER_CLOSETS_FAILURE])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + error: null, + }; + const error = 'foo'; + + expect( + reducer(state, { + payload: { error }, + type: actionType, + }).error, + ).toBe(error); + }, + ); + + it('should handle other actions by returning the previous state', () => { + const state = { + ...initialState, + error: toBlackoutError(new Error('error')), + }; + + expect(reducer(state, randomAction).error).toBe(state.error); + }); + }); + + describe('isLoading() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).isLoading; + + expect(state).toBe(initialState.isLoading); + expect(state).toBe(false); + }); + + it.each([actionTypes.FETCH_USER_CLOSETS_REQUEST])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + isLoading: false, + }; + + expect( + reducer(state, { + type: actionType, + }).isLoading, + ).toBe(true); + }, + ); + + it.each([actionTypes.FETCH_USER_CLOSETS_SUCCESS])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + isLoading: true, + }; + + expect( + reducer(state, { + payload: { result: '' }, + type: actionType, + }).isLoading, + ).toBe(initialState.isLoading); + }, + ); + + it.each([actionTypes.FETCH_USER_CLOSETS_FAILURE])( + 'should handle %s action type', + actionType => { + const state = { + ...initialState, + error: null, + }; + const error = 'foo'; + + expect( + reducer(state, { + payload: { error }, + type: actionType, + }).isLoading, + ).toBe(initialState.isLoading); + }, + ); + + it('should handle other actions by returning the previous state', () => { + const state = { ...initialState, isLoading: false }; + + expect(reducer(state, randomAction).isLoading).toBe(state.isLoading); + }); + }); + + describe('result() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).result; + + expect(state).toBe(initialState.result); + }); + + it.each([actionTypes.FETCH_USER_CLOSETS_SUCCESS])( + 'should handle %s action type', + actionType => { + const expectedResult = mockGetUserClosetsResponse; + const state = { + ...initialState, + isLoading: true, + }; + + expect( + reducer(state, { + payload: expectedResult, + type: actionType, + }).result, + ).toBe(expectedResult); + }, + ); + + it('should handle other actions by returning the previous state', () => { + const state = mockState?.closets; + + expect(reducer(state, randomAction).result).toBe(state.result); + }); + }); + + describe('closet() reducer', () => { + it('should return the initial state', () => { + const state = reducer(undefined, randomAction).closetItems; + + expect(state).toEqual(initialState.closetItems); + expect(state).toEqual({ error: null, isLoading: false, result: null }); + }); + + it('should handle FETCH_USER_CLOSET_ITEMS_REQUEST action type', () => { + const state: UserClosetsState = { + ...fromReducer.INITIAL_STATE, + closetItems: { + error: toBlackoutError(new Error('dummy error')), + isLoading: false, + result: null, + }, + }; + + expect( + reducer(state, { + type: actionTypes.FETCH_USER_CLOSET_ITEMS_REQUEST, + }).closetItems, + ).toEqual({ + error: null, + isLoading: true, + }); + }); + + it('should handle FETCH_USER_CLOSET_ITEMS_FAILURE action type', () => { + const state: UserClosetsState = { + ...fromReducer.INITIAL_STATE, + closetItems: { + error: null, + isLoading: true, + result: null, + }, + }; + + expect( + reducer(state, { + type: actionTypes.FETCH_USER_CLOSET_ITEMS_FAILURE, + payload: { error: toBlackoutError(new Error('dummy error')) }, + }).closetItems, + ).toEqual({ + error: toBlackoutError(new Error('dummy error')), + isLoading: false, + }); + }); + + it('should handle FETCH_USER_CLOSET_ITEMS_SUCCESS action type', () => { + const state: UserClosetsState = { + ...fromReducer.INITIAL_STATE, + closetItems: { + error: null, + isLoading: true, + result: null, + }, + }; + + const expectedResult = mockGetUserClosetItemsResponse; + + expect( + reducer(state, { + type: actionTypes.FETCH_USER_CLOSET_ITEMS_SUCCESS, + payload: expectedResult, + }).closetItems, + ).toEqual({ + error: null, + isLoading: false, + result: expectedResult, + }); + }); + + it('should handle other actions by returning the previous state', () => { + const state = { + ...initialState, + closetItems: { error: null, isLoading: false, result: null }, + }; + + expect(reducer(state, randomAction).closetItems).toEqual( + state.closetItems, + ); + }); + }); + + describe('getError() selector', () => { + it('should return the `error` property from a given state', () => { + expect(fromReducer.getError(initialState)).toBe(initialState.error); + }); + }); + + describe('getIsLoading() selector', () => { + it('should return the `isLoading` property from a given state', () => { + expect(fromReducer.getIsLoading(initialState)).toBe( + initialState.isLoading, + ); + }); + }); + + describe('getResult() selector', () => { + it('should return the `result` property from a given state', () => { + expect(fromReducer.getResult(initialState)).toBe(initialState.result); + }); + }); + + describe('getUserCloset() selector', () => { + it('should return the `closetSummaries` property from a given state', () => { + expect(fromReducer.getUserClosetItems(initialState)).toBe( + initialState.closetItems, + ); + }); + }); +}); diff --git a/packages/redux/src/users/closets/__tests__/selectors.test.ts b/packages/redux/src/users/closets/__tests__/selectors.test.ts new file mode 100644 index 000000000..26764f790 --- /dev/null +++ b/packages/redux/src/users/closets/__tests__/selectors.test.ts @@ -0,0 +1,135 @@ +import * as selectors from '../selectors.js'; +import { merge } from 'lodash-es'; +import { mockBaseState } from '../../__fixtures__/state.fixtures.js'; +import { + mockGetUserClosetItemsResponse, + mockGetUserClosetsResponse, +} from 'tests/__fixtures__/users/index.mjs'; + +describe('redux selectors', () => { + describe('areUserClosetsLoading()', () => { + const mockLoadingState = merge({}, mockBaseState, { + users: { closets: { isLoading: true } }, + }); + + it('should return `isLoading` value from `closets` slice', () => { + expect(selectors.areUserClosetsLoading(mockBaseState)).toBe(false); + + expect(selectors.areUserClosetsLoading(mockLoadingState)).toBe(true); + }); + }); + + describe('getUserClosetsError()', () => { + const dummyError = new Error('dummy error'); + + const mockErrorState = merge({}, mockBaseState, { + users: { closets: { error: dummyError } }, + }); + + it('should return `error` value from `closets` slice', () => { + expect(selectors.getUserClosetsError(mockBaseState)).toBeNull(); + + expect(selectors.getUserClosetsError(mockErrorState)).toBe(dummyError); + }); + }); + + describe('getUserClosets()', () => { + it('should return `result` value from `closets` slice', () => { + const userClosets = mockGetUserClosetsResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { result: userClosets }, + }, + }); + + expect(selectors.getUserClosets(mockStateWithResult)).toEqual( + userClosets, + ); + }); + }); + + describe('areUserClosetsFetched()', () => { + it('should return true if the user closets is fetched', () => { + const userClosets = mockGetUserClosetsResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { result: userClosets }, + }, + }); + + expect(selectors.areUserClosetsFetched(mockStateWithResult)).toBeTruthy(); + }); + }); + + describe('areUserClosetItemsLoading()', () => { + const mockLoadingState = merge({}, mockBaseState, { + users: { + closets: { + closetItems: { + error: null, + isLoading: true, + result: null, + }, + }, + }, + }); + + it('should return `isLoading` value from `closets.closet` slice', () => { + expect(selectors.areUserClosetItemsLoading(mockBaseState)).toBe(false); + + expect(selectors.areUserClosetItemsLoading(mockLoadingState)).toBe(true); + }); + }); + + describe('getUserClosetItemsError()', () => { + const dummyError = new Error('dummy error'); + + const mockErrorState = merge({}, mockBaseState, { + users: { closets: { closetItems: { error: dummyError } } }, + }); + + it('should return `error` value from `closets.closet` slice', () => { + expect(selectors.getUserClosetItemsError(mockBaseState)).toBeNull(); + + expect(selectors.getUserClosetItemsError(mockErrorState)).toBe( + dummyError, + ); + }); + }); + + describe('getUserClosetItemsResult()', () => { + const closetSummaries = mockGetUserClosetItemsResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { closetItems: { result: closetSummaries } }, + }, + }); + + it('should return `result` value from `closets.closet` slice', () => { + expect(selectors.getUserClosetItemsResult(mockBaseState)).toBeNull(); + + expect(selectors.getUserClosetItemsResult(mockStateWithResult)).toEqual( + closetSummaries, + ); + }); + }); + + describe('areUserClosetItemsFetched()', () => { + it('should return true if the user closet is fetched', () => { + const userCloset = mockGetUserClosetItemsResponse; + + const mockStateWithResult = merge({}, mockBaseState, { + users: { + closets: { closetItems: { result: userCloset } }, + }, + }); + + expect( + selectors.areUserClosetItemsFetched(mockStateWithResult), + ).toBeTruthy(); + }); + }); +}); diff --git a/packages/redux/src/users/closets/actionTypes.ts b/packages/redux/src/users/closets/actionTypes.ts new file mode 100644 index 000000000..13283ca62 --- /dev/null +++ b/packages/redux/src/users/closets/actionTypes.ts @@ -0,0 +1,54 @@ +/** + * Action type dispatched when the fetch user closet items request fails. + */ +export const FETCH_USER_CLOSET_ITEMS_FAILURE = + '@farfetch/blackout-redux/FETCH_USER_CLOSET_ITEMS_FAILURE'; +/** + * Action type dispatched when the fetch user closet items request starts. + */ +export const FETCH_USER_CLOSET_ITEMS_REQUEST = + '@farfetch/blackout-redux/FETCH_USER_CLOSET_ITEMS_REQUEST'; +/** + * Action type dispatched when the fetch user closet items request succeeds. + */ +export const FETCH_USER_CLOSET_ITEMS_SUCCESS = + '@farfetch/blackout-redux/FETCH_USER_CLOSET_ITEMS_SUCCESS'; + +/** + * Action type dispatched when the fetch user closets request fails. + */ +export const FETCH_USER_CLOSETS_FAILURE = + '@farfetch/blackout-redux/FETCH_USER_CLOSETS_FAILURE'; +/** + * Action type dispatched when the fetch user closets request starts. + */ +export const FETCH_USER_CLOSETS_REQUEST = + '@farfetch/blackout-redux/FETCH_USER_CLOSETS_REQUEST'; +/** + * Action type dispatched when the fetch user closets request succeeds. + */ +export const FETCH_USER_CLOSETS_SUCCESS = + '@farfetch/blackout-redux/FETCH_USER_CLOSETS_SUCCESS'; + +/** + * Action type dispatched when the remove user closet item request fails. + */ +export const REMOVE_USER_CLOSET_ITEM_FAILURE = + '@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_FAILURE'; +/** + * Action type dispatched when the remove user closet item request starts. + */ +export const REMOVE_USER_CLOSET_ITEM_REQUEST = + '@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_REQUEST'; +/** + * Action type dispatched when the remove user closet item request succeeds. + */ +export const REMOVE_USER_CLOSET_ITEM_SUCCESS = + '@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_SUCCESS'; + +/** Action type dispatched when resetting the user closets state. */ +export const RESET_USER_CLOSETS = '@farfetch/blackout-redux/RESET_USER_CLOSETS'; + +/** Action type dispatched when resetting the user closet items slice state. */ +export const RESET_USER_CLOSET_ITEMS_STATE = + '@farfetch/blackout-redux/RESET_USER_CLOSET_ITEMS_STATE'; diff --git a/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap new file mode 100644 index 000000000..19dde64d1 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/fetchUserClosets.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fetchUserClosets() action creator should create the correct actions for when the fetch user closets procedure is successful: fetch user closets success payload 1`] = ` +Object { + "payload": Array [ + Object { + "createdDate": "2023-08-17T11:52:35.941Z", + "id": "3455f70e-f756-4ad5-b8e3-46d32ac74def", + "totalItems": 1, + }, + ], + "type": "@farfetch/blackout-redux/FETCH_USER_CLOSETS_SUCCESS", +} +`; diff --git a/packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap new file mode 100644 index 000000000..7bdefebab --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/__snapshots__/removeUserClosetItem.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`removeUserClosetItem() action creator should create the correct actions for when the remove user closet item procedure is successful: remove user closet item success payload 1`] = ` +Object { + "type": "@farfetch/blackout-redux/REMOVE_USER_CLOSET_ITEM_SUCCESS", +} +`; diff --git a/packages/redux/src/users/closets/actions/__tests__/fetchUserClosetItems.test.ts b/packages/redux/src/users/closets/actions/__tests__/fetchUserClosetItems.test.ts new file mode 100644 index 000000000..9978564f2 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/fetchUserClosetItems.test.ts @@ -0,0 +1,156 @@ +import * as actionTypes from '../../actionTypes.js'; +import { + closetId, + config, + expectedConfig, + getExpectedUserClosetItemsPayload, + mockGetUserClosetItemsResponse, + mockProductImgQueryParam, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { fetchUserClosetItems } from '../index.js'; +import { getUserClosetItems } from '@farfetch/blackout-client'; +import { INITIAL_STATE } from '../../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import thunk from 'redux-thunk'; +import type { + GetOptionsArgument, + StoreState, +} from '../../../../types/index.js'; + +jest.mock('@farfetch/blackout-client', () => ({ + ...jest.requireActual('@farfetch/blackout-client'), + getUserClosetItems: jest.fn(), +})); + +const getOptions = () => ({ productImgQueryParam: mockProductImgQueryParam }); +const mockMiddlewares = [ + thunk.withExtraArgument({ + getOptions, + }), +]; + +const usersMockStore = (state = {}) => + mockStore({ users: INITIAL_STATE }, state, mockMiddlewares); +const usersMockStoreWithoutMiddlewares = (state = {}) => + mockStore({ orders: INITIAL_STATE }, state); +const query = {}; + +describe('fetchUserClosetItems() action creator', () => { + let store = usersMockStore(); + + beforeEach(() => { + jest.clearAllMocks(); + store = usersMockStore(); + }); + + it('should create the correct actions for when the fetch user closet items procedure fails', async () => { + const expectedError = new Error('fetch user closet items error'); + + (getUserClosetItems as jest.Mock).mockRejectedValueOnce(expectedError); + + await expect( + async () => + await fetchUserClosetItems( + userId, + closetId, + query, + config, + )(store.dispatch, store.getState as () => StoreState, { getOptions }), + ).rejects.toThrow(expectedError); + + expect(getUserClosetItems).toHaveBeenCalledTimes(1); + expect(getUserClosetItems).toHaveBeenCalledWith( + userId, + closetId, + query, + expectedConfig, + ); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { type: actionTypes.FETCH_USER_CLOSET_ITEMS_REQUEST }, + { + type: actionTypes.FETCH_USER_CLOSET_ITEMS_FAILURE, + payload: { error: expectedError }, + }, + ]), + ); + }); + + it('should create the correct actions for when the fetch user closet items procedure is successful', async () => { + (getUserClosetItems as jest.Mock).mockResolvedValueOnce( + mockGetUserClosetItemsResponse, + ); + + const expectedPayload = getExpectedUserClosetItemsPayload( + mockProductImgQueryParam, + ); + + await fetchUserClosetItems( + userId, + closetId, + query, + config, + )(store.dispatch, store.getState as () => StoreState, { getOptions }).then( + clientResult => { + expect(clientResult).toEqual(mockGetUserClosetItemsResponse); + }, + ); + + expect(getUserClosetItems).toHaveBeenCalledTimes(1); + expect(getUserClosetItems).toHaveBeenCalledWith( + userId, + closetId, + query, + expectedConfig, + ); + expect(store.getActions()).toEqual([ + { + type: actionTypes.FETCH_USER_CLOSET_ITEMS_REQUEST, + }, + { + payload: expectedPayload, + type: actionTypes.FETCH_USER_CLOSET_ITEMS_SUCCESS, + }, + ]); + }); + + it('should create the correct actions for when the fetch user closet items procedure is successful without receiving options', async () => { + store = usersMockStoreWithoutMiddlewares(); + (getUserClosetItems as jest.Mock).mockResolvedValueOnce( + mockGetUserClosetItemsResponse, + ); + + const expectedPayload = getExpectedUserClosetItemsPayload(); + + await fetchUserClosetItems( + userId, + closetId, + query, + config, + )( + store.dispatch, + store.getState as () => StoreState, + {} as GetOptionsArgument, + ).then(clientResult => { + expect(clientResult).toEqual(mockGetUserClosetItemsResponse); + }); + + expect(getUserClosetItems).toHaveBeenCalledTimes(1); + expect(getUserClosetItems).toHaveBeenCalledWith( + userId, + closetId, + query, + expectedConfig, + ); + expect(store.getActions()).toEqual([ + { + type: actionTypes.FETCH_USER_CLOSET_ITEMS_REQUEST, + }, + { + payload: expectedPayload, + type: actionTypes.FETCH_USER_CLOSET_ITEMS_SUCCESS, + }, + ]); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts b/packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts new file mode 100644 index 000000000..c05423ce5 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/fetchUserClosets.test.ts @@ -0,0 +1,76 @@ +import * as actionTypes from '../../actionTypes.js'; +import { + config, + expectedConfig, + mockGetUserClosetsResponse, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { fetchUserClosets } from '../index.js'; +import { find } from 'lodash-es'; +import { getUserClosets } from '@farfetch/blackout-client'; +import { INITIAL_STATE } from '../../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; + +jest.mock('@farfetch/blackout-client', () => ({ + ...jest.requireActual('@farfetch/blackout-client'), + getUserClosets: jest.fn(), +})); + +const usersMockStore = (state = {}) => + mockStore({ users: INITIAL_STATE }, state); + +describe('fetchUserClosets() action creator', () => { + let store = usersMockStore(); + + beforeEach(() => { + jest.clearAllMocks(); + store = usersMockStore(); + }); + + it('should create the correct actions for when the fetch user closets procedure fails', async () => { + const expectedError = new Error('fetch user closets error'); + + (getUserClosets as jest.Mock).mockRejectedValueOnce(expectedError); + + await expect( + async () => await fetchUserClosets(userId, config)(store.dispatch), + ).rejects.toThrow(expectedError); + + expect(getUserClosets).toHaveBeenCalledTimes(1); + expect(getUserClosets).toHaveBeenCalledWith(userId, expectedConfig); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { type: actionTypes.FETCH_USER_CLOSETS_REQUEST }, + { + type: actionTypes.FETCH_USER_CLOSETS_FAILURE, + payload: { error: expectedError }, + }, + ]), + ); + }); + + it('should create the correct actions for when the fetch user closets procedure is successful', async () => { + (getUserClosets as jest.Mock).mockResolvedValueOnce( + mockGetUserClosetsResponse, + ); + + await fetchUserClosets(userId, config)(store.dispatch); + + const actionResults = store.getActions(); + + expect(getUserClosets).toHaveBeenCalledTimes(1); + expect(getUserClosets).toHaveBeenCalledWith(userId, expectedConfig); + expect(actionResults).toMatchObject([ + { type: actionTypes.FETCH_USER_CLOSETS_REQUEST }, + { + payload: mockGetUserClosetsResponse, + type: actionTypes.FETCH_USER_CLOSETS_SUCCESS, + }, + ]); + expect( + find(actionResults, { + type: actionTypes.FETCH_USER_CLOSETS_SUCCESS, + }), + ).toMatchSnapshot('fetch user closets success payload'); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts b/packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts new file mode 100644 index 000000000..d1a0c958c --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/removeUserClosetItem.test.ts @@ -0,0 +1,95 @@ +import * as actionTypes from '../../actionTypes.js'; +import { + closetId, + closetItemId, + config, + expectedConfig, + userId, +} from 'tests/__fixtures__/users/index.mjs'; +import { deleteUserClosetItem } from '@farfetch/blackout-client'; +import { find } from 'lodash-es'; +import { INITIAL_STATE } from '../../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import { removeUserClosetItem } from '../index.js'; + +jest.mock('@farfetch/blackout-client', () => ({ + ...jest.requireActual('@farfetch/blackout-client'), + deleteUserClosetItem: jest.fn(), +})); + +const usersMockStore = (state = {}) => + mockStore({ users: INITIAL_STATE }, state); + +describe('removeUserClosetItem() action creator', () => { + let store = usersMockStore(); + + beforeEach(() => { + jest.clearAllMocks(); + store = usersMockStore(); + }); + + it('should create the correct actions for when the remove user closet item procedure fails', async () => { + const expectedError = new Error('remove user closet item error'); + + (deleteUserClosetItem as jest.Mock).mockRejectedValueOnce(expectedError); + + await expect( + async () => + await removeUserClosetItem( + userId, + closetId, + closetItemId, + config, + )(store.dispatch), + ).rejects.toThrow(expectedError); + + expect(deleteUserClosetItem).toHaveBeenCalledTimes(1); + expect(deleteUserClosetItem).toHaveBeenCalledWith( + userId, + closetId, + closetItemId, + expectedConfig, + ); + expect(store.getActions()).toEqual( + expect.arrayContaining([ + { type: actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST }, + { + type: actionTypes.REMOVE_USER_CLOSET_ITEM_FAILURE, + payload: { error: expectedError }, + }, + ]), + ); + }); + + it('should create the correct actions for when the remove user closet item procedure is successful', async () => { + (deleteUserClosetItem as jest.Mock).mockResolvedValueOnce(200); + + await removeUserClosetItem( + userId, + closetId, + closetItemId, + config, + )(store.dispatch); + + const actionResults = store.getActions(); + + expect(deleteUserClosetItem).toHaveBeenCalledTimes(1); + expect(deleteUserClosetItem).toHaveBeenCalledWith( + userId, + closetId, + closetItemId, + expectedConfig, + ); + expect(actionResults).toMatchObject([ + { type: actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST }, + { + type: actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS, + }, + ]); + expect( + find(actionResults, { + type: actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS, + }), + ).toMatchSnapshot('remove user closet item success payload'); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/resetUserClosetItems.test.ts b/packages/redux/src/users/closets/actions/__tests__/resetUserClosetItems.test.ts new file mode 100644 index 000000000..44a008875 --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/resetUserClosetItems.test.ts @@ -0,0 +1,23 @@ +import * as actionTypes from '../../actionTypes.js'; +import { INITIAL_STATE } from '../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import { resetUserClosetItems } from '../index.js'; + +const closetsMockStore = (state = {}) => + mockStore({ closets: INITIAL_STATE }, state); +let store: ReturnType; + +describe('resetUserClosetItems() action creator', () => { + beforeEach(() => { + jest.clearAllMocks(); + store = closetsMockStore(); + }); + + it('should dispatch the correct action for when the reset user closet items is called', async () => { + await resetUserClosetItems()(store.dispatch); + + expect(store.getActions()).toMatchObject([ + { type: actionTypes.RESET_USER_CLOSET_ITEMS_STATE }, + ]); + }); +}); diff --git a/packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts b/packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts new file mode 100644 index 000000000..aa6a4f68b --- /dev/null +++ b/packages/redux/src/users/closets/actions/__tests__/resetUserClosets.test.ts @@ -0,0 +1,23 @@ +import * as actionTypes from '../../actionTypes.js'; +import { INITIAL_STATE } from '../../reducer.js'; +import { mockStore } from '../../../../../tests/index.js'; +import { resetUserClosets } from '../index.js'; + +const closetsMockStore = (state = {}) => + mockStore({ closets: INITIAL_STATE }, state); +let store: ReturnType; + +describe('resetUserClosets() action creator', () => { + beforeEach(() => { + jest.clearAllMocks(); + store = closetsMockStore(); + }); + + it('should dispatch the correct action for when the reset user closets is called', async () => { + await resetUserClosets()(store.dispatch); + + expect(store.getActions()).toMatchObject([ + { type: actionTypes.RESET_USER_CLOSETS }, + ]); + }); +}); diff --git a/packages/redux/src/users/closets/actions/factories/fetchUserClosetItemsFactory.ts b/packages/redux/src/users/closets/actions/factories/fetchUserClosetItemsFactory.ts new file mode 100644 index 000000000..3d72028ae --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/fetchUserClosetItemsFactory.ts @@ -0,0 +1,77 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type Closet, + type ClosetSummary, + type Config, + type GetUserClosetItems, + type GetUserClosetItemsQuery, + toBlackoutError, + type User, +} from '@farfetch/blackout-client'; +import adaptProductImages from '../../../../helpers/adapters/adaptProductImages.js'; +import type { Dispatch } from 'redux'; +import type { FetchUserClosetItemsAction } from '../../types/actions.types.js'; +import type { + GetOptionsArgument, + StoreState, +} from '../../../../types/index.js'; + +/** + * Method responsible for fetching the items (paginated) from a specific closet. + * + * @param getUserClosetItems - Get user closet items client. + * + * @returns Thunk factory. + */ +const fetchUserClosetItemsFactory = + (getUserClosetItems: GetUserClosetItems) => + ( + userId: User['id'], + closetId: ClosetSummary['id'], + query?: GetUserClosetItemsQuery, + config?: Config, + ) => + async ( + dispatch: Dispatch, + getState: () => StoreState, + { + getOptions = arg => ({ productImgQueryParam: arg.productImgQueryParam }), + }: GetOptionsArgument, + ): Promise => { + try { + dispatch({ + type: actionTypes.FETCH_USER_CLOSET_ITEMS_REQUEST, + }); + + const { productImgQueryParam } = getOptions(getState); + const result = await getUserClosetItems(userId, closetId, query, config); + + const adaptedResult = { + ...result, + entries: result.entries.map(item => ({ + ...item, + images: adaptProductImages(item.images.images, { + productImgQueryParam, + }), + })), + }; + + dispatch({ + type: actionTypes.FETCH_USER_CLOSET_ITEMS_SUCCESS, + payload: adaptedResult, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.FETCH_USER_CLOSET_ITEMS_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default fetchUserClosetItemsFactory; diff --git a/packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts b/packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts new file mode 100644 index 000000000..6929eb2fd --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/fetchUserClosetsFactory.ts @@ -0,0 +1,50 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type ClosetSummary, + type Config, + type GetUserClosets, + toBlackoutError, + type User, +} from '@farfetch/blackout-client'; +import type { Dispatch } from 'redux'; +import type { FetchUserClosetsAction } from '../../types/actions.types.js'; + +/** + * Method responsible for fetching the user closets. + * + * @param getUserClosets - Get user closets client. + * + * @returns Thunk factory. + */ +const fetchUserClosetsFactory = + (getUserClosets: GetUserClosets) => + (userId: User['id'], config?: Config) => + async ( + dispatch: Dispatch, + ): Promise => { + try { + dispatch({ + type: actionTypes.FETCH_USER_CLOSETS_REQUEST, + }); + + const result = await getUserClosets(userId, config); + + dispatch({ + type: actionTypes.FETCH_USER_CLOSETS_SUCCESS, + payload: result, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.FETCH_USER_CLOSETS_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default fetchUserClosetsFactory; diff --git a/packages/redux/src/users/closets/actions/factories/index.ts b/packages/redux/src/users/closets/actions/factories/index.ts new file mode 100644 index 000000000..12c053cf2 --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/index.ts @@ -0,0 +1,3 @@ +export { default as fetchUserClosetItemsFactory } from './fetchUserClosetItemsFactory.js'; +export { default as fetchUserClosetsFactory } from './fetchUserClosetsFactory.js'; +export { default as removeUserClosetItemFactory } from './removeUserClosetItemFactory.js'; diff --git a/packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts b/packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts new file mode 100644 index 000000000..ffc112f3c --- /dev/null +++ b/packages/redux/src/users/closets/actions/factories/removeUserClosetItemFactory.ts @@ -0,0 +1,58 @@ +import * as actionTypes from '../../../actionTypes.js'; +import { + type ClosetItem, + type ClosetSummary, + type Config, + type DeleteUserClosetItem, + toBlackoutError, + type User, +} from '@farfetch/blackout-client'; +import type { Dispatch } from 'redux'; +import type { RemoveUserClosetItemAction } from '../../types/actions.types.js'; + +/** + * Method responsible for removing a specific item from user closet. + * + * @param deleteUserClosetItem - Delete user closet item client. + * + * @returns Thunk factory. + */ +const removeUserClosetItemFactory = + (deleteUserClosetItem: DeleteUserClosetItem) => + ( + userId: User['id'], + closetId: ClosetSummary['id'], + itemId: ClosetItem['id'], + config?: Config, + ) => + async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST, + }); + + const result = await deleteUserClosetItem( + userId, + closetId, + itemId, + config, + ); + + dispatch({ + type: actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS, + }); + + return result; + } catch (error) { + const errorAsBlackoutError = toBlackoutError(error); + + dispatch({ + payload: { error: errorAsBlackoutError }, + type: actionTypes.REMOVE_USER_CLOSET_ITEM_FAILURE, + }); + + throw errorAsBlackoutError; + } + }; + +export default removeUserClosetItemFactory; diff --git a/packages/redux/src/users/closets/actions/fetchUserClosetItems.ts b/packages/redux/src/users/closets/actions/fetchUserClosetItems.ts new file mode 100644 index 000000000..944244f26 --- /dev/null +++ b/packages/redux/src/users/closets/actions/fetchUserClosetItems.ts @@ -0,0 +1,7 @@ +import { fetchUserClosetItemsFactory } from './factories/index.js'; +import { getUserClosetItems } from '@farfetch/blackout-client'; + +/** + * Fetch user closet items. + */ +export default fetchUserClosetItemsFactory(getUserClosetItems); diff --git a/packages/redux/src/users/closets/actions/fetchUserClosets.ts b/packages/redux/src/users/closets/actions/fetchUserClosets.ts new file mode 100644 index 000000000..86c275f73 --- /dev/null +++ b/packages/redux/src/users/closets/actions/fetchUserClosets.ts @@ -0,0 +1,7 @@ +import { fetchUserClosetsFactory } from './factories/index.js'; +import { getUserClosets } from '@farfetch/blackout-client'; + +/** + * Fetch user closets. + */ +export default fetchUserClosetsFactory(getUserClosets); diff --git a/packages/redux/src/users/closets/actions/index.ts b/packages/redux/src/users/closets/actions/index.ts new file mode 100644 index 000000000..71ebd22da --- /dev/null +++ b/packages/redux/src/users/closets/actions/index.ts @@ -0,0 +1,7 @@ +export { default as fetchUserClosetItems } from './fetchUserClosetItems.js'; +export { default as fetchUserClosets } from './fetchUserClosets.js'; +export { default as removeUserClosetItem } from './removeUserClosetItem.js'; +export { default as resetUserClosetItems } from './resetUserClosetItems.js'; +export { default as resetUserClosets } from './resetUserClosets.js'; + +export * from './factories/index.js'; diff --git a/packages/redux/src/users/closets/actions/removeUserClosetItem.ts b/packages/redux/src/users/closets/actions/removeUserClosetItem.ts new file mode 100644 index 000000000..c3c61b989 --- /dev/null +++ b/packages/redux/src/users/closets/actions/removeUserClosetItem.ts @@ -0,0 +1,7 @@ +import { deleteUserClosetItem } from '@farfetch/blackout-client'; +import { removeUserClosetItemFactory } from './factories/index.js'; + +/** + * Remove user closet item. + */ +export default removeUserClosetItemFactory(deleteUserClosetItem); diff --git a/packages/redux/src/users/closets/actions/resetUserClosetItems.ts b/packages/redux/src/users/closets/actions/resetUserClosetItems.ts new file mode 100644 index 000000000..0e8fcdc61 --- /dev/null +++ b/packages/redux/src/users/closets/actions/resetUserClosetItems.ts @@ -0,0 +1,18 @@ +import * as actionTypes from '../actionTypes.js'; +import type { Dispatch } from 'redux'; +import type { ResetUserClosetItemsStateAction } from '../types/index.js'; + +/** + * Reset user closet items slice state only + * to its initial value. + * + * @returns - Thunk. + */ +const resetUserClosetState = + () => (dispatch: Dispatch) => { + dispatch({ + type: actionTypes.RESET_USER_CLOSET_ITEMS_STATE, + }); + }; + +export default resetUserClosetState; diff --git a/packages/redux/src/users/closets/actions/resetUserClosets.ts b/packages/redux/src/users/closets/actions/resetUserClosets.ts new file mode 100644 index 000000000..1c67a9792 --- /dev/null +++ b/packages/redux/src/users/closets/actions/resetUserClosets.ts @@ -0,0 +1,17 @@ +import * as actionTypes from '../actionTypes.js'; +import type { Dispatch } from 'redux'; +import type { ResetUserClosetsStateAction } from '../types/index.js'; + +/** + * Reset user closets state to its initial value. + * + * @returns - Thunk. + */ +const resetUserClosets = + () => (dispatch: Dispatch) => { + dispatch({ + type: actionTypes.RESET_USER_CLOSETS, + }); + }; + +export default resetUserClosets; diff --git a/packages/redux/src/users/closets/reducer.ts b/packages/redux/src/users/closets/reducer.ts new file mode 100644 index 000000000..9a87bd2eb --- /dev/null +++ b/packages/redux/src/users/closets/reducer.ts @@ -0,0 +1,96 @@ +import * as actionTypes from './actionTypes.js'; +import { type AnyAction, combineReducers, type Reducer } from 'redux'; +import { createReducerWithResult } from '../../helpers/reducerFactory.js'; +import type * as T from './types/index.js'; + +export const INITIAL_STATE: T.UserClosetsState = { + result: null, + error: null, + isLoading: false, + closetItems: { + result: null, + error: null, + isLoading: false, + }, +}; + +const isLoading = ( + state = INITIAL_STATE.isLoading, + action: AnyAction, +): T.UserClosetsState['isLoading'] => { + switch (action.type) { + case actionTypes.FETCH_USER_CLOSETS_REQUEST: + return true; + case actionTypes.FETCH_USER_CLOSETS_FAILURE: + case actionTypes.FETCH_USER_CLOSETS_SUCCESS: + return INITIAL_STATE.isLoading; + default: + return state; + } +}; + +const error = ( + state = INITIAL_STATE.error, + action: AnyAction, +): T.UserClosetsState['error'] => { + switch (action.type) { + case actionTypes.FETCH_USER_CLOSETS_REQUEST: + return INITIAL_STATE.error; + case actionTypes.FETCH_USER_CLOSETS_FAILURE: + return (action as T.FetchUserClosetsFailureAction).payload.error; + default: + return state; + } +}; + +const result = ( + state = INITIAL_STATE.result, + action: AnyAction, +): T.UserClosetsState['result'] => { + switch (action.type) { + case actionTypes.FETCH_USER_CLOSETS_SUCCESS: + return (action as T.FetchUserClosetsSuccessAction).payload; + default: + return state; + } +}; + +export const closetItems = createReducerWithResult( + ['FETCH_USER_CLOSET_ITEMS', 'REMOVE_USER_CLOSET_ITEM'], + INITIAL_STATE.closetItems, + actionTypes, + false, + false, + actionTypes.RESET_USER_CLOSET_ITEMS_STATE, +); + +export const getError = (state: T.UserClosetsState) => state.error; +export const getIsLoading = (state: T.UserClosetsState) => state.isLoading; +export const getResult = (state: T.UserClosetsState) => state.result; +export const getUserClosetItems = (state: T.UserClosetsState) => + state.closetItems; + +const reducer = combineReducers({ + isLoading, + result, + error, + closetItems, +}); + +/** + * Reducer for user closets state. + * + * @param state - Current redux state. + * @param action - Action dispatched. + * + * @returns New state. + */ +const closetsReducer: Reducer = (state, action) => { + if (action.type === actionTypes.RESET_USER_CLOSETS) { + return INITIAL_STATE; + } + + return reducer(state, action); +}; + +export default closetsReducer; diff --git a/packages/redux/src/users/closets/selectors.ts b/packages/redux/src/users/closets/selectors.ts new file mode 100644 index 000000000..2ba1b6777 --- /dev/null +++ b/packages/redux/src/users/closets/selectors.ts @@ -0,0 +1,102 @@ +import { + getError, + getIsLoading, + getResult, + getUserClosetItems, +} from './reducer.js'; +import { getUserClosets as getUserClosetsFromReducer } from '../reducer.js'; +import type { StoreState } from '../../types/storeState.types.js'; +import type { UsersState } from '../types/state.types.js'; + +/** + * Returns the user closets result from the application state. + * + * @param state - Application state. + * + * @returns Returns user closet result. + */ +export const getUserClosets = (state: StoreState) => + getResult(getUserClosetsFromReducer(state.users as UsersState)); + +/** + * Returns the user closets error. + * + * @param state - Application state. + * + * @returns User closet error. + */ +export const getUserClosetsError = (state: StoreState) => + getError(getUserClosetsFromReducer(state.users as UsersState)); + +/** + * Returns the loading status of the user closets area. + * + * @param state - Application state. + * + * @returns Loader status. + */ +export const areUserClosetsLoading = (state: StoreState) => + getIsLoading(getUserClosetsFromReducer(state.users as UsersState)); + +/** + * Retrieves if the user closets has been fetched. + * + * Will return true if an user closets request + * has been made that returned either successfully or failed + * and false otherwise. + * + * @param state - Application state. + * + * @returns isFetched status of the user closets. + */ +export const areUserClosetsFetched = (state: StoreState) => + (!!getUserClosets(state) || !!getUserClosetsError(state)) && + !areUserClosetsLoading(state); + +/** + * Returns the user closet items result from the application state. + * + * @param state - Application state. + * + * @returns User closet items result. + */ +export const getUserClosetItemsResult = (state: StoreState) => + getUserClosetItems(getUserClosetsFromReducer(state.users as UsersState)) + .result; + +/** + * Returns the user closet items error. + * + * @param state - Application state. + * + * @returns Error details. + */ +export const getUserClosetItemsError = (state: StoreState) => + getUserClosetItems(getUserClosetsFromReducer(state.users as UsersState)) + .error; + +/** + * Returns the loading status of the user closet items area. + * + * @param state - Application state. + * + * @returns Loader status. + */ +export const areUserClosetItemsLoading = (state: StoreState) => + getUserClosetItems(getUserClosetsFromReducer(state.users as UsersState)) + .isLoading; + +/** + * Retrieves if the user closet items were fetched. + * + * Will return true if an user closet items request + * has been made that returned either successfully or failed + * and false otherwise. + * + * @param state - Application state. + * + * @returns isFetched status of the user closet items. + */ +export const areUserClosetItemsFetched = (state: StoreState) => + (!!getUserClosetItemsResult(state) || !!getUserClosetItemsError(state)) && + !areUserClosetItemsLoading(state); diff --git a/packages/redux/src/users/closets/types/actions.types.ts b/packages/redux/src/users/closets/types/actions.types.ts new file mode 100644 index 000000000..4df9994f2 --- /dev/null +++ b/packages/redux/src/users/closets/types/actions.types.ts @@ -0,0 +1,87 @@ +import type * as actionTypes from '../actionTypes.js'; +import type { Action } from 'redux'; +import type { + BlackoutError, + Closet, + ClosetSummary, +} from '@farfetch/blackout-client'; +import type { ClosetItemAdapted } from './state.types.js'; + +type ClosetPayload = Omit & { + entries: Array; +}; + +// +// Fetch user closet +// +export interface FetchUserClosetItemsFailureAction extends Action { + payload: { error: BlackoutError }; + type: typeof actionTypes.FETCH_USER_CLOSET_ITEMS_FAILURE; +} + +export interface FetchUserClosetItemsRequestAction extends Action { + type: typeof actionTypes.FETCH_USER_CLOSET_ITEMS_REQUEST; +} + +export interface FetchUserClosetItemsSuccessAction extends Action { + payload: ClosetPayload; + type: typeof actionTypes.FETCH_USER_CLOSET_ITEMS_SUCCESS; +} + +export type FetchUserClosetItemsAction = + | FetchUserClosetItemsRequestAction + | FetchUserClosetItemsFailureAction + | FetchUserClosetItemsSuccessAction; + +// +// Fetch user closets +// +export interface FetchUserClosetsFailureAction extends Action { + payload: { error: BlackoutError }; + type: typeof actionTypes.FETCH_USER_CLOSETS_FAILURE; +} + +export interface FetchUserClosetsRequestAction extends Action { + type: typeof actionTypes.FETCH_USER_CLOSETS_REQUEST; +} + +export interface FetchUserClosetsSuccessAction extends Action { + payload: ClosetSummary[]; + type: typeof actionTypes.FETCH_USER_CLOSETS_SUCCESS; +} + +export type FetchUserClosetsAction = + | FetchUserClosetsRequestAction + | FetchUserClosetsFailureAction + | FetchUserClosetsSuccessAction; + +// +// Remove user closet item +// +export interface RemoveUserClosetItemFailureAction extends Action { + payload: { error: BlackoutError }; + type: typeof actionTypes.REMOVE_USER_CLOSET_ITEM_FAILURE; +} + +export interface RemoveUserClosetItemRequestAction extends Action { + type: typeof actionTypes.REMOVE_USER_CLOSET_ITEM_REQUEST; +} + +export interface RemoveUserClosetItemSuccessAction extends Action { + type: typeof actionTypes.REMOVE_USER_CLOSET_ITEM_SUCCESS; +} + +export type RemoveUserClosetItemAction = + | RemoveUserClosetItemRequestAction + | RemoveUserClosetItemFailureAction + | RemoveUserClosetItemSuccessAction; + +/** Actions dispatched when the reset user closets state request is made. */ +export interface ResetUserClosetsStateAction extends Action { + type: typeof actionTypes.RESET_USER_CLOSETS; +} + +/** Actions dispatched when the reset user closet state request is made. */ +export interface ResetUserClosetItemsStateAction extends Action { + type: typeof actionTypes.RESET_USER_CLOSET_ITEMS_STATE; +} diff --git a/packages/redux/src/users/closets/types/index.ts b/packages/redux/src/users/closets/types/index.ts new file mode 100644 index 000000000..592cd7a1b --- /dev/null +++ b/packages/redux/src/users/closets/types/index.ts @@ -0,0 +1,2 @@ +export * from './actions.types.js'; +export * from './state.types.js'; diff --git a/packages/redux/src/users/closets/types/state.types.ts b/packages/redux/src/users/closets/types/state.types.ts new file mode 100644 index 000000000..d1e611fdc --- /dev/null +++ b/packages/redux/src/users/closets/types/state.types.ts @@ -0,0 +1,19 @@ +import type { + Closet, + ClosetItem, + ClosetSummary, +} from '@farfetch/blackout-client'; +import type { ProductImagesAdapted } from '../../../index.js'; +import type { StateWithResult } from '../../../types/subAreaState.types.js'; + +export type ClosetItemAdapted = Omit & { + images: ProductImagesAdapted; +}; + +export type ClosetItemsAdapted = Omit & { + entries: Array; +}; + +export type UserClosetsState = StateWithResult & { + closetItems: StateWithResult; +}; diff --git a/packages/redux/src/users/reducer.ts b/packages/redux/src/users/reducer.ts index 727c4b8d6..ad57abf3a 100644 --- a/packages/redux/src/users/reducer.ts +++ b/packages/redux/src/users/reducer.ts @@ -15,6 +15,9 @@ import benefitsReducer, { entitiesMapper as benefitsEntitiesMapper, INITIAL_STATE as INITIAL_BENEFITS_STATE, } from './benefits/reducer.js'; +import closetsReducer, { + INITIAL_STATE as INITIAL_USER_CLOSETS_STATE, +} from './closets/reducer.js'; import contactsReducer, { entitiesMapper as contactsEntitiesMapper, INITIAL_STATE as INITIAL_CONTACTS_STATE, @@ -47,6 +50,7 @@ export const INITIAL_STATE: UsersState = { attributes: INITIAL_ATTRIBUTES_STATE, authentication: INITIAL_AUTHENTICATION_STATE, benefits: INITIAL_BENEFITS_STATE, + closets: INITIAL_USER_CLOSETS_STATE, contacts: INITIAL_CONTACTS_STATE, creditMovements: INITIAL_CREDITS_STATE.creditMovements, credits: INITIAL_CREDITS_STATE.credits, @@ -215,6 +219,8 @@ export const getAuthentication = ( ): UsersState['authentication'] => state.authentication; export const getPersonalIds = (state: UsersState): UsersState['personalIds'] => state.personalIds; +export const getUserClosets = (state: UsersState): UsersState['closets'] => + state.closets; const reducer: Reducer = combineReducers({ error, @@ -224,6 +230,7 @@ const reducer: Reducer = combineReducers({ attributes: attributesReducer, authentication: authenticationReducer, benefits: benefitsReducer, + closets: closetsReducer, contacts: contactsReducer, creditMovements: creditsReducers.creditMovements, credits: creditsReducers.credits, diff --git a/packages/redux/src/users/selectors.ts b/packages/redux/src/users/selectors.ts index e8ef53269..fae40b547 100644 --- a/packages/redux/src/users/selectors.ts +++ b/packages/redux/src/users/selectors.ts @@ -146,6 +146,7 @@ export * from './addresses/selectors.js'; export * from './attributes/selectors.js'; export * from './authentication/selectors.js'; export * from './benefits/selectors.js'; +export * from './closets/selectors.js'; export * from './contacts/selectors.js'; export * from './credits/selectors.js'; export * from './personalIds/selectors.js'; diff --git a/packages/redux/src/users/types/state.types.ts b/packages/redux/src/users/types/state.types.ts index 4b23f7a03..fa863fb32 100644 --- a/packages/redux/src/users/types/state.types.ts +++ b/packages/redux/src/users/types/state.types.ts @@ -18,6 +18,7 @@ import type { } from '../preferences/types/index.js'; import type { TitlesState } from '../titles/types/index.js'; import type { UserAddressesState } from '../addresses/types/index.js'; +import type { UserClosetsState } from '../closets/types/index.js'; import type { UserPersonalIdsState } from '../personalIds/types/index.js'; import type { UserReturnsState } from '../returns/types/index.js'; @@ -37,4 +38,5 @@ export type UsersState = CombinedState<{ returns: UserReturnsState; titles: TitlesState; updatePreferences: UpdatePreferencesState; + closets: UserClosetsState; }>; diff --git a/tests/__fixtures__/checkout/checkout.fixtures.mts b/tests/__fixtures__/checkout/checkout.fixtures.mts index 2647c015f..ed798af40 100644 --- a/tests/__fixtures__/checkout/checkout.fixtures.mts +++ b/tests/__fixtures__/checkout/checkout.fixtures.mts @@ -1365,6 +1365,16 @@ export const mockInitialState = { result: null, }, }, + closets: { + error: null, + isLoading: false, + result: null, + closetItems: { + error: null, + isLoading: false, + result: null, + }, + }, }, payments: { paymentIntentCharge: { diff --git a/tests/__fixtures__/exchanges/exchanges.fixtures.mts b/tests/__fixtures__/exchanges/exchanges.fixtures.mts index 1857171e4..4d5db7bc5 100644 --- a/tests/__fixtures__/exchanges/exchanges.fixtures.mts +++ b/tests/__fixtures__/exchanges/exchanges.fixtures.mts @@ -100,12 +100,12 @@ export const responses = { { criteria: ExchangeFilterLogicOperatorCriteria.ProductId, comparator: ExchangeFilterLogicOperatorComparator.Equals, - value: '18061196', + values: '18061196', }, { criteria: ExchangeFilterLogicOperatorCriteria.Price, comparator: ExchangeFilterLogicOperatorComparator.LessThanOrEqual, - value: '1.0', + values: '1.0', }, ], }, @@ -113,12 +113,12 @@ export const responses = { { criteria: ExchangeFilterConditionCriteria.ProductId, comparator: ExchangeFilterConditionComparator.Equals, - value: '18061196', + values: '18061196', }, { criteria: ExchangeFilterConditionCriteria.Price, comparator: ExchangeFilterConditionComparator.LessThanOrEqual, - value: '1.0', + values: '1.0', }, ], }, diff --git a/tests/__fixtures__/users/index.mts b/tests/__fixtures__/users/index.mts index 157fce886..c4b1c03e8 100644 --- a/tests/__fixtures__/users/index.mts +++ b/tests/__fixtures__/users/index.mts @@ -10,5 +10,6 @@ export * from './preferences.fixtures.mjs'; export * from './titles.fixtures.mjs'; export * from './userAttribute.fixtures.mjs'; export * from './userAttributes.fixtures.mjs'; +export * from './userCloset.fixtures.mjs'; export * from './userReturns.fixtures.mjs'; export * from './users.fixtures.mjs'; diff --git a/tests/__fixtures__/users/userCloset.fixtures.mts b/tests/__fixtures__/users/userCloset.fixtures.mts new file mode 100644 index 000000000..dad7edc2a --- /dev/null +++ b/tests/__fixtures__/users/userCloset.fixtures.mts @@ -0,0 +1,278 @@ +import { + GenderCode, + ProductVariantAttributeType, + toBlackoutError, +} from '@farfetch/blackout-client'; + +export const closetId = '3455f70e-f756-4ad5-b8e3-46d32ac74def'; +export const closetItemId = '654321'; +export const mockProductImgQueryParam = '?c=2'; + +export const mockGetUserClosetItemsResponse = { + number: 1, + totalPages: 1, + totalItems: 1, + entries: [ + { + id: closetItemId, + orderId: 'ABC123', + merchantId: 123, + productId: 123, + productName: 'product1', + productDescription: 'description1', + attributes: [ + { + type: ProductVariantAttributeType.Size, + value: '10', + description: '', + }, + ], + images: { + images: [ + { + order: 1, + size: '200', + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099951_200.jpg', + }, + { + order: 2, + size: '200', + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099952_200.jpg', + }, + { + order: 3, + size: '200', + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099953_200.jpg', + }, + { + order: 4, + size: '200', + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099954_200.jpg', + }, + { + order: 5, + size: '200', + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099955_200.jpg', + }, + ], + liveModel: { + globalId: '00000000-0000-0000-0000-000000000000', + id: 0, + measurements: [], + name: 'string', + }, + liveModelId: 0, + productSize: '26', + tag: 'string', + }, + categories: [ + { + id: 0, + name: 'string', + parentId: 0, + gender: GenderCode.Woman, + }, + ], + colors: [ + { + color: { + id: 112504, + name: 'Red', + }, + tags: ['MainColor'], + }, + ], + price: { + priceExclTaxes: 0, + priceInclTaxes: 0, + priceInclTaxesWithoutDiscount: 0, + discountExclTaxes: 0, + discountInclTaxes: 0, + discountRate: 0, + taxesRate: 0, + taxesValue: 0, + tags: ['string'], + formattedPrice: 'string', + formattedPriceWithoutDiscount: 'string', + formattedPriceWithoutCurrency: 'string', + formattedPriceWithoutDiscountAndCurrency: 'string', + taxType: 'string', + }, + purchasePrice: { + priceExclTaxes: 0, + priceInclTaxes: 0, + priceInclTaxesWithoutDiscount: 0, + discountExclTaxes: 0, + discountInclTaxes: 0, + discountRate: 0, + taxesRate: 0, + taxesValue: 0, + tags: ['string'], + formattedPrice: 'string', + formattedPriceWithoutDiscount: 'string', + formattedPriceWithoutCurrency: 'string', + formattedPriceWithoutDiscountAndCurrency: 'string', + taxType: 'string', + }, + brand: { + description: 'Yves Saint Laurent Beauty', + id: 45511224, + name: 'Yves Saint Laurent Beauty', + priceType: 0, + isActive: true, + }, + customAttributes: '', + isAvailable: true, + createdDate: '2023-08-17T11:52:35.941Z', + }, + ], +}; + +export const getExpectedUserClosetItemsPayload = ( + productImgQueryParam = '', +) => { + return { + number: 1, + totalPages: 1, + totalItems: 1, + entries: [ + { + id: closetItemId, + orderId: 'ABC123', + merchantId: 123, + productId: 123, + productName: 'product1', + productDescription: 'description1', + attributes: [ + { + type: ProductVariantAttributeType.Size, + value: '10', + description: '', + }, + ], + images: [ + { + order: 1, + size: '200', + sources: { + 200: `https://cdn-images.farfetch.com/12/09/16/86/12091686_11099951_200.jpg${productImgQueryParam}`, + }, + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099951_200.jpg', + }, + { + order: 2, + size: '200', + sources: { + 200: `https://cdn-images.farfetch.com/12/09/16/86/12091686_11099952_200.jpg${productImgQueryParam}`, + }, + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099952_200.jpg', + }, + { + order: 3, + size: '200', + sources: { + 200: `https://cdn-images.farfetch.com/12/09/16/86/12091686_11099953_200.jpg${productImgQueryParam}`, + }, + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099953_200.jpg', + }, + { + order: 4, + size: '200', + sources: { + 200: `https://cdn-images.farfetch.com/12/09/16/86/12091686_11099954_200.jpg${productImgQueryParam}`, + }, + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099954_200.jpg', + }, + { + order: 5, + size: '200', + sources: { + 200: `https://cdn-images.farfetch.com/12/09/16/86/12091686_11099955_200.jpg${productImgQueryParam}`, + }, + url: 'https://cdn-images.farfetch.com/12/09/16/86/12091686_11099955_200.jpg', + }, + ], + categories: [ + { + id: 0, + name: 'string', + parentId: 0, + gender: GenderCode.Woman, + }, + ], + colors: [ + { + color: { + id: 112504, + name: 'Red', + }, + tags: ['MainColor'], + }, + ], + price: { + priceExclTaxes: 0, + priceInclTaxes: 0, + priceInclTaxesWithoutDiscount: 0, + discountExclTaxes: 0, + discountInclTaxes: 0, + discountRate: 0, + taxesRate: 0, + taxesValue: 0, + tags: ['string'], + formattedPrice: 'string', + formattedPriceWithoutDiscount: 'string', + formattedPriceWithoutCurrency: 'string', + formattedPriceWithoutDiscountAndCurrency: 'string', + taxType: 'string', + }, + purchasePrice: { + priceExclTaxes: 0, + priceInclTaxes: 0, + priceInclTaxesWithoutDiscount: 0, + discountExclTaxes: 0, + discountInclTaxes: 0, + discountRate: 0, + taxesRate: 0, + taxesValue: 0, + tags: ['string'], + formattedPrice: 'string', + formattedPriceWithoutDiscount: 'string', + formattedPriceWithoutCurrency: 'string', + formattedPriceWithoutDiscountAndCurrency: 'string', + taxType: 'string', + }, + brand: { + description: 'Yves Saint Laurent Beauty', + id: 45511224, + name: 'Yves Saint Laurent Beauty', + priceType: 0, + isActive: true, + }, + customAttributes: '', + isAvailable: true, + createdDate: '2023-08-17T11:52:35.941Z', + }, + ], + }; +}; + +export const mockGetUserClosetsResponse = [ + { + id: closetId, + createdDate: '2023-08-17T11:52:35.941Z', + totalItems: 1, + }, +]; + +export const mockState = { + closets: { + error: toBlackoutError(new Error('error')), + isLoading: false, + result: mockGetUserClosetsResponse, + closetItems: { + error: toBlackoutError(new Error('error')), + isLoading: false, + result: getExpectedUserClosetItemsPayload(mockProductImgQueryParam), + }, + }, +}; diff --git a/tests/__fixtures__/users/users.fixtures.mts b/tests/__fixtures__/users/users.fixtures.mts index ebf631957..58ea4cc25 100644 --- a/tests/__fixtures__/users/users.fixtures.mts +++ b/tests/__fixtures__/users/users.fixtures.mts @@ -193,6 +193,16 @@ export const mockUserInitialState: UsersState = { result: null, }, }, + closets: { + error: null, + isLoading: false, + result: null, + closet: { + error: null, + isLoading: false, + result: null, + }, + }, }; export const mockGuestUserEntities = {