Skip to content

Commit d230cab

Browse files
committed
chore: increase code coverage
1 parent a0ac108 commit d230cab

File tree

10 files changed

+325
-10
lines changed

10 files changed

+325
-10
lines changed

src/components/About/About.test.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { act, render, screen } from '@testing-library/react';
1+
import { act, render, screen, fireEvent } from '@testing-library/react';
22
import { LIBRARIES } from 'helpers/const';
33
import About from 'components/About';
44
import { AppContext } from 'context/AppContext';
5+
import { AppActions } from 'context/Reducer';
56

67
describe('<About />', () => {
78
const state = {
@@ -23,4 +24,11 @@ describe('<About />', () => {
2324
const listElement = screen.getByTestId('about-libraries-list');
2425
expect(listElement.children.length).toEqual(LIBRARIES.length);
2526
});
27+
28+
it('closes the modal', () => {
29+
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' });
30+
expect(dispatch).toHaveBeenCalledWith({
31+
type: AppActions.HIDE_ABOUT_MODAL,
32+
});
33+
});
2634
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { fireEvent, render, screen } from '@testing-library/react';
2+
import ActionBar from './ActionBar';
3+
import { AppContext } from 'context/AppContext';
4+
import { AppActions } from 'context/Reducer';
5+
6+
describe('<ActionBar />', () => {
7+
const dispatch = jest.fn();
8+
const state = {
9+
code: 'console.log("hello")',
10+
loading: false,
11+
result: [],
12+
error: '',
13+
sidebarOpen: false,
14+
historyOpen: false,
15+
aboutModalOpen: false,
16+
shareUrl: '',
17+
codeSample: '',
18+
codeSampleName: '',
19+
};
20+
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
function renderComponent(customState = {}) {
26+
render(
27+
<AppContext.Provider
28+
value={{ state: { ...state, ...customState }, dispatch }}
29+
>
30+
<ActionBar />
31+
</AppContext.Provider>,
32+
);
33+
}
34+
35+
it('renders all main action buttons', () => {
36+
renderComponent();
37+
expect(screen.getByText('Run Code')).toBeInTheDocument();
38+
expect(screen.getByText('Clear Console')).toBeInTheDocument();
39+
expect(screen.getByText('History')).toBeInTheDocument();
40+
expect(screen.getByText('Code Samples')).toBeInTheDocument();
41+
expect(screen.getByText('Share Code')).toBeInTheDocument();
42+
expect(screen.getByText('About JS Playground')).toBeInTheDocument();
43+
});
44+
45+
it('calls runCode when "Run Code" is clicked', () => {
46+
renderComponent();
47+
fireEvent.click(screen.getByText('Run Code'));
48+
expect(dispatch).toHaveBeenCalledWith({
49+
type: AppActions.CODE_RUNNING,
50+
});
51+
});
52+
53+
it('dispatches CLEAR_RESULT when "Clear Console" is clicked', () => {
54+
renderComponent();
55+
fireEvent.click(screen.getByText('Clear Console'));
56+
expect(dispatch).toHaveBeenCalledWith({ type: AppActions.CLEAR_RESULT });
57+
});
58+
59+
it('dispatches SHOW_HISTORY when "History" is clicked', () => {
60+
renderComponent();
61+
fireEvent.click(screen.getByText('History'));
62+
expect(dispatch).toHaveBeenCalledWith({ type: AppActions.SHOW_HISTORY });
63+
});
64+
65+
it('dispatches SET_SHARE_URL when "Share Code" is clicked', () => {
66+
renderComponent();
67+
fireEvent.click(screen.getByText('Share Code'));
68+
expect(dispatch).toHaveBeenCalledWith(
69+
expect.objectContaining({ type: AppActions.SET_SHARE_URL }),
70+
);
71+
72+
const call = dispatch.mock.calls.find(
73+
([arg]) => arg.type === AppActions.SET_SHARE_URL,
74+
);
75+
expect(call[0].payload).toContain('code=');
76+
});
77+
78+
it('dispatches SHOW_ABOUT_MODAL when "About JS Playground" is clicked', () => {
79+
renderComponent();
80+
fireEvent.click(screen.getByText('About JS Playground'));
81+
expect(dispatch).toHaveBeenCalledWith({
82+
type: AppActions.SHOW_ABOUT_MODAL,
83+
});
84+
});
85+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { render } from '@testing-library/react';
2+
import Spinner from './Spinner';
3+
4+
describe('Spinner Component', () => {
5+
it('renders the svg element', () => {
6+
const { container } = render(<Spinner />);
7+
const spinner = container.querySelector('svg.text-gray-300.animate-spin');
8+
expect(spinner).toBeInTheDocument();
9+
});
10+
});

src/context/AppContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext, useMemo, useReducer } from 'react';
2-
import { getLocalStorage, STORAGE } from 'services/storage';
2+
import { getLocalStorage } from 'services/storage';
3+
import { STORAGE } from 'helpers/const';
34
import { reducer } from 'context/Reducer';
45

56
const initialState: AppState = {

src/context/Reducer.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,67 @@ describe('Reducer tests', () => {
7373
});
7474
expect(state).toEqual(INITIAL_STATE);
7575
});
76+
77+
describe('show and hide about modal', () => {
78+
it('update sidebar value when show about modal is dispatched', () => {
79+
const state = reducer(INITIAL_STATE, {
80+
type: AppActions.SHOW_ABOUT_MODAL,
81+
});
82+
expect(state.sidebarOpen).toEqual(false);
83+
expect(state.aboutModalOpen).toEqual(true);
84+
});
85+
86+
it('update sidebar value when hide about modal is dispatched', () => {
87+
const state = reducer(INITIAL_STATE, {
88+
type: AppActions.HIDE_ABOUT_MODAL,
89+
});
90+
expect(state.sidebarOpen).toEqual(false);
91+
expect(state.aboutModalOpen).toEqual(false);
92+
});
93+
});
94+
95+
describe('show and hide history', () => {
96+
it('update sidebar value when show history is dispatched', () => {
97+
const state = reducer(INITIAL_STATE, {
98+
type: AppActions.SHOW_HISTORY,
99+
});
100+
expect(state.sidebarOpen).toEqual(false);
101+
expect(state.historyOpen).toEqual(true);
102+
});
103+
104+
it('update sidebar value when hide history is dispatched', () => {
105+
const state = reducer(INITIAL_STATE, {
106+
type: AppActions.HIDE_HISTORY,
107+
});
108+
expect(state.sidebarOpen).toEqual(false);
109+
expect(state.historyOpen).toEqual(false);
110+
});
111+
});
112+
113+
describe('toggle sidebar', () => {
114+
it('toggle sidebar open when toggle sidebar is dispatched', () => {
115+
const state = reducer(INITIAL_STATE, {
116+
type: AppActions.SHOW_SIDEBAR,
117+
});
118+
expect(state.sidebarOpen).toEqual(true);
119+
});
120+
121+
it('toggle sidebar close when toggle sidebar is dispatched again', () => {
122+
const initialState = { ...INITIAL_STATE, sidebarOpen: true };
123+
const state = reducer(initialState, {
124+
type: AppActions.HIDE_SIDEBAR,
125+
});
126+
expect(state.sidebarOpen).toEqual(false);
127+
});
128+
});
129+
130+
it('update shareUrl when set share url is dispatched', () => {
131+
const payload = 'share-url';
132+
133+
const state = reducer(INITIAL_STATE, {
134+
type: AppActions.SET_SHARE_URL,
135+
payload,
136+
});
137+
expect(state.shareUrl).toEqual(payload);
138+
});
76139
});

src/context/Reducer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { saveToHistory, setLocalStorage, STORAGE } from 'services/storage';
1+
import { saveToHistory, setLocalStorage } from 'services/storage';
2+
import { STORAGE } from 'helpers/const';
23

34
export const AppActions = {
45
UPDATE_CODE: 'UPDATE_CODE',

src/helpers/const.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,10 @@ console.log(calculate({ operation: '*', operand1: 4, operand2: 6 })); // Outpu
102102
];
103103

104104
export const MAX_SHARE_CODE_LENGTH = 2000;
105+
106+
export const MAX_HISTORY_SIZE = 20;
107+
108+
export const STORAGE = {
109+
CODE: '@abolkog/jscode',
110+
HISTORY: 'abolkog/jscode-history',
111+
};

src/hooks/useCodeRunner.test.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ReactNode } from 'react';
2+
import { renderHook, act } from '@testing-library/react';
3+
import useCodeRunner from './useCodeRunner';
4+
import { AppActions } from 'context/Reducer';
5+
import { AppContext } from 'context/AppContext';
6+
7+
describe('useCodeRunner', () => {
8+
const state = {} as AppState;
9+
let dispatch: jest.Mock;
10+
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
dispatch = jest.fn();
14+
});
15+
16+
const createWrapper = ({ children }: { children: ReactNode }) => (
17+
<AppContext.Provider value={{ state, dispatch }}>
18+
{children}
19+
</AppContext.Provider>
20+
);
21+
22+
it('should dispatch CODE_RUNNING and CODE_RUN_SUCCESS on valid code', async () => {
23+
const { result } = renderHook(() => useCodeRunner(), {
24+
wrapper: createWrapper,
25+
});
26+
27+
const code = 'const a = 2 + 2; a;';
28+
29+
await act(async () => {
30+
await result.current.runCode(code);
31+
});
32+
33+
expect(dispatch).toHaveBeenNthCalledWith(1, {
34+
type: AppActions.CODE_RUNNING,
35+
});
36+
37+
expect(dispatch).toHaveBeenNthCalledWith(2, {
38+
type: AppActions.CODE_RUN_SUCCESS,
39+
payload: 4,
40+
});
41+
});
42+
43+
it('should dispatch CODE_RUN_ERROR on invalid code', async () => {
44+
const { result } = renderHook(() => useCodeRunner(), {
45+
wrapper: createWrapper,
46+
});
47+
48+
const code = 'throw new Error("Invalid code dude");';
49+
50+
await act(async () => {
51+
await result.current.runCode(code);
52+
});
53+
54+
expect(dispatch).toHaveBeenCalledWith({ type: AppActions.CODE_RUNNING });
55+
expect(dispatch).toHaveBeenCalledWith({
56+
type: AppActions.CODE_RUN_ERROR,
57+
payload: 'Invalid code dude',
58+
});
59+
});
60+
});

