diff --git a/packages/favorites/src/lib/Favorites.test.tsx b/packages/favorites/src/lib/Favorites.test.tsx index b0ccfc488..5b3b7b2f3 100644 --- a/packages/favorites/src/lib/Favorites.test.tsx +++ b/packages/favorites/src/lib/Favorites.test.tsx @@ -3,6 +3,7 @@ import { render, waitFor, fireEvent, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; // eslint-disable-next-line @nx/enforce-module-boundaries import { server } from '@availity/mock/src/lib/server'; +import { http, HttpResponse } from 'msw'; import avMessages from '@availity/message-core'; import { FavoritesProvider } from './Favorites'; import { FavoriteHeart } from './FavoriteHeart'; @@ -15,8 +16,9 @@ const domain = () => { } if (window.location.hostname) { - return `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : '' - }`; + return `${window.location.protocol}//${window.location.hostname}${ + window.location.port ? `:${window.location.port}` : '' + }`; } return '*'; @@ -274,5 +276,81 @@ describe('Favorites', () => { }); }); + // The Settings API PUT responds with the saved settings object ({ favorites: [...] }), + // NOT wrapped in `settings: []` like the GET list response. These tests use a fixed + // response (not an echo of the request) so the mock cannot hide a response-shape mismatch. + describe('Settings PUT response shape', () => { + it('should add favorite when PUT responds with an unwrapped { favorites } body', async () => { + server.use( + http.put('/api/utils/v1/settings', () => + HttpResponse.json( + { + favorites: [ + { id: '123', pos: 0 }, + { id: '456', pos: 1 }, + { id: '789', pos: 2 }, + ], + }, + { status: 200 } + ) + ) + ); + const user = userEvent.setup(); + + const { container } = render( + + + + + + ); + + const heart = container.querySelector('#av-favorite-heart-789'); + + await waitFor(() => expect(heart).not.toBeChecked()); + + if (heart) await user.click(heart); + + await waitFor(() => expect(heart).toBeChecked()); + expect(avMessages.send).toHaveBeenCalledWith({ + event: 'av:favorites:update', + favorites: [ + { id: '123', pos: 0 }, + { id: '456', pos: 1 }, + { id: '789', pos: 2 }, + ], + }); + }); + + it('should remove favorite when PUT responds with an unwrapped { favorites } body', async () => { + server.use( + http.put('/api/utils/v1/settings', () => + HttpResponse.json({ favorites: [{ id: '456', pos: 1 }] }, { status: 200 }) + ) + ); + const user = userEvent.setup(); + + const { container } = render( + + + + + + ); + + const heart = container.querySelector('#av-favorite-heart-123'); + + await waitFor(() => expect(heart).toBeChecked()); + + if (heart) await user.click(heart); + + await waitFor(() => expect(heart).not.toBeChecked()); + + expect(avMessages.send).toHaveBeenCalledWith({ + event: 'av:favorites:update', + favorites: [{ id: '456', pos: 1 }], + }); + }); + }); }); diff --git a/packages/favorites/src/lib/utils.ts b/packages/favorites/src/lib/utils.ts index 8b7729a87..1fb39b0ab 100644 --- a/packages/favorites/src/lib/utils.ts +++ b/packages/favorites/src/lib/utils.ts @@ -13,31 +13,38 @@ export const isFavorite = (arg: Favorite) => Boolean(typeof arg?.id === 'string' export const validateFavorites = (unvalidatedFavorites: Favorite[]) => { const validatedFavorites = Array.isArray(unvalidatedFavorites) ? unvalidatedFavorites?.filter(isFavorite) : []; return validatedFavorites; -} +}; type MutationVariables = { favorites: Favorite[]; targetFavoriteId: string; -} +}; -interface FavoritesSettings { +/** Settings API GET response: the saved settings are wrapped in a `settings` list. */ +interface FavoritesGetResponse { settings: { favorites: Favorite[] }[]; } -const submit = async ({ favorites, targetFavoriteId }: MutationVariables, applicationId: string) => { - const response = await avSettingsApi.setApplication(applicationId, { favorites }); - return { favorites: response.data.settings[0].favorites, targetFavoriteId }; +/** Settings API PUT response: the saved settings object is returned directly, NOT wrapped in `settings`. */ +interface FavoritesPutResponse { + favorites: Favorite[]; } +const submit = async ({ favorites, targetFavoriteId }: MutationVariables, applicationId: string) => { + const response = await avSettingsApi.setApplication(applicationId, { favorites }); + return { favorites: response.data.favorites, targetFavoriteId }; +}; + const getFavorites = async (applicationId: string) => { - const result = await avSettingsApi.getApplication(applicationId); + const result = await avSettingsApi.getApplication(applicationId); const unvalidatedFavorites = result?.data?.settings?.[0]?.favorites; const validatedFavorites = validateFavorites(unvalidatedFavorites); return validatedFavorites; }; -export const useFavoritesQuery = (enabled: boolean, applicationId: string): UseQueryResult => useQuery({ queryKey: ['favorites'], queryFn: () => getFavorites(applicationId), enabled }); +export const useFavoritesQuery = (enabled: boolean, applicationId: string): UseQueryResult => + useQuery({ queryKey: ['favorites'], queryFn: () => getFavorites(applicationId), enabled }); type MutationOptions = { onMutationStart?: (targetFavoriteId: string) => void; @@ -46,13 +53,14 @@ type MutationOptions = { export const useSubmitFavorites = ({ onMutationStart }: MutationOptions, applicationId: string) => { const queryClient = useQueryClient(); const { mutateAsync: submitFavorites, ...rest } = useMutation({ - mutationFn: ({ favorites, targetFavoriteId }: MutationVariables) => submit({ favorites, targetFavoriteId }, applicationId), + mutationFn: ({ favorites, targetFavoriteId }: MutationVariables) => + submit({ favorites, targetFavoriteId }, applicationId), onMutate(variables) { onMutationStart?.(variables.targetFavoriteId); }, onSuccess(data) { queryClient.setQueryData(['favorites'], data.favorites); - } + }, }); return { submitFavorites, ...rest }; }; diff --git a/packages/mock/src/lib/handlers.ts b/packages/mock/src/lib/handlers.ts index b40d06f0a..a8074969b 100644 --- a/packages/mock/src/lib/handlers.ts +++ b/packages/mock/src/lib/handlers.ts @@ -96,7 +96,8 @@ export const handlers = [ if (params.applicationId === 'Gateway-AvNavigation') { await delay(defaultDelay); return HttpResponse.json(settings, { status: 200 }); - } if (params.applicationId === 'AVATAR') { + } + if (params.applicationId === 'AVATAR') { await delay(defaultDelay); return HttpResponse.json(avatarSettings, { status: 200 }); } @@ -105,7 +106,8 @@ export const handlers = [ const body = await context.request.json(); await delay(defaultDelay); - return HttpResponse.json({ settings: [body] }, { status: 201 }); + // The real Settings API PUT returns the saved settings object directly (not wrapped in `settings`) + return HttpResponse.json(body, { status: 201 }); }), // Codes @@ -365,42 +367,41 @@ export const handlers = [ }; if (password) { - return password === "1234" ? - new HttpResponse(null, { - status: 200, - headers: { - 'av-scan-bytes': fileSize, - 'av-scan-result': 'accepted', - 'cache-control': 'no-store', - 'decryption-result': 'accepted', - references: `["approved/${params.bucket}/${params.location}"]`, - 's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`, - 'transfer-encoding': 'chunked', - 'tus-resumable': '1.0.0', - 'upload-length': fileSize, - 'upload-metadata': metadata, - 'upload-offset': fileSize, - 'upload-result': 'accepted', - }, - }) - : - new HttpResponse(null, { - status: 200, - headers: { - 'av-scan-bytes': fileSize, - 'av-scan-result': 'accepted', - 'cache-control': 'no-store', - 'decryption-result': 'rejected', - references: `["approved/${params.bucket}/${params.location}"]`, - 's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`, - 'transfer-encoding': 'chunked', - 'tus-resumable': '1.0.0', - 'upload-length': fileSize, - 'upload-metadata': metadata, - 'upload-offset': fileSize, - 'upload-result': 'encrypted', - }, - }); + return password === '1234' + ? new HttpResponse(null, { + status: 200, + headers: { + 'av-scan-bytes': fileSize, + 'av-scan-result': 'accepted', + 'cache-control': 'no-store', + 'decryption-result': 'accepted', + references: `["approved/${params.bucket}/${params.location}"]`, + 's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`, + 'transfer-encoding': 'chunked', + 'tus-resumable': '1.0.0', + 'upload-length': fileSize, + 'upload-metadata': metadata, + 'upload-offset': fileSize, + 'upload-result': 'accepted', + }, + }) + : new HttpResponse(null, { + status: 200, + headers: { + 'av-scan-bytes': fileSize, + 'av-scan-result': 'accepted', + 'cache-control': 'no-store', + 'decryption-result': 'rejected', + references: `["approved/${params.bucket}/${params.location}"]`, + 's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`, + 'transfer-encoding': 'chunked', + 'tus-resumable': '1.0.0', + 'upload-length': fileSize, + 'upload-metadata': metadata, + 'upload-offset': fileSize, + 'upload-result': 'encrypted', + }, + }); } await delay(1000);