Skip to content

Commit 642c0b7

Browse files
add some unit test
1 parent b36d9f6 commit 642c0b7

25 files changed

+635
-6
lines changed

__mocks__/file-mock.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const fileMock = 'test-file-stub';
2+
export default fileMock;

__mocks__/i18next-mock.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const i18n = {
2+
init: () => {},
3+
t: (k: string) => k,
4+
use: () => {
5+
return i18n;
6+
},
7+
};
8+
export default i18n;

__mocks__/match-media-mock.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default Object.defineProperty(window, 'matchMedia', {
2+
writable: true,
3+
value: jest.fn().mockImplementation(query => ({
4+
matches: false,
5+
media: query,
6+
onchange: null,
7+
addListener: jest.fn(), // deprecated
8+
removeListener: jest.fn(), // deprecated
9+
addEventListener: jest.fn(),
10+
removeEventListener: jest.fn(),
11+
dispatchEvent: jest.fn(),
12+
})),
13+
});

__mocks__/nanoid-mock.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const nanoid = {
2+
nanoid: () => '',
3+
};
4+
export default nanoid;

__mocks__/react-i18next-mock.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const reactI18Next: any = jest.createMockFromModule('react-i18next');
2+
3+
reactI18Next.useTranslation = () => {
4+
return {
5+
t: (str: string) => str,
6+
i18n: {
7+
changeLanguage: () => new Promise(() => {}),
8+
},
9+
};
10+
};
11+
12+
export default reactI18Next;

__mocks__/style-mock.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};

src/components/game/Controllers/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const Controllers: React.FC<ControllersProps> = ({ isSpinning, onSpin }) => {
7777
);
7878

