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);