mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Refactored data loading to be more performant in admin-x-activitypub (#21770)
refs [AP-614](https://linear.app/ghost/issue/AP-614/investigate-client-loading-issues) Refactored data loading to be more performant in admin-x-activitypub: - Utilised tanstack query for suggested profiles instead of rolling our own custom hook. This allows us to utilise tanstack for caching - Refactored how data is loaded for the Inbox / Feed view - Queries are no longer cleared when switching between the 2 layouts - Refactored how activity data is key'd to make caching more deterministic as well as allow mutating a specific activity cache (i.e when adding a note, ensure it only gets added the the feed activities) - Removed redundant `excludeNonFollowers` parameter - This is no longer used by the API
This commit is contained in:
parent
995edaa966
commit
04ed106c73
10 changed files with 65 additions and 144 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@tryghost/admin-x-activitypub",
|
"name": "@tryghost/admin-x-activitypub",
|
||||||
"version": "0.3.30",
|
"version": "0.3.31",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -10,17 +10,14 @@ const MainContent = () => {
|
||||||
switch (mainRoute) {
|
switch (mainRoute) {
|
||||||
case 'search':
|
case 'search':
|
||||||
return <Search />;
|
return <Search />;
|
||||||
break;
|
|
||||||
case 'activity':
|
case 'activity':
|
||||||
return <Activities />;
|
return <Activities />;
|
||||||
break;
|
|
||||||
case 'profile':
|
case 'profile':
|
||||||
return <Profile />;
|
return <Profile />;
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
const layout = (mainRoute === 'inbox' || mainRoute === '') ? 'inbox' : 'feed';
|
const layout = (mainRoute === 'inbox' || mainRoute === '') ? 'inbox' : 'feed';
|
||||||
|
|
||||||
return <Inbox layout={layout} />;
|
return <Inbox layout={layout} />;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -213,7 +213,6 @@ export class ActivityPubAPI {
|
||||||
async getActivities(
|
async getActivities(
|
||||||
includeOwn: boolean = false,
|
includeOwn: boolean = false,
|
||||||
includeReplies: boolean = false,
|
includeReplies: boolean = false,
|
||||||
excludeNonFollowers: boolean = false,
|
|
||||||
filter: {type?: string[]} | null = null,
|
filter: {type?: string[]} | null = null,
|
||||||
cursor?: string
|
cursor?: string
|
||||||
): Promise<{data: Activity[], next: string | null}> {
|
): Promise<{data: Activity[], next: string | null}> {
|
||||||
|
@ -227,9 +226,6 @@ export class ActivityPubAPI {
|
||||||
if (includeReplies) {
|
if (includeReplies) {
|
||||||
url.searchParams.set('includeReplies', includeReplies.toString());
|
url.searchParams.set('includeReplies', includeReplies.toString());
|
||||||
}
|
}
|
||||||
if (excludeNonFollowers) {
|
|
||||||
url.searchParams.set('excludeNonFollowers', excludeNonFollowers.toString());
|
|
||||||
}
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
url.searchParams.set('filter', JSON.stringify(filter));
|
url.searchParams.set('filter', JSON.stringify(filter));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import ViewProfileModal from './modals/ViewProfileModal';
|
||||||
|
|
||||||
import getUsername from '../utils/get-username';
|
import getUsername from '../utils/get-username';
|
||||||
import stripHtml from '../utils/strip-html';
|
import stripHtml from '../utils/strip-html';
|
||||||
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
|
import {GET_ACTIVITIES_QUERY_KEY_NOTIFICATIONS, useActivitiesForUser} from '../hooks/useActivityPubQueries';
|
||||||
|
|
||||||
interface ActivitiesProps {}
|
interface ActivitiesProps {}
|
||||||
|
|
||||||
|
@ -97,10 +97,13 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
||||||
includeReplies: true,
|
includeReplies: true,
|
||||||
filter: {
|
filter: {
|
||||||
type: ['Follow', 'Like', `Create:Note:isReplyToOwn`]
|
type: ['Follow', 'Like', `Create:Note:isReplyToOwn`]
|
||||||
}
|
},
|
||||||
|
key: GET_ACTIVITIES_QUERY_KEY_NOTIFICATIONS
|
||||||
});
|
});
|
||||||
const {data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading} = getActivitiesQuery;
|
const {data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading} = getActivitiesQuery;
|
||||||
const activities = (data?.pages.flatMap(page => page.data) ?? []);
|
const activities = (data?.pages.flatMap(page => page.data) ?? [])
|
||||||
|
// If there somehow are duplicate activities, filter them out so the list rendering doesn't break
|
||||||
|
.filter((activity, index, self) => index === self.findIndex(a => a.id === activity.id));
|
||||||
|
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
|
@ -10,11 +10,16 @@ import Separator from './global/Separator';
|
||||||
import ViewProfileModal from './modals/ViewProfileModal';
|
import ViewProfileModal from './modals/ViewProfileModal';
|
||||||
import getName from '../utils/get-name';
|
import getName from '../utils/get-name';
|
||||||
import getUsername from '../utils/get-username';
|
import getUsername from '../utils/get-username';
|
||||||
import useSuggestedProfiles from '../hooks/useSuggestedProfiles';
|
|
||||||
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||||
import {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system';
|
import {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system';
|
||||||
|
import {
|
||||||
|
GET_ACTIVITIES_QUERY_KEY_FEED,
|
||||||
|
GET_ACTIVITIES_QUERY_KEY_INBOX,
|
||||||
|
useActivitiesForUser,
|
||||||
|
useSuggestedProfiles,
|
||||||
|
useUserDataForUser
|
||||||
|
} from '../hooks/useActivityPubQueries';
|
||||||
import {handleViewContent} from '../utils/content-handlers';
|
import {handleViewContent} from '../utils/content-handlers';
|
||||||
import {useActivitiesForUser, useUserDataForUser} from '../hooks/useActivityPubQueries';
|
|
||||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
type Layout = 'inbox' | 'feed';
|
type Layout = 'inbox' | 'feed';
|
||||||
|
@ -24,6 +29,9 @@ interface InboxProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Inbox: React.FC<InboxProps> = ({layout}) => {
|
const Inbox: React.FC<InboxProps> = ({layout}) => {
|
||||||
|
const {updateRoute} = useRouting();
|
||||||
|
|
||||||
|
// Initialise activities for the inbox or feed
|
||||||
const typeFilter = layout === 'inbox'
|
const typeFilter = layout === 'inbox'
|
||||||
? ['Create:Article']
|
? ['Create:Article']
|
||||||
: ['Create:Note', 'Announce:Note'];
|
: ['Create:Note', 'Announce:Note'];
|
||||||
|
@ -31,20 +39,26 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
|
||||||
const {getActivitiesQuery, updateActivity} = useActivitiesForUser({
|
const {getActivitiesQuery, updateActivity} = useActivitiesForUser({
|
||||||
handle: 'index',
|
handle: 'index',
|
||||||
includeOwn: true,
|
includeOwn: true,
|
||||||
excludeNonFollowers: true,
|
|
||||||
filter: {
|
filter: {
|
||||||
type: typeFilter
|
type: typeFilter
|
||||||
}
|
},
|
||||||
|
key: layout === 'inbox' ? GET_ACTIVITIES_QUERY_KEY_INBOX : GET_ACTIVITIES_QUERY_KEY_FEED
|
||||||
});
|
});
|
||||||
|
|
||||||
const {data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading} = getActivitiesQuery;
|
const {data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading} = getActivitiesQuery;
|
||||||
|
|
||||||
const {updateRoute} = useRouting();
|
const activities = (data?.pages.flatMap(page => page.data) ?? [])
|
||||||
|
// If there somehow are duplicate activities, filter them out so the list rendering doesn't break
|
||||||
|
.filter((activity, index, self) => index === self.findIndex(a => a.id === activity.id))
|
||||||
|
// Filter out replies
|
||||||
|
.filter((activity) => {
|
||||||
|
return !activity.object.inReplyTo;
|
||||||
|
});
|
||||||
|
|
||||||
const {suggested, isLoadingSuggested} = useSuggestedProfiles();
|
// Initialise suggested profiles
|
||||||
|
const {suggestedProfilesQuery} = useSuggestedProfiles('index', 3);
|
||||||
const activities = (data?.pages.flatMap(page => page.data) ?? []).filter((activity) => {
|
const {data: suggestedData, isLoading: isLoadingSuggested} = suggestedProfilesQuery;
|
||||||
return !activity.object.inReplyTo;
|
const suggested = suggestedData || [];
|
||||||
});
|
|
||||||
|
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
|
@ -13,8 +13,8 @@ import NiceModal from '@ebay/nice-modal-react';
|
||||||
import ViewProfileModal from './modals/ViewProfileModal';
|
import ViewProfileModal from './modals/ViewProfileModal';
|
||||||
|
|
||||||
import Separator from './global/Separator';
|
import Separator from './global/Separator';
|
||||||
import useSuggestedProfiles from '../hooks/useSuggestedProfiles';
|
|
||||||
import {useSearchForUser} from '../hooks/useActivityPubQueries';
|
import {useSearchForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries';
|
||||||
|
|
||||||
interface SearchResultItem {
|
interface SearchResultItem {
|
||||||
actor: ActorProperties;
|
actor: ActorProperties;
|
||||||
|
@ -122,7 +122,9 @@ interface SearchProps {}
|
||||||
|
|
||||||
const Search: React.FC<SearchProps> = ({}) => {
|
const Search: React.FC<SearchProps> = ({}) => {
|
||||||
// Initialise suggested profiles
|
// Initialise suggested profiles
|
||||||
const {suggested, isLoadingSuggested, updateSuggestedProfile} = useSuggestedProfiles(6);
|
const {suggestedProfilesQuery, updateSuggestedProfile} = useSuggestedProfiles('index', 6);
|
||||||
|
const {data: suggestedData, isLoading: isLoadingSuggested} = suggestedProfilesQuery;
|
||||||
|
const suggested = suggestedData || [];
|
||||||
|
|
||||||
// Initialise search query
|
// Initialise search query
|
||||||
const queryInputRef = useRef<HTMLInputElement>(null);
|
const queryInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import MainHeader from './MainHeader';
|
import MainHeader from './MainHeader';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Button} from '@tryghost/admin-x-design-system';
|
import {Button} from '@tryghost/admin-x-design-system';
|
||||||
import {useQueryClient} from '@tanstack/react-query';
|
|
||||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
interface MainNavigationProps {
|
interface MainNavigationProps {
|
||||||
|
@ -10,15 +9,6 @@ interface MainNavigationProps {
|
||||||
|
|
||||||
const MainNavigation: React.FC<MainNavigationProps> = ({page}) => {
|
const MainNavigation: React.FC<MainNavigationProps> = ({page}) => {
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const handleRouteChange = (newRoute: string) => {
|
|
||||||
queryClient.removeQueries({
|
|
||||||
queryKey: ['activities:index']
|
|
||||||
});
|
|
||||||
|
|
||||||
updateRoute(newRoute);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainHeader>
|
<MainHeader>
|
||||||
|
@ -27,13 +17,13 @@ const MainNavigation: React.FC<MainNavigationProps> = ({page}) => {
|
||||||
className={`${page === 'inbox' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
|
className={`${page === 'inbox' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
|
||||||
label='Inbox'
|
label='Inbox'
|
||||||
unstyled
|
unstyled
|
||||||
onClick={() => handleRouteChange('inbox')}
|
onClick={() => updateRoute('inbox')}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={`${page === 'feed' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
|
className={`${page === 'feed' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
|
||||||
label='Feed'
|
label='Feed'
|
||||||
unstyled
|
unstyled
|
||||||
onClick={() => handleRouteChange('feed')}
|
onClick={() => updateRoute('feed')}
|
||||||
/>
|
/>
|
||||||
<Button className={`${page === 'activities' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Notifications' unstyled onClick={() => updateRoute('activity')} />
|
<Button className={`${page === 'activities' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Notifications' unstyled onClick={() => updateRoute('activity')} />
|
||||||
<Button className={`${page === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Search' unstyled onClick={() => updateRoute('search')} />
|
<Button className={`${page === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Search' unstyled onClick={() => updateRoute('search')} />
|
||||||
|
|
|
@ -258,20 +258,24 @@ export function useFollow(handle: string, onSuccess: () => void, onError: () =>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const GET_ACTIVITIES_QUERY_KEY_INBOX = 'inbox';
|
||||||
|
export const GET_ACTIVITIES_QUERY_KEY_FEED = 'feed';
|
||||||
|
export const GET_ACTIVITIES_QUERY_KEY_NOTIFICATIONS = 'notifications';
|
||||||
|
|
||||||
export function useActivitiesForUser({
|
export function useActivitiesForUser({
|
||||||
handle,
|
handle,
|
||||||
includeOwn = false,
|
includeOwn = false,
|
||||||
includeReplies = false,
|
includeReplies = false,
|
||||||
excludeNonFollowers = false,
|
filter = null,
|
||||||
filter = null
|
key = null
|
||||||
}: {
|
}: {
|
||||||
handle: string;
|
handle: string;
|
||||||
includeOwn?: boolean;
|
includeOwn?: boolean;
|
||||||
includeReplies?: boolean;
|
includeReplies?: boolean;
|
||||||
excludeNonFollowers?: boolean;
|
|
||||||
filter?: {type?: string[]} | null;
|
filter?: {type?: string[]} | null;
|
||||||
|
key?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const queryKey = [`activities:${handle}`, {includeOwn, includeReplies, filterTypes: filter?.type}];
|
const queryKey = [`activities:${handle}`, key, {includeOwn, includeReplies, filter}];
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const getActivitiesQuery = useInfiniteQuery({
|
const getActivitiesQuery = useInfiniteQuery({
|
||||||
|
@ -279,7 +283,7 @@ export function useActivitiesForUser({
|
||||||
async queryFn({pageParam}: {pageParam?: string}) {
|
async queryFn({pageParam}: {pageParam?: string}) {
|
||||||
const siteUrl = await getSiteUrl();
|
const siteUrl = await getSiteUrl();
|
||||||
const api = createActivityPubAPI(handle, siteUrl);
|
const api = createActivityPubAPI(handle, siteUrl);
|
||||||
return api.getActivities(includeOwn, includeReplies, excludeNonFollowers, filter, pageParam);
|
return api.getActivities(includeOwn, includeReplies, filter, pageParam);
|
||||||
},
|
},
|
||||||
getNextPageParam(prevPage) {
|
getNextPageParam(prevPage) {
|
||||||
return prevPage.next;
|
return prevPage.next;
|
||||||
|
@ -346,9 +350,19 @@ export function useSearchForUser(handle: string, query: string) {
|
||||||
return {searchQuery, updateProfileSearchResult};
|
return {searchQuery, updateProfileSearchResult};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSuggestedProfiles(handle: string, handles: string[]) {
|
export function useSuggestedProfiles(handle: string, limit = 3) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const queryKey = ['profiles', {handles}];
|
const queryKey = ['profiles', limit];
|
||||||
|
|
||||||
|
const suggestedHandles = [
|
||||||
|
'@index@activitypub.ghost.org',
|
||||||
|
'@index@john.onolan.org',
|
||||||
|
'@index@www.coffeeandcomplexity.com',
|
||||||
|
'@index@ghost.codenamejimmy.com',
|
||||||
|
'@index@www.syphoncontinuity.com',
|
||||||
|
'@index@www.cosmico.org',
|
||||||
|
'@index@silverhuang.com'
|
||||||
|
];
|
||||||
|
|
||||||
const suggestedProfilesQuery = useQuery({
|
const suggestedProfilesQuery = useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
|
@ -357,7 +371,10 @@ export function useSuggestedProfiles(handle: string, handles: string[]) {
|
||||||
const api = createActivityPubAPI(handle, siteUrl);
|
const api = createActivityPubAPI(handle, siteUrl);
|
||||||
|
|
||||||
return Promise.allSettled(
|
return Promise.allSettled(
|
||||||
handles.map(h => api.getProfile(h))
|
suggestedHandles
|
||||||
|
.sort(() => Math.random() - 0.5)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(suggestedHandle => api.getProfile(suggestedHandle))
|
||||||
).then((results) => {
|
).then((results) => {
|
||||||
return results
|
return results
|
||||||
.filter((result): result is PromiseFulfilledResult<Profile> => result.status === 'fulfilled')
|
.filter((result): result is PromiseFulfilledResult<Profile> => result.status === 'fulfilled')
|
||||||
|
@ -494,7 +511,7 @@ export function useNoteMutationForUser(handle: string) {
|
||||||
return [activity, ...current];
|
return [activity, ...current];
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.setQueriesData([`activities:${handle}`], (current?: {pages: {data: Activity[]}[]}) => {
|
queryClient.setQueriesData([`activities:${handle}`, GET_ACTIVITIES_QUERY_KEY_FEED], (current?: {pages: {data: Activity[]}[]}) => {
|
||||||
if (current === undefined) {
|
if (current === undefined) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {useMemo} from 'react';
|
|
||||||
import {useSuggestedProfiles as useSuggestedProfilesQuery} from '../hooks/useActivityPubQueries';
|
|
||||||
|
|
||||||
export const SUGGESTED_HANDLES = [
|
|
||||||
'@index@activitypub.ghost.org',
|
|
||||||
'@index@john.onolan.org',
|
|
||||||
'@index@www.coffeeandcomplexity.com',
|
|
||||||
'@index@ghost.codenamejimmy.com',
|
|
||||||
'@index@www.syphoncontinuity.com',
|
|
||||||
'@index@www.cosmico.org',
|
|
||||||
'@index@silverhuang.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
const useSuggestedProfiles = (limit = 3) => {
|
|
||||||
const handles = useMemo(() => {
|
|
||||||
return SUGGESTED_HANDLES
|
|
||||||
.sort(() => Math.random() - 0.5)
|
|
||||||
.slice(0, limit);
|
|
||||||
}, [limit]);
|
|
||||||
|
|
||||||
const {suggestedProfilesQuery, updateSuggestedProfile} = useSuggestedProfilesQuery('index', handles);
|
|
||||||
const {data: suggested = [], isLoading: isLoadingSuggested} = suggestedProfilesQuery;
|
|
||||||
|
|
||||||
return {suggested, isLoadingSuggested, updateSuggestedProfile};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSuggestedProfiles;
|
|
|
@ -1,71 +0,0 @@
|
||||||
import {Mock, vi} from 'vitest';
|
|
||||||
import {renderHook} from '@testing-library/react';
|
|
||||||
import type {UseQueryResult} from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import * as useActivityPubQueries from '../../../src/hooks/useActivityPubQueries';
|
|
||||||
import useSuggestedProfiles, {SUGGESTED_HANDLES} from '../../../src/hooks/useSuggestedProfiles';
|
|
||||||
import type{Profile} from '../../../src/api/activitypub';
|
|
||||||
|
|
||||||
vi.mock('../../../src/hooks/useActivityPubQueries');
|
|
||||||
|
|
||||||
describe('useSuggestedProfiles', function () {
|
|
||||||
let mockUpdateSuggestedProfile: Mock;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockUpdateSuggestedProfile = vi.fn();
|
|
||||||
|
|
||||||
vi.mocked(useActivityPubQueries.useSuggestedProfiles).mockImplementation((handle, handles) => {
|
|
||||||
// We expect the handle to be 'index', throw if anything else
|
|
||||||
if (handle !== 'index') {
|
|
||||||
throw new Error(`Expected handle to be: [index], got: [${handle}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the mocked query result making sure that the data is the
|
|
||||||
// same as the handles passed in. For the purposes of this test,
|
|
||||||
// we don't need to test the internals of useSuggestedProfilesQuery
|
|
||||||
return {
|
|
||||||
suggestedProfilesQuery: {
|
|
||||||
data: handles,
|
|
||||||
isLoading: false
|
|
||||||
} as unknown as UseQueryResult<Profile[], unknown>,
|
|
||||||
updateSuggestedProfile: mockUpdateSuggestedProfile
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the default number of suggested profiles', function () {
|
|
||||||
const {result} = renderHook(() => useSuggestedProfiles());
|
|
||||||
|
|
||||||
// Check that the correct number of suggested profiles are returned
|
|
||||||
expect(result.current.suggested.length).toEqual(3);
|
|
||||||
|
|
||||||
// Check that the suggested profiles are in the SUGGESTED_HANDLES array
|
|
||||||
result.current.suggested.forEach((suggested) => {
|
|
||||||
expect(SUGGESTED_HANDLES).toContain(suggested);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the specified number of suggested profiles', function () {
|
|
||||||
const {result} = renderHook(() => useSuggestedProfiles(5));
|
|
||||||
|
|
||||||
// Assert that the correct number of suggested profiles are returned
|
|
||||||
expect(result.current.suggested.length).toEqual(5);
|
|
||||||
|
|
||||||
// Assert that the suggested profiles are in the SUGGESTED_HANDLES array
|
|
||||||
result.current.suggested.forEach((suggested) => {
|
|
||||||
expect(SUGGESTED_HANDLES).toContain(suggested);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a loading state', function () {
|
|
||||||
const {result} = renderHook(() => useSuggestedProfiles());
|
|
||||||
|
|
||||||
expect(result.current.isLoadingSuggested).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a function to update a suggested profile', function () {
|
|
||||||
const {result} = renderHook(() => useSuggestedProfiles());
|
|
||||||
|
|
||||||
expect(result.current.updateSuggestedProfile).toEqual(mockUpdateSuggestedProfile);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Reference in a new issue