Ghost/apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx
Jono M a8083960d8
Added test examples to AdminX demo (#19116)
refs https://github.com/TryGhost/Product/issues/4182

Updated framework to include shared test config for easier app setup.
2023-11-23 12:59:48 +00:00

459 lines
17 KiB
TypeScript

import {InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {act, renderHook, waitFor} from '@testing-library/react';
import React, {ReactNode} from 'react';
import FrameworkProvider from '../../../../src/providers/FrameworkProvider';
import {createInfiniteQuery, createMutation, createPaginatedQuery, createQuery, createQueryWithId} from '../../../../src/utils/api/hooks';
import {withMockFetch} from '../../../utils/mockFetch';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false
}
}
});
const wrapper: React.FC<{ children: ReactNode }> = ({children}) => (
<FrameworkProvider
externalNavigate={() => {}}
ghostVersion='5.x'
sentryDSN=''
unsplashConfig={{
Authorization: '',
'Accept-Version': '',
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': true
}}
onDelete={() => {}}
onInvalidate={() => {}}
onUpdate={() => {}}
>
{/* Being nested, this overrides the default QueryClientProvider from the framework */}
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</FrameworkProvider>
);
describe('API hooks', function () {
describe('createQuery', function () {
afterEach(function () {
queryClient.clear();
});
it('makes an API request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/'
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'GET',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
});
});
it('sends default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?a=%3F');
});
});
it('can override default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery({searchParams: {b: '1'}}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?b=1');
});
});
it('can transform return data', async function () {
await withMockFetch({json: {test: 1}}, async () => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/',
returnData: data => (data as {test: number}).test + 1
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual(2);
});
});
});
describe('createPaginatedQuery', function () {
afterEach(function () {
queryClient.clear();
});
it('makes a paginated API request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/'
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/?page=1', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'GET',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
});
});
it('sends default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?a=%3F&page=1');
});
});
it('can override default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery({searchParams: {b: '1'}}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?b=1&page=1');
});
});
it('can transform return data', async function () {
await withMockFetch({json: {test: 1}}, async () => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
returnData: data => ({test: (data as {test: number}).test + 1, meta: undefined})
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 2});
});
});
it('exposes pagination metadata', async function () {
await withMockFetch({json: {meta: {pagination: {pages: 2, total: 100}}}}, async () => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {limit: '15'}
});
const {result} = renderHook(() => useTestQuery({}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.pagination.limit).toEqual(15);
expect(result.current.pagination.page).toEqual(1);
expect(result.current.pagination.pages).toEqual(2);
expect(result.current.pagination.total).toEqual(100);
});
});
it('supports navigating pages', async function () {
await withMockFetch({json: {meta: {pagination: {pages: 2}}}}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/'
});
const {result} = renderHook(() => useTestQuery({}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=1');
act(() => result.current.pagination.nextPage());
await waitFor(() => expect(mock.calls.length).toBe(2));
expect(mock.calls[1][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=2');
act(() => result.current.pagination.prevPage());
await waitFor(() => expect(mock.calls.length).toBe(3));
expect(mock.calls[2][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=1');
act(() => result.current.pagination.setPage(5));
await waitFor(() => expect(mock.calls.length).toBe(4));
expect(mock.calls[3][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=5');
});
});
});
describe('createInfiniteQuery', function () {
afterEach(function () {
queryClient.clear();
});
it('makes a paginated API request', async function () {
await withMockFetch({
json: {test: 1, pagination: {next: 2}}
}, async (mock) => {
const useTestQuery = createInfiniteQuery({
dataType: 'test',
path: '/test/',
defaultNextPageParams: (lastPage, otherParams) => ({
...otherParams,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
page: ((lastPage as any).pagination.next || 1).toString()
}),
returnData: (originalData) => {
const {pages} = originalData as InfiniteData<{test: number}>;
return pages.map(page => page.test);
}
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual([1]);
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'GET',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
await act(() => result.current.fetchNextPage());
await waitFor(() => expect(mock.calls.length).toBe(2));
expect(mock.calls[1][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=2');
await waitFor(() => expect(result.current.data).toEqual([1, 1]));
});
});
});
describe('createQueryWithId', function () {
afterEach(function () {
queryClient.clear();
});
it('fills in the ID in the request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestQuery = createQueryWithId({
dataType: 'test',
path: id => `/test/${id}/`
});
const {result} = renderHook(() => useTestQuery('1'), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/1/');
});
});
});
describe('createMutation', function () {
afterEach(function () {
queryClient.clear();
});
it('makes a request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestMutation = createMutation({
path: () => '/test/',
method: 'PUT'
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
expect(await result.current.mutateAsync({})).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'PUT',
mode: 'cors',
body: undefined,
signal: expect.any(AbortSignal)
}]);
});
});
it('computes path, body, searchParams', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestMutation = createMutation({
path: payload => `/test/${payload}/`,
searchParams: payload => ({a: `${payload}`}),
body: payload => ({b: `${payload}`}),
method: 'POST'
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
expect(await result.current.mutateAsync('hello')).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/hello/?a=hello', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'content-type': 'application/json',
'x-ghost-version': '5.x'
},
method: 'POST',
mode: 'cors',
body: '{"b":"hello"}',
signal: expect.any(AbortSignal)
}]);
});
});
it('can invalidate queries in the cache', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
queryClient.setQueryData(['MyDataType', '1'], {test: 1});
queryClient.setQueryData(['MyDataType', '2'], {test: 2});
const useTestMutation = createMutation({
path: () => '/test/',
method: 'PUT',
invalidateQueries: {dataType: 'MyDataType'}
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
await result.current.mutateAsync({});
expect(mock.calls.length).toBe(1);
expect(queryClient.getQueryState(['MyDataType', '1'])?.isInvalidated).toBe(true);
expect(queryClient.getQueryState(['MyDataType', '2'])?.isInvalidated).toBe(true);
});
});
it('can update queries in the cache', async function () {
await withMockFetch({
json: {test: 10}
}, async (mock) => {
queryClient.setQueryData(['MyDataType', '1'], {test: 1});
queryClient.setQueryData(['MyDataType', '2'], {test: 2});
const useTestMutation = createMutation({
path: () => '/test/',
method: 'PUT',
updateQueries: {
emberUpdateType: 'skip',
dataType: 'MyDataType',
update: (newData, currentData) => {
return {test: (newData as {test: number}).test + (currentData as {test: number}).test};
}
}
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
await result.current.mutateAsync({});
expect(mock.calls.length).toBe(1);
expect(queryClient.getQueryData(['MyDataType', '1'])).toEqual({test: 11});
expect(queryClient.getQueryData(['MyDataType', '2'])).toEqual({test: 12});
});
});
});
});