Skip to content

Commit

Permalink
Add unit test for generic reducer and session hooks (#76)
Browse files Browse the repository at this point in the history
* test: add unit tests for generic reducer

Signed-off-by: Lin Wang <[email protected]>

* test: add unit tests for useDeleteSession and usePatchSession

Signed-off-by: Lin Wang <[email protected]>

* test: remove unnecessary await

Signed-off-by: Lin Wang <[email protected]>

---------

Signed-off-by: Lin Wang <[email protected]>
  • Loading branch information
wanglam authored Dec 18, 2023
1 parent 66292ce commit 877de96
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 0 deletions.
32 changes: 32 additions & 0 deletions public/contexts/__mocks__/core_context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { BehaviorSubject } from 'rxjs';
import { coreMock } from '../../../../../src/core/public/mocks';

export const useCore = jest.fn(() => {
const useCoreMock = {
services: {
...coreMock.createStart(),
sessions: {
sessions$: new BehaviorSubject({
objects: [
{
id: '1',
title: 'foo',
},
],
total: 1,
}),
status$: new BehaviorSubject('idle'),
load: jest.fn(),
},
sessionLoad: {},
},
};
useCoreMock.services.http.delete.mockReturnValue(Promise.resolve());
useCoreMock.services.http.put.mockReturnValue(Promise.resolve());
return useCoreMock;
});
59 changes: 59 additions & 0 deletions public/hooks/__tests__/fetch_reducer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { genericReducer } from '../fetch_reducer';

describe('genericReducer', () => {
it('should return original state', () => {
expect(
genericReducer(
{ data: { foo: 'bar' }, loading: false },
// mock not supported type
{ type: ('not-supported-type' as unknown) as 'request' }
)
).toEqual({
data: { foo: 'bar' },
loading: false,
});
});

it('should return state follow request action', () => {
expect(genericReducer({ data: { foo: 'bar' }, loading: false }, { type: 'request' })).toEqual({
data: { foo: 'bar' },
loading: true,
});
});

it('should return state follow success action', () => {
expect(
genericReducer(
{ data: { foo: 'bar' }, loading: false },
{ type: 'success', payload: { foo: 'baz' } }
)
).toEqual({
data: { foo: 'baz' },
loading: false,
});
});

it('should return state follow failure action', () => {
const error = new Error();
expect(
genericReducer({ data: { foo: 'bar' }, loading: false }, { type: 'failure', error })
).toEqual({
error,
loading: false,
});
expect(
genericReducer(
{ data: { foo: 'bar' }, loading: false },
{ type: 'failure', error: { body: error } }
)
).toEqual({
error,
loading: false,
});
});
});
180 changes: 180 additions & 0 deletions public/hooks/__tests__/use_sessions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { useDeleteSession, usePatchSession } from '../use_sessions';
import { renderHook, act } from '@testing-library/react-hooks';
import { useCore } from '../../contexts/core_context';
import { HttpHandler } from '../../../../../src/core/public';

jest.mock('../../contexts/core_context');
const useCoreMocked = useCore as jest.MockedFunction<typeof useCore>;

describe('useDeleteSession', () => {
it('should call delete with path and signal', async () => {
const { result } = renderHook(() => useDeleteSession());

await act(async () => {
await result.current.deleteSession('foo');
});
expect(useCoreMocked.mock.results[0].value.services.http.delete).toHaveBeenCalledWith(
'/api/assistant/session/foo',
expect.objectContaining({
signal: expect.any(Object),
})
);
});

it('should be loading after deleteSession called', async () => {
const { result, waitFor } = renderHook(() => useDeleteSession());
useCoreMocked.mock.results[0].value.services.http.delete.mockReturnValue(new Promise(() => {}));

act(() => {
result.current.deleteSession('foo');
});

await waitFor(() => {
expect(result.current.loading).toBe(true);
});
});

it('should return data after delete success', async () => {
const { result, waitFor } = renderHook(() => useDeleteSession());
useCoreMocked.mock.results[0].value.services.http.delete.mockReturnValue(
Promise.resolve('deleted')
);

act(() => {
result.current.deleteSession('foo');
});

await waitFor(() => {
expect(result.current.data).toBe('deleted');
expect(result.current.loading).toBe(false);
});
});

it('should throw error after abort', async () => {
const { result, waitFor } = renderHook(() => useDeleteSession());
const abortErrorMock = new Error('Abort');
useCoreMocked.mock.results[0].value.services.http.delete.mockImplementation(((
_path,
options
) => {
return new Promise((_resolve, reject) => {
if (options?.signal) {
options.signal.onabort = () => {
reject(abortErrorMock);
};
}
});
}) as HttpHandler);

let deleteSessionPromise: Promise<void>;
act(() => {
deleteSessionPromise = result.current.deleteSession('foo');
});

let deleteSessionError;
await act(async () => {
result.current.abort();
try {
await deleteSessionPromise;
} catch (error) {
deleteSessionError = error;
}
});

expect(result.current.isAborted()).toBe(true);
expect(deleteSessionError).toBe(abortErrorMock);

await waitFor(() => {
expect(result.current.error).toBe(abortErrorMock);
});
});
});

describe('usePatchSession', () => {
it('should call put with path, query and signal', async () => {
const { result } = renderHook(() => usePatchSession());

await act(async () => {
await result.current.patchSession('foo', 'new-title');
});
expect(useCoreMocked.mock.results[0].value.services.http.put).toHaveBeenCalledWith(
'/api/assistant/session/foo',
expect.objectContaining({
signal: expect.any(Object),
body: JSON.stringify({ title: 'new-title' }),
})
);
});

it('should be loading after patchSession called', async () => {
const { result, waitFor } = renderHook(() => usePatchSession());
useCoreMocked.mock.results[0].value.services.http.put.mockReturnValue(new Promise(() => {}));

act(() => {
result.current.patchSession('foo', 'new-title');
});

await waitFor(() => {
expect(result.current.loading).toBe(true);
});
});

it('should return data after patch session success', async () => {
const { result, waitFor } = renderHook(() => usePatchSession());
useCoreMocked.mock.results[0].value.services.http.put.mockReturnValue(
Promise.resolve({
title: 'new-title',
})
);

act(() => {
result.current.patchSession('foo', 'new-title');
});

await waitFor(() => {
expect(result.current.data).toEqual({ title: 'new-title' });
expect(result.current.loading).toBe(false);
});
});

it('should throw error after abort', async () => {
const { result, waitFor } = renderHook(() => usePatchSession());
const abortErrorMock = new Error('Abort');
useCoreMocked.mock.results[0].value.services.http.put.mockImplementation(((_path, options) => {
return new Promise((_resolve, reject) => {
if (options?.signal) {
options.signal.onabort = () => {
reject(abortErrorMock);
};
}
});
}) as HttpHandler);

let patchSessionPromise: Promise<void>;
act(() => {
patchSessionPromise = result.current.patchSession('foo', 'new-title');
});

let patchSessionError;
await act(async () => {
result.current.abort();
try {
await patchSessionPromise;
} catch (error) {
patchSessionError = error;
}
});

expect(result.current.isAborted()).toBe(true);
expect(patchSessionError).toBe(abortErrorMock);

await waitFor(() => {
expect(result.current.error).toBe(abortErrorMock);
});
});
});

0 comments on commit 877de96

Please sign in to comment.