7979
return (
80-
<div className={styles.controllers}>
80+
<div className={styles.controllers} data-cy="controllers">
8181
<div className={styles['controllers__settings']}>
8282
<ButtonIcon
8383
icon={InfoSvg}

src/components/game/Reel/index.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface ReelProps {
1616
animationDuration: number;
1717
}
1818

19-
const Reel: React.FC<ReelProps> = ({ symbols: reel, reelIndex, animationDuration }) => {
19+
const Reel: React.FC<ReelProps> = ({ symbols, reelIndex, animationDuration }) => {
2020
const areSlotsSpinning = useSelector((state: State) => state.slotMachine.isSpinning);
2121
const [isReelSpinning, setIsReelsSpinning] = useState<boolean>(false);
2222
const reelRef = useRef<HTMLDivElement>(null);
@@ -52,7 +52,7 @@ const Reel: React.FC<ReelProps> = ({ symbols: reel, reelIndex, animationDuration
5252
y: (index: number) => index * symbolSizeInPx,
5353
});
5454

55-
const reelHeight: number = reel.length * symbolSizeInPx;
55+
const reelHeight: number = symbols.length * symbolSizeInPx;
5656
const wrapOffsetTop: number = -symbolSizeInPx;
5757
const wrapOffsetBottom: number = reelHeight + wrapOffsetTop;
5858
const wrap = gsap.utils.wrap(wrapOffsetTop, wrapOffsetBottom);
@@ -75,8 +75,8 @@ const Reel: React.FC<ReelProps> = ({ symbols: reel, reelIndex, animationDuration
7575
}, []);
7676

7777
return (
78-
<div className={styles.reel} ref={reelRef}>
79-
{reel.map((symbol, index) => (
78+
<div className={styles.reel} ref={reelRef} data-cy="reel">
79+
{symbols.map((symbol, index) => (
8080
<SymbolComponent
8181
symbol={symbol}
8282
isSpinning={isReelSpinning}

src/components/game/Reels/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const Reels: React.FunctionComponent<ReelsProps> = ({ reels }) => {
1515
getRandomNumber(MIN_SPIN_ANIMATION_DURATION, MAX_SPIN_ANIMATION_DURATION) * Math.random();
1616

1717
return (
18-
<div className={styles.reels}>
18+
<div className={styles.reels} data-cy="reels">
1919
{reels.map((reel, index) => (
2020
<Reel
2121
symbols={reel}
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ChangeEvent, useContext, useState } from 'react';
2+
import { useDispatch } from 'react-redux';
3+
import { useTranslation } from 'react-i18next';
4+
import { Button, Checkbox } from '@/components';
5+
import { GAME_RESET, RESET_MODAL_DISMISSED } from '@/store/action-types';
6+
import { ModalContext, ModalContextData } from '@/context/ModalContext';
7+
import styles from './styles.module.scss';
8+
9+
const ResetGame: React.FC = () => {
10+
const [t] = useTranslation();
11+
const dispatch = useDispatch();
12+
const { modalProps, closeModal } = useContext<ModalContextData>(ModalContext);
13+
const [doNotShowAgain, setDoNotShowAgain] = useState<boolean>(false);
14+
const buttonLabel: string = modalProps?.hasNoCredits ? t('global.ok') : t('global.yes');
15+
16+
const handleConfirmDiscard = (): void => {
17+
dispatch({ type: GAME_RESET });
18+
if (doNotShowAgain) {
19+
dispatch({ type: RESET_MODAL_DISMISSED, payload: true });
20+
}
21+
closeModal();
22+
};
23+
24+
const handleDeclineReset = (): void => {
25+
if (doNotShowAgain) {
26+
dispatch({ type: RESET_MODAL_DISMISSED, payload: false });
27+
}
28+
closeModal();
29+
};
30+
31+
const handleOnChange = (event: ChangeEvent) => {
32+
const target = event.target as HTMLInputElement;
33+
setDoNotShowAgain(target.checked);
34+
};
35+
36+
return (
37+
<div className={styles.reset}>
38+
<p>{modalProps?.hasNoCredits ? t('reset.resetGame') : t('reset.resetGame')}</p>
39+
<div className={styles['reset__buttons']}>
40+
{!modalProps?.hasNoCredits && (
41+
<Button
42+
label={t('global.no')}
43+
aria-label={t('reset.clickDeclineDiscard')}
44+
title={t('reset.clickDeclineDiscard')}
45+
onClick={handleDeclineReset}
46+
/>
47+
)}
48+
<Button
49+
label={buttonLabel}
50+
aria-label={t('reset.clickDiscard', {
51+
buttonLabel,
52+
})}
53+
title={t('reset.clickDiscard', {
54+
buttonLabel,
55+
})}
56+
onClick={handleConfirmDiscard}
57+
/>
58+
</div>
59+
{!modalProps?.hasNoCredits && (
60+
<Checkbox
61+
label={t('reset.doNotShowAgain')}
62+
checked={doNotShowAgain}
63+
onChange={handleOnChange}
64+
/>
65+
)}
66+
</div>
67+
);
68+
};
69+
70+
export { ResetGame };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.reset {
2+
padding: 2rem 2rem 1rem;
3+
color: $color-blue;
4+
text-align: center;
5+
line-height: 1.25rem;
6+
7+
&__buttons {
8+
display: flex;
9+
justify-content: center;
10+
gap: 0.7rem;
11+
margin: 2rem 0;
12+
13+
// for safari below 14.1 that not supports gap with flexbox
14+
@media not all and (min-resolution: 0.001dpcm) {
15+
@supports (-webkit-appearance: none) {
16+
margin: -0.35rem;
17+
& > * {
18+
margin: 0.35rem;
19+
}
20+
}
21+
}
22+
23+
button {
24+
min-width: 3.25rem;
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type Styles = {
2+
'reset': string;
3+
'reset__buttons': string;
4+
};
5+
6+
export type ClassNames = keyof Styles;
7+
8+
declare const styles: Styles;
9+
10+
export default styles;

src/components/game/Symbol/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const Symbol: React.FC<SymbolProps> = ({
6969
return (
7070
<div
7171
id={isBonusWildCard ? undefined : `symbol-${reelIndex}`}
72+
data-cy="symbol"
7273
className={`${styles.symbol} ${isSpinning ? styles['symbol--spinning'] : ''} ${
7374
willBeReplacedByBonusWildCardSymbol ? styles['symbol--hidden'] : ''
7475
} ${isBonusWildCard ? styles['symbol--showing'] : ''} ${
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Button renders without errors and matches snapshot 1`] = `
4+
<button
5+
class="button "
6+
>
7+
Button
8+
</button>
9+
`;
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import { Button } from './index';
3+
4+
describe('Button', () => {
5+
const onClickMock = jest.fn();
6+
const defaultProps = {
7+
label: 'Button',
8+
onClick: onClickMock,
9+
};
10+
it('renders without errors and matches snapshot', () => {
11+
const {
12+
container: { firstChild },
13+
} = render(<Button {...defaultProps} />);
14+
15+
expect(firstChild).toMatchSnapshot();
16+
});
17+
18+
it('should display label', () => {
19+
render(<Button {...defaultProps} />);
20+
screen.getByText('Button');
21+
});
22+
23+
it('should handle click events', () => {
24+
const onClickMock = jest.fn();
25+
const {
26+
container: { firstChild },
27+
} = render(<Button {...defaultProps} onClick={onClickMock} />);
28+
29+
firstChild && fireEvent.click(firstChild);
30+
31+
expect(onClickMock).toBeCalled();
32+
});
33+
34+
describe('Disabled Button', () => {
35+
test('should display text', () => {
36+
render(<Button {...defaultProps} disabled={true} />);
37+
screen.getByText('Button');
38+
});
39+
40+
it('should be disabled', () => {
41+
const {
42+
container: { firstChild },
43+
} = render(<Button {...defaultProps} disabled={true} />);
44+
45+
expect(firstChild).toHaveAttribute('disabled');
46+
});
47+
48+
it('should not handle click events', () => {
49+
const {
50+
container: { firstChild },
51+
} = render(<Button {...defaultProps} disabled />);
52+
53+
firstChild && fireEvent.click(firstChild);
54+
55+
expect(onClickMock).not.toBeCalled();
56+
});
57+
});
58+
59+
it('should play sound on click', () => {
60+
const sound = new Audio() as HTMLAudioElement;
61+
const playMock = jest.fn();
62+
Object.defineProperty(global.window.HTMLMediaElement.prototype, 'play', {
63+
configurable: true,
64+
get() {
65+
return playMock;
66+
},
67+
});
68+
const {
69+
container: { firstChild },
70+
} = render(<Button {...defaultProps} buttonSound={sound} />);
71+
72+
firstChild && fireEvent.click(firstChild);
73+
74+
expect(playMock).toBeCalled();
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`ButtonIcon renders without errors and matches snapshot 1`] = `
4+
<button
5+
class="button "
6+
>
7+
<svg
8+
aria-hidden="true"
9+
data-testid="button-icon"
10+
height="50"
11+
viewBox="-11.5 -10.232 23 20.463"
12+
width="50"
13+
xmlns="http://www.w3.org/2000/svg"
14+
>
15+
<circle
16+
fill="#61dafb"
17+
r="2.05"
18+
/>
19+
<g
20+
fill="none"
21+
stroke="#61dafb"
22+
>
23+
<ellipse
24+
id="e1"
25+
rx="11"
26+
ry="4.2"
27+
/>
28+
<ellipse
29+
rx="11"
30+
ry="4.2"
31+
transform="rotate(60)"
32+
/>
33+
<ellipse
34+
rx="11"
35+
ry="4.2"
36+
transform="rotate(120)"
37+
/>
38+
</g>
39+
</svg>
40+
</button>
41+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ReactSvg } from '@/assets/svg';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { ButtonIcon } from './index';
4+
5+
describe('ButtonIcon', () => {
6+
const sound = new Audio() as HTMLAudioElement;
7+
const onClickMock = jest.fn();
8+
const defaultProps = {
9+
icon: ReactSvg,
10+
buttonSound: sound,
11+
onClick: onClickMock,
12+
};
13+
it('renders without errors and matches snapshot', () => {
14+
const {
15+
container: { firstChild },
16+
} = render(<ButtonIcon {...defaultProps} />);
17+
18+
expect(firstChild).toMatchSnapshot();
19+
});
20+
21+
it('should display icon', () => {
22+
render(<ButtonIcon {...defaultProps} />);
23+
const icon = screen.getByTestId('button-icon');
24+
expect(icon).toBeTruthy();
25+
});
26+
27+
it('should handle click events', () => {
28+
const {
29+
container: { firstChild },
30+
} = render(<ButtonIcon {...defaultProps} />);
31+
32+
firstChild && fireEvent.click(firstChild);
33+
34+
expect(onClickMock).toBeCalled();
35+
});
36+
37+
it('should play sound on click', () => {
38+
const playMock = jest.fn();
39+
Object.defineProperty(global.window.HTMLMediaElement.prototype, 'play', {
40+
configurable: true,
41+
get() {
42+
return playMock;
43+
},
44+
});
45+
const {
46+
container: { firstChild },
47+
} = render(<ButtonIcon {...defaultProps} />);
48+
49+
firstChild && fireEvent.click(firstChild);
50+
51+
expect(playMock).toBeCalled();
52+
});
53+
});

0 commit comments

Comments
 (0)