src/services/storage.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1+
import { compress } from 'lz-string';
2+
import { STORAGE } from 'helpers/const';
13
import * as StorageService from 'services/storage';
24

5+
jest.mock('../helpers/const', () => {
6+
const actual = jest.requireActual('../helpers/const');
7+
return {
8+
...actual,
9+
MAX_HISTORY_SIZE: 2,
10+
};
11+
});
12+
313
describe('Storage tests', () => {
414
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
15+
const today = '2025-05-26';
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
jest.useFakeTimers();
20+
jest.setSystemTime(new Date(today));
21+
});
22+
23+
afterEach(() => {
24+
jest.useRealTimers();
25+
});
526

627
it('Save value to locale storage', () => {
728
StorageService.setLocalStorage('mock', 'value');
@@ -26,4 +47,69 @@ describe('Storage tests', () => {
2647
StorageService.clearLocalStorage('value');
2748
expect(removeItemSpy).toHaveBeenCalledWith('value');
2849
});
50+
51+
describe('getHistory', () => {
52+
const mockCode = 'console.log(1)';
53+
beforeEach(jest.clearAllMocks);
54+
55+
it('return empty array when no history found', () => {
56+
jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(null);
57+
const result = StorageService.getHistory();
58+
expect(result).toEqual([]);
59+
});
60+
61+
it('return history result', () => {
62+
const mockHistory = [
63+
{ code: 'test', date: today },
64+
{ code: 'test2', date: today },
65+
];
66+
jest
67+
.spyOn(Storage.prototype, 'getItem')
68+
.mockReturnValueOnce(JSON.stringify(mockHistory));
69+
const result = StorageService.getHistory();
70+
expect(result.length).toEqual(mockHistory.length);
71+
});
72+
73+
it('save code to history', () => {
74+
jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(null);
75+
76+
StorageService.saveToHistory(mockCode);
77+
expect(setItemSpy).toHaveBeenCalledWith(
78+
STORAGE.HISTORY,
79+
JSON.stringify([{ code: compress(mockCode), date: today }]),
80+
);
81+
});
82+
83+
it('remove oldest item in history if max reached', () => {
84+
jest.spyOn(Storage.prototype, 'getItem').mockReturnValueOnce(
85+
JSON.stringify([
86+
{ code: compress('console.log(2)'), date: today },
87+
{ code: compress('console.log(3)'), date: today },
88+
{ code: compress('console.log(4)'), date: today },
89+
]),
90+
);
91+
92+
StorageService.saveToHistory(mockCode);
93+
expect(setItemSpy).toHaveBeenCalledWith(
94+
STORAGE.HISTORY,
95+
JSON.stringify([
96+
{ code: compress('console.log(3)'), date: today },
97+
{ code: compress('console.log(4)'), date: today },
98+
{ code: compress(mockCode), date: today },
99+
]),
100+
);
101+
});
102+
103+
it('does not save code to history if it already exist', () => {
104+
const mockCode = 'console.log(1)';
105+
jest
106+
.spyOn(Storage.prototype, 'getItem')
107+
.mockReturnValueOnce(
108+
JSON.stringify([{ code: compress(mockCode), date: today }]),
109+
);
110+
111+
StorageService.saveToHistory(mockCode);
112+
expect(setItemSpy).not.toHaveBeenCalled();
113+
});
114+
});
29115
});

src/services/storage.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { compress, decompress } from 'lz-string';
2-
3-
const MAX_HISTORY_SIZE = 20;
4-
5-
export const STORAGE = {
6-
CODE: '@abolkog/jscode',
7-
HISTORY: 'abolkog/jscode-history',
8-
};
2+
import { MAX_HISTORY_SIZE, STORAGE } from 'helpers/const';
93

104
export const getLocalStorage = (key: string, defaultValue = '') => {
115
return localStorage.getItem(key) || defaultValue;

0 commit comments

Comments
 (0)