import { Mock } from 'ts-mockery'; import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import type { ShlinkState } from '../../../src/container/types'; import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrlDetailAction } from '../../../src/short-urls/reducers/shortUrlDetail'; import { shortUrlDetailReducerCreator } from '../../../src/short-urls/reducers/shortUrlDetail'; import type { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList'; describe('shortUrlDetailReducer', () => { const getShortUrlCall = jest.fn(); const buildShlinkApiClient = () => Mock.of({ getShortUrl: getShortUrlCall }); const { reducer, getShortUrlDetail } = shortUrlDetailReducerCreator(buildShlinkApiClient); beforeEach(jest.clearAllMocks); describe('reducer', () => { const action = (type: string) => Mock.of({ type }); it('returns loading on GET_SHORT_URL_DETAIL_START', () => { const state = reducer({ loading: false, error: false }, action(getShortUrlDetail.pending.toString())); const { loading } = state; expect(loading).toEqual(true); }); it('stops loading and returns error on GET_SHORT_URL_DETAIL_ERROR', () => { const state = reducer({ loading: true, error: false }, action(getShortUrlDetail.rejected.toString())); const { loading, error } = state; expect(loading).toEqual(false); expect(error).toEqual(true); }); it('return short URL on GET_SHORT_URL_DETAIL', () => { const actionShortUrl = Mock.of({ longUrl: 'foo', shortCode: 'bar' }); const state = reducer( { loading: true, error: false }, { type: getShortUrlDetail.fulfilled.toString(), payload: actionShortUrl }, ); const { loading, error, shortUrl } = state; expect(loading).toEqual(false); expect(error).toEqual(false); expect(shortUrl).toEqual(actionShortUrl); }); }); describe('getShortUrlDetail', () => { const dispatchMock = jest.fn(); const buildGetState = (shortUrlsList?: ShortUrlsList) => () => Mock.of({ shortUrlsList }); it('dispatches start and error when promise is rejected', async () => { getShortUrlCall.mockRejectedValue({}); await getShortUrlDetail({ shortCode: 'abc123', domain: '' })(dispatchMock, buildGetState(), {}); expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: getShortUrlDetail.pending.toString(), })); expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: getShortUrlDetail.rejected.toString(), })); expect(getShortUrlCall).toHaveBeenCalledTimes(1); }); it.each([ [undefined], [Mock.all()], [ Mock.of({ shortUrls: { data: [] }, }), ], [ Mock.of({ shortUrls: { data: [Mock.of({ shortCode: 'this_will_not_match' })], }, }), ], ])('performs API call when short URL is not found in local state', async (shortUrlsList?: ShortUrlsList) => { const resolvedShortUrl = Mock.of({ longUrl: 'foo', shortCode: 'abc123' }); getShortUrlCall.mockResolvedValue(resolvedShortUrl); await getShortUrlDetail({ shortCode: 'abc123', domain: '' })(dispatchMock, buildGetState(shortUrlsList), {}); expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: getShortUrlDetail.pending.toString(), })); expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: getShortUrlDetail.fulfilled.toString(), payload: resolvedShortUrl, })); expect(getShortUrlCall).toHaveBeenCalledTimes(1); }); it('avoids API calls when short URL is found in local state', async () => { const foundShortUrl = Mock.of({ longUrl: 'foo', shortCode: 'abc123' }); getShortUrlCall.mockResolvedValue(Mock.all()); await getShortUrlDetail(foundShortUrl)( dispatchMock, buildGetState(Mock.of({ shortUrls: { data: [foundShortUrl], }, })), {}, ); expect(dispatchMock).toHaveBeenCalledTimes(2); expect(dispatchMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: getShortUrlDetail.pending.toString(), })); expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: getShortUrlDetail.fulfilled.toString(), payload: foundShortUrl, })); expect(getShortUrlCall).not.toHaveBeenCalled(); }); }); });