Skip to content

Commit 1d0b031

Browse files
authored
Merge pull request #1270 from violetadev/violetadev/testsetup
[Tests] extract jest config to file, add jest commands, add mocks and add missing tests
2 parents c29486f + cada3b5 commit 1d0b031

File tree

7 files changed

+443
-34
lines changed

7 files changed

+443
-34
lines changed

desktop-app/jest.config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
moduleDirectories: ['node_modules', 'release/app/node_modules', 'src'],
3+
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
4+
moduleNameMapper: {
5+
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
6+
'<rootDir>/.erb/mocks/fileMock.js',
7+
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
8+
},
9+
setupFiles: [
10+
'./.erb/scripts/check-build-exists.ts',
11+
'<rootDir>/setupTests.js',
12+
],
13+
testEnvironment: 'jsdom',
14+
testEnvironmentOptions: {
15+
url: 'http://localhost/',
16+
},
17+
testPathIgnorePatterns: ['release/app/dist', '.erb/dll'],
18+
transform: {
19+
'\\.(ts|tsx|js|jsx)$': 'ts-jest',
20+
},
21+
};

desktop-app/package.json

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
3939
"start:preloadWebview": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload-webview.dev.ts",
4040
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
41-
"test": "jest",
41+
"test": "jest --config ./jest.config.js",
42+
"test:watch": "jest --watch --config ./jest.config.js",
43+
"test:coverage": "jest --coverage --config ./jest.config.js",
4244
"typecheck": "tsc --noEmit"
4345
},
4446
"lint-staged": {
@@ -70,38 +72,6 @@
7072
}
7173
]
7274
},
73-
"jest": {
74-
"moduleDirectories": [
75-
"node_modules",
76-
"release/app/node_modules",
77-
"src"
78-
],
79-
"moduleFileExtensions": [
80-
"js",
81-
"jsx",
82-
"ts",
83-
"tsx",
84-
"json"
85-
],
86-
"moduleNameMapper": {
87-
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
88-
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
89-
},
90-
"setupFiles": [
91-
"./.erb/scripts/check-build-exists.ts"
92-
],
93-
"testEnvironment": "jsdom",
94-
"testEnvironmentOptions": {
95-
"url": "http://localhost/"
96-
},
97-
"testPathIgnorePatterns": [
98-
"release/app/dist",
99-
".erb/dll"
100-
],
101-
"transform": {
102-
"\\.(ts|tsx|js|jsx)$": "ts-jest"
103-
}
104-
},
10575
"dependencies": {
10676
"@fontsource/lato": "^5.0.17",
10777
"@headlessui-float/react": "^0.12.0",

desktop-app/setupTests.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
window.electron = {
2+
ipcRenderer: {
3+
sendMessage: jest.fn(),
4+
on: jest.fn(),
5+
once: jest.fn(),
6+
invoke: jest.fn(),
7+
removeListener: jest.fn(),
8+
removeAllListeners: jest.fn(),
9+
},
10+
store: {
11+
set: jest.fn(),
12+
get: jest.fn(),
13+
},
14+
};
15+
16+
global.IntersectionObserver = jest.fn(() => ({
17+
root: null,
18+
rootMargin: '',
19+
thresholds: [],
20+
observe: jest.fn(),
21+
unobserve: jest.fn(),
22+
disconnect: jest.fn(),
23+
takeRecords: jest.fn(),
24+
}));
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import '@testing-library/jest-dom';
2+
import { act, render, screen } from '@testing-library/react';
3+
import Button from './index';
4+
5+
jest.mock('@iconify/react', () => ({
6+
Icon: () => <div data-testid="icon" />,
7+
}));
8+
9+
describe('Button Component', () => {
10+
it('renders with default props', () => {
11+
render(<Button>Click me</Button>);
12+
const buttonElement = screen.getByRole('button', { name: /click me/i });
13+
expect(buttonElement).toBeInTheDocument();
14+
});
15+
16+
it('applies custom class name', () => {
17+
render(<Button className="custom-class">Click me</Button>);
18+
const buttonElement = screen.getByRole('button', { name: /click me/i });
19+
expect(buttonElement).toHaveClass('custom-class');
20+
});
21+
22+
it('renders loading icon when isLoading is true', () => {
23+
render(<Button isLoading>Click me</Button>);
24+
const loadingIcon = screen.getByTestId('icon');
25+
expect(loadingIcon).toBeInTheDocument();
26+
});
27+
28+
it('renders confirmation icon when loading is done', () => {
29+
jest.useFakeTimers();
30+
const { rerender } = render(<Button isLoading>Click me</Button>);
31+
32+
act(() => {
33+
rerender(<Button isLoading={false}>Click me</Button>);
34+
jest.runAllTimers(); // Use act to advance timers
35+
});
36+
37+
const confirmationIcon = screen.getByTestId('icon');
38+
expect(confirmationIcon).toBeInTheDocument();
39+
jest.useRealTimers();
40+
});
41+
42+
it('applies primary button styles', () => {
43+
render(<Button isPrimary>Click me</Button>);
44+
const buttonElement = screen.getByRole('button', { name: /click me/i });
45+
expect(buttonElement).toHaveClass('bg-emerald-500');
46+
expect(buttonElement).toHaveClass('text-white');
47+
});
48+
49+
it('applies action button styles', () => {
50+
render(<Button isActionButton>Click me</Button>);
51+
const buttonElement = screen.getByRole('button', { name: /click me/i });
52+
expect(buttonElement).toHaveClass('bg-slate-200');
53+
});
54+
55+
it('applies subtle hover styles', () => {
56+
render(<Button subtle>Click me</Button>);
57+
const buttonElement = screen.getByRole('button', { name: /click me/i });
58+
expect(buttonElement).toHaveClass('hover:bg-slate-200');
59+
});
60+
61+
it('disables hover effects when disableHoverEffects is true', () => {
62+
render(
63+
<Button disableHoverEffects subtle>
64+
Click me
65+
</Button>
66+
);
67+
const buttonElement = screen.getByRole('button', { name: /click me/i });
68+
expect(buttonElement).not.toHaveClass('hover:bg-slate-200');
69+
});
70+
71+
it('renders children correctly when not loading or loading done', () => {
72+
render(<Button>Click me</Button>);
73+
const buttonElement = screen.getByText('Click me');
74+
expect(buttonElement).toBeInTheDocument();
75+
});
76+
77+
it('does not render children when loading or loading done', () => {
78+
const { rerender } = render(<Button isLoading>Click me</Button>);
79+
expect(screen.queryByText('Click me')).not.toBeInTheDocument();
80+
81+
act(() => {
82+
rerender(<Button isLoading={false}>Click me</Button>);
83+
});
84+
85+
expect(screen.queryByText('Click me')).not.toBeInTheDocument();
86+
});
87+
});
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { Device } from 'common/deviceList';
4+
import { Provider, useDispatch } from 'react-redux';
5+
import { configureStore } from '@reduxjs/toolkit';
6+
import {
7+
addSuites,
8+
deleteAllSuites,
9+
} from 'renderer/store/features/device-manager';
10+
import { ReactNode } from 'react';
11+
import { transformFile } from './utils';
12+
import { ManageSuitesTool } from './ManageSuitesTool';
13+
14+
jest.mock('renderer/store/features/device-manager', () => ({
15+
addSuites: jest.fn(() => ({ type: 'addSuites' })),
16+
deleteAllSuites: jest.fn(() => ({ type: 'deleteAllSuites' })),
17+
default: jest.fn((state = {}) => state), // Mock the reducer as a function
18+
}));
19+
20+
jest.mock('./utils', () => ({
21+
transformFile: jest.fn(),
22+
}));
23+
24+
jest.mock('renderer/components/FileUploader', () => ({
25+
FileUploader: ({
26+
handleFileUpload,
27+
}: {
28+
handleFileUpload: (file: File) => void;
29+
}) => (
30+
<button
31+
type="button"
32+
data-testid="mock-file-uploader"
33+
onClick={() =>
34+
handleFileUpload(
35+
new File(['{}'], 'test.json', { type: 'application/json' })
36+
)
37+
}
38+
>
39+
Mock File Uploader
40+
</button>
41+
),
42+
}));
43+
44+
jest.mock('./helpers', () => ({
45+
onFileDownload: jest.fn(),
46+
setCustomDevices: jest.fn(),
47+
}));
48+
49+
jest.mock('react-redux', () => ({
50+
...jest.requireActual('react-redux'),
51+
useDispatch: jest.fn(),
52+
}));
53+
54+
const renderWithRedux = (
55+
component:
56+
| string
57+
| number
58+
| boolean
59+
| Iterable<ReactNode>
60+
| JSX.Element
61+
| null
62+
| undefined
63+
) => {
64+
const store = configureStore({
65+
reducer: {
66+
deviceManager: jest.requireMock('renderer/store/features/device-manager')
67+
.default,
68+
},
69+
});
70+
71+
return {
72+
...render(<Provider store={store}>{component}</Provider>),
73+
store,
74+
};
75+
};
76+
77+
describe('ManageSuitesTool', () => {
78+
let setCustomDevicesStateMock: jest.Mock<() => void, Device[]>;
79+
const dispatchMock = jest.fn();
80+
81+
beforeEach(() => {
82+
(useDispatch as jest.Mock).mockReturnValue(dispatchMock);
83+
84+
setCustomDevicesStateMock = jest.fn();
85+
86+
renderWithRedux(
87+
<ManageSuitesTool setCustomDevicesState={setCustomDevicesStateMock} />
88+
);
89+
});
90+
91+
it('renders the component correctly', () => {
92+
expect(screen.getByTestId('download-btn')).toBeInTheDocument();
93+
expect(screen.getByTestId('upload-btn')).toBeInTheDocument();
94+
expect(screen.getByTestId('reset-btn')).toBeInTheDocument();
95+
});
96+
97+
it('opens the modal when download button is clicked', () => {
98+
fireEvent.click(screen.getByTestId('download-btn'));
99+
expect(screen.getByText('Import your devices')).toBeInTheDocument();
100+
});
101+
102+
it('opens the reset confirmation dialog when reset button is clicked', () => {
103+
fireEvent.click(screen.getByTestId('reset-btn'));
104+
expect(
105+
screen.getByText('Do you want to reset all settings?')
106+
).toBeInTheDocument();
107+
});
108+
109+
it('closes the reset confirmation dialog when the close button is clicked', () => {
110+
fireEvent.click(screen.getByTestId('reset-btn'));
111+
fireEvent.click(screen.getByText('Cancel'));
112+
expect(
113+
screen.queryByText('Do you want to reset all settings?')
114+
).not.toBeInTheDocument();
115+
});
116+
117+
it('dispatches deleteAllSuites and clears custom devices on reset confirmation', async () => {
118+
fireEvent.click(screen.getByTestId('reset-btn'));
119+
fireEvent.click(screen.getByText('Confirm'));
120+
121+
await waitFor(() => {
122+
expect(deleteAllSuites).toHaveBeenCalled();
123+
expect(setCustomDevicesStateMock).toHaveBeenCalledWith([]);
124+
});
125+
});
126+
127+
it('handles successful file upload and processes custom devices and suites', async () => {
128+
const mockSuites = [
129+
{ id: '1', name: 'first suite', devices: [] },
130+
{ id: '2', name: 'second suite', devices: [] },
131+
];
132+
133+
(transformFile as jest.Mock).mockResolvedValue({
134+
customDevices: ['device1', 'device2'],
135+
suites: mockSuites,
136+
});
137+
138+
fireEvent.click(screen.getByTestId('download-btn'));
139+
fireEvent.click(screen.getByTestId('mock-file-uploader'));
140+
141+
await waitFor(() => {
142+
expect(transformFile).toHaveBeenCalledWith(expect.any(File));
143+
expect(dispatchMock).toHaveBeenCalledWith(addSuites(mockSuites));
144+
});
145+
});
146+
147+
it('handles error in file upload', async () => {
148+
(transformFile as jest.Mock).mockRejectedValue(
149+
new Error('File upload failed')
150+
);
151+
152+
fireEvent.click(screen.getByTestId('download-btn'));
153+
154+
fireEvent.click(screen.getByTestId('mock-file-uploader'));
155+
156+
await waitFor(() => {
157+
expect(transformFile).toHaveBeenCalledWith(expect.any(File));
158+
expect(
159+
screen.getByText('There has been an error, please try again.')
160+
).toBeInTheDocument();
161+
});
162+
163+
fireEvent.click(screen.getByText('Close'));
164+
165+
expect(
166+
screen.queryByText('There has been an error, please try again.')
167+
).not.toBeInTheDocument();
168+
});
169+
});

0 commit comments

Comments
 (0)