Skip to content

Commit 0566091

Browse files
Feature: Add Comprehensive Unit Test Coverage for Common Utilities and Hooks (#1691)
* fix: sanitize dangerouslySetInnerHTML with DOMPurify to prevent XSS * feat: introduce several new plays, common UI components, and a sanitizeHTML utility. * Eslint version issues solved --------- Co-authored-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com>
1 parent d6be1be commit 0566091

12 files changed

Lines changed: 782 additions & 0 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { renderHook } from '@testing-library/react';
2+
import useCacheResponse from '../useCacheResponse';
3+
4+
describe('useCacheResponse', () => {
5+
it('stores and retrieves cached data', () => {
6+
const { result } = renderHook(() => useCacheResponse());
7+
const [retrieveCache, createCache] = result.current;
8+
9+
createCache('testKey', { data: 'hello' });
10+
expect(retrieveCache('testKey')).toEqual({ data: 'hello' });
11+
});
12+
13+
it('returns null for a cache key that does not exist', () => {
14+
const { result } = renderHook(() => useCacheResponse());
15+
const [retrieveCache] = result.current;
16+
17+
expect(retrieveCache('nonExistentKey')).toBeNull();
18+
});
19+
20+
it('overwrites existing cache entries', () => {
21+
const { result } = renderHook(() => useCacheResponse());
22+
const [retrieveCache, createCache] = result.current;
23+
24+
createCache('key', 'first');
25+
createCache('key', 'second');
26+
expect(retrieveCache('key')).toBe('second');
27+
});
28+
29+
it('supports different data types', () => {
30+
const { result } = renderHook(() => useCacheResponse());
31+
const [retrieveCache, createCache] = result.current;
32+
33+
createCache('number', 42);
34+
createCache('array', [1, 2, 3]);
35+
createCache('null', null);
36+
37+
expect(retrieveCache('number')).toBe(42);
38+
expect(retrieveCache('array')).toEqual([1, 2, 3]);
39+
// null is falsy, so the hook returns null for it too
40+
expect(retrieveCache('null')).toBeNull();
41+
});
42+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import useContributors from '../useContributors';
3+
4+
beforeEach(() => {
5+
global.fetch = jest.fn();
6+
process.env.REACT_APP_PLAY_API_URL = 'https://api.test.com';
7+
});
8+
9+
afterEach(() => {
10+
jest.restoreAllMocks();
11+
delete process.env.REACT_APP_PLAY_API_URL;
12+
});
13+
14+
const mockContributors = [
15+
{ login: 'user1', type: 'User', contributions: 50 },
16+
{ login: 'dependabot', type: 'Bot', contributions: 100 },
17+
{ login: 'user2', type: 'User', contributions: 30 },
18+
{ login: 'user3', type: 'User', contributions: 80 }
19+
];
20+
21+
describe('useContributors', () => {
22+
it('fetches contributors and filters out bots', async () => {
23+
global.fetch.mockResolvedValueOnce({
24+
json: () => Promise.resolve(mockContributors)
25+
});
26+
27+
const { result } = renderHook(() => useContributors(false));
28+
29+
expect(result.current.isLoading).toBe(true);
30+
31+
await waitFor(() => {
32+
expect(result.current.isLoading).toBe(false);
33+
});
34+
35+
expect(result.current.data).toHaveLength(3);
36+
expect(result.current.data.every((c) => c.type !== 'Bot')).toBe(true);
37+
expect(result.current.error).toBeUndefined();
38+
});
39+
40+
it('sorts contributors by contributions when sorted=true', async () => {
41+
global.fetch.mockResolvedValueOnce({
42+
json: () => Promise.resolve(mockContributors)
43+
});
44+
45+
const { result } = renderHook(() => useContributors(true));
46+
47+
await waitFor(() => {
48+
expect(result.current.isLoading).toBe(false);
49+
});
50+
51+
const contributions = result.current.data.map((c) => c.contributions);
52+
expect(contributions).toEqual([80, 50, 30]); // descending order, bot excluded
53+
});
54+
55+
it('sets error on fetch failure', async () => {
56+
const mockError = new Error('Network error');
57+
global.fetch.mockRejectedValueOnce(mockError);
58+
59+
const { result } = renderHook(() => useContributors(false));
60+
61+
await waitFor(() => {
62+
expect(result.current.isLoading).toBe(false);
63+
});
64+
65+
expect(result.current.error).toBe(mockError);
66+
expect(result.current.data).toBeUndefined();
67+
});
68+
69+
it('fetches from the correct API URL', async () => {
70+
global.fetch.mockResolvedValueOnce({
71+
json: () => Promise.resolve([])
72+
});
73+
74+
renderHook(() => useContributors(false));
75+
76+
expect(global.fetch).toHaveBeenCalledWith('https://api.test.com/react-play/contributors');
77+
});
78+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import useFeaturedPlays from '../useFeaturedPlays';
3+
import { submit } from 'common/services/request';
4+
5+
// Mock the services/request module
6+
jest.mock('common/services/request', () => ({
7+
submit: jest.fn()
8+
}));
9+
10+
jest.mock('common/services/request/query/fetch-plays-filter', () => ({
11+
FetchPlaysFilter: {
12+
getAllFeaturedPlays: jest.fn(() => 'mock-query')
13+
}
14+
}));
15+
16+
describe('useFeaturedPlays', () => {
17+
afterEach(() => {
18+
jest.clearAllMocks();
19+
});
20+
21+
it('returns featured plays data on success', async () => {
22+
const mockPlays = [
23+
{ id: 1, name: 'Play 1' },
24+
{ id: 2, name: 'Play 2' }
25+
];
26+
submit.mockResolvedValueOnce(mockPlays);
27+
28+
const { result } = renderHook(() => useFeaturedPlays());
29+
30+
// [loading, error, data]
31+
expect(result.current[0]).toBe(true); // loading
32+
33+
await waitFor(() => {
34+
expect(result.current[0]).toBe(false); // loading done
35+
});
36+
37+
expect(result.current[1]).toBeNull(); // no error
38+
expect(result.current[2]).toEqual(mockPlays); // data
39+
});
40+
41+
it('sets error on failure', async () => {
42+
const mockError = { message: 'GraphQL error' };
43+
submit.mockRejectedValueOnce([mockError]);
44+
45+
const { result } = renderHook(() => useFeaturedPlays());
46+
47+
await waitFor(() => {
48+
expect(result.current[0]).toBe(false);
49+
});
50+
51+
expect(result.current[1]).toEqual(mockError); // error
52+
});
53+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import useFetch from '../useFetch';
3+
4+
// Mock global fetch
5+
beforeEach(() => {
6+
global.fetch = jest.fn();
7+
});
8+
9+
afterEach(() => {
10+
jest.restoreAllMocks();
11+
});
12+
13+
describe('useFetch', () => {
14+
it('returns data on successful fetch', async () => {
15+
const mockData = [{ id: 1, name: 'Play 1' }];
16+
global.fetch.mockResolvedValueOnce({
17+
json: () => Promise.resolve(mockData)
18+
});
19+
20+
const { result } = renderHook(() => useFetch('https://api.example.com/data'));
21+
22+
// Initially loading
23+
expect(result.current.loading).toBe(true);
24+
expect(result.current.data).toEqual([]);
25+
26+
await waitFor(() => {
27+
expect(result.current.loading).toBe(false);
28+
});
29+
30+
expect(result.current.data).toEqual(mockData);
31+
expect(result.current.error).toBeNull();
32+
});
33+
34+
it('sets error on fetch failure', async () => {
35+
const mockError = new Error('Network error');
36+
global.fetch.mockRejectedValueOnce(mockError);
37+
38+
const { result } = renderHook(() => useFetch('https://api.example.com/fail'));
39+
40+
await waitFor(() => {
41+
expect(result.current.loading).toBe(false);
42+
});
43+
44+
expect(result.current.error).toBe(mockError);
45+
expect(result.current.data).toEqual([]);
46+
});
47+
48+
it('passes options to fetch', async () => {
49+
global.fetch.mockResolvedValueOnce({
50+
json: () => Promise.resolve({})
51+
});
52+
53+
const options = { method: 'POST', body: JSON.stringify({ test: true }) };
54+
renderHook(() => useFetch('https://api.example.com/data', options));
55+
56+
expect(global.fetch).toHaveBeenCalledWith('https://api.example.com/data', options);
57+
});
58+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import useGitHub from '../useGitHub';
3+
4+
beforeEach(() => {
5+
global.fetch = jest.fn();
6+
});
7+
8+
afterEach(() => {
9+
jest.restoreAllMocks();
10+
});
11+
12+
describe('useGitHub', () => {
13+
it('fetches GitHub user data successfully', async () => {
14+
const mockUser = {
15+
login: 'octocat',
16+
name: 'The Octocat',
17+
public_repos: 8
18+
};
19+
global.fetch.mockResolvedValueOnce({
20+
json: () => Promise.resolve(mockUser)
21+
});
22+
23+
const { result } = renderHook(() => useGitHub('octocat'));
24+
25+
expect(result.current.isLoading).toBe(true);
26+
27+
await waitFor(() => {
28+
expect(result.current.isLoading).toBe(false);
29+
});
30+
31+
expect(result.current.data).toEqual(mockUser);
32+
expect(result.current.error).toBeUndefined();
33+
expect(global.fetch).toHaveBeenCalledWith('https://api.github.com/users/octocat');
34+
});
35+
36+
it('handles fetch errors', async () => {
37+
const mockError = new Error('Rate limited');
38+
global.fetch.mockRejectedValueOnce(mockError);
39+
40+
const { result } = renderHook(() => useGitHub('unknown-user'));
41+
42+
await waitFor(() => {
43+
expect(result.current.isLoading).toBe(false);
44+
});
45+
46+
expect(result.current.error).toBe(mockError);
47+
expect(result.current.data).toBeUndefined();
48+
});
49+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { renderHook } from '@testing-library/react';
2+
import useLikePlays from '../useLikePlays';
3+
import { submit } from 'common/services/request';
4+
5+
jest.mock('common/services/request', () => ({
6+
submit: jest.fn()
7+
}));
8+
9+
jest.mock('common/services/request/query/like-play', () => ({
10+
likeIndividualPlay: jest.fn((obj) => ({ type: 'LIKE', ...obj })),
11+
unlikeIndividualPlay: jest.fn((obj) => ({ type: 'UNLIKE', ...obj }))
12+
}));
13+
14+
describe('useLikePlays', () => {
15+
afterEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
it('likePlay calls submit and resolves on success', async () => {
20+
const mockResponse = { id: 1, liked: true };
21+
submit.mockResolvedValueOnce(mockResponse);
22+
23+
const { result } = renderHook(() => useLikePlays());
24+
const response = await result.current.likePlay({ play_id: 'abc' });
25+
26+
expect(response).toEqual(mockResponse);
27+
expect(submit).toHaveBeenCalledTimes(1);
28+
});
29+
30+
it('likePlay rejects on submit failure', async () => {
31+
const mockError = new Error('Mutation failed');
32+
submit.mockRejectedValueOnce(mockError);
33+
34+
const { result } = renderHook(() => useLikePlays());
35+
36+
await expect(result.current.likePlay({ play_id: 'abc' })).rejects.toThrow('Mutation failed');
37+
});
38+
39+
it('unLikePlay calls submit and resolves on success', async () => {
40+
const mockResponse = { id: 1, liked: false };
41+
submit.mockResolvedValueOnce(mockResponse);
42+
43+
const { result } = renderHook(() => useLikePlays());
44+
const response = await result.current.unLikePlay({ play_id: 'abc' });
45+
46+
expect(response).toEqual(mockResponse);
47+
});
48+
49+
it('unLikePlay rejects on submit failure', async () => {
50+
submit.mockRejectedValueOnce(new Error('Delete failed'));
51+
52+
const { result } = renderHook(() => useLikePlays());
53+
54+
await expect(result.current.unLikePlay({ play_id: 'abc' })).rejects.toThrow('Delete failed');
55+
});
56+
});

0 commit comments

Comments
 (0)