Merge pull request #1270 from violetadev/violetadev/testsetup

[Tests] extract jest config to file, add jest commands, add mocks and add missing tests
This commit is contained in:
Manoj Vivek 2024-08-05 11:08:29 +05:30 committed by GitHub
commit 1d0b0313ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 443 additions and 34 deletions

View file

@ -0,0 +1,21 @@
module.exports = {
moduleDirectories: ['node_modules', 'release/app/node_modules', 'src'],
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/.erb/mocks/fileMock.js',
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
},
setupFiles: [
'./.erb/scripts/check-build-exists.ts',
'<rootDir>/setupTests.js',
],
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost/',
},
testPathIgnorePatterns: ['release/app/dist', '.erb/dll'],
transform: {
'\\.(ts|tsx|js|jsx)$': 'ts-jest',
},
};

View file

@ -38,7 +38,9 @@
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:preloadWebview": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload-webview.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"test": "jest",
"test": "jest --config ./jest.config.js",
"test:watch": "jest --watch --config ./jest.config.js",
"test:coverage": "jest --coverage --config ./jest.config.js",
"typecheck": "tsc --noEmit"
},
"lint-staged": {
@ -70,38 +72,6 @@
}
]
},
"jest": {
"moduleDirectories": [
"node_modules",
"release/app/node_modules",
"src"
],
"moduleFileExtensions": [
"js",
"jsx",
"ts",
"tsx",
"json"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
},
"setupFiles": [
"./.erb/scripts/check-build-exists.ts"
],
"testEnvironment": "jsdom",
"testEnvironmentOptions": {
"url": "http://localhost/"
},
"testPathIgnorePatterns": [
"release/app/dist",
".erb/dll"
],
"transform": {
"\\.(ts|tsx|js|jsx)$": "ts-jest"
}
},
"dependencies": {
"@fontsource/lato": "^5.0.17",
"@headlessui-float/react": "^0.12.0",

24
desktop-app/setupTests.js Normal file
View file

@ -0,0 +1,24 @@
window.electron = {
ipcRenderer: {
sendMessage: jest.fn(),
on: jest.fn(),
once: jest.fn(),
invoke: jest.fn(),
removeListener: jest.fn(),
removeAllListeners: jest.fn(),
},
store: {
set: jest.fn(),
get: jest.fn(),
},
};
global.IntersectionObserver = jest.fn(() => ({
root: null,
rootMargin: '',
thresholds: [],
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
takeRecords: jest.fn(),
}));

View file

@ -0,0 +1,87 @@
import '@testing-library/jest-dom';
import { act, render, screen } from '@testing-library/react';
import Button from './index';
jest.mock('@iconify/react', () => ({
Icon: () => <div data-testid="icon" />,
}));
describe('Button Component', () => {
it('renders with default props', () => {
render(<Button>Click me</Button>);
const buttonElement = screen.getByRole('button', { name: /click me/i });
expect(buttonElement).toBeInTheDocument();
});
it('applies custom class name', () => {
render(<Button className="custom-class">Click me</Button>);
const buttonElement = screen.getByRole('button', { name: /click me/i });
expect(buttonElement).toHaveClass('custom-class');
});
it('renders loading icon when isLoading is true', () => {
render(<Button isLoading>Click me</Button>);
const loadingIcon = screen.getByTestId('icon');
expect(loadingIcon).toBeInTheDocument();
});
it('renders confirmation icon when loading is done', () => {
jest.useFakeTimers();
const { rerender } = render(<Button isLoading>Click me</Button>);
act(() => {
rerender(<Button isLoading={false}>Click me</Button>);
jest.runAllTimers(); // Use act to advance timers
});
const confirmationIcon = screen.getByTestId('icon');
expect(confirmationIcon).toBeInTheDocument();
jest.useRealTimers();
});
it('applies primary button styles', () => {
render(<Button isPrimary>Click me</Button>);
const buttonElement = screen.getByRole('button', { name: /click me/i });
expect(buttonElement).toHaveClass('bg-emerald-500');
expect(buttonElement).toHaveClass('text-white');
});
it('applies action button styles', () => {
render(<Button isActionButton>Click me</Button>);
const buttonElement = screen.getByRole('button', { name: /click me/i });
expect(buttonElement).toHaveClass('bg-slate-200');
});
it('applies subtle hover styles', () => {
render(<Button subtle>Click me</Button>);
const buttonElement = screen.getByRole('button', { name: /click me/i });
expect(buttonElement).toHaveClass('hover:bg-slate-200');
});
it('disables hover effects when disableHoverEffects is true', () => {
render(
<Button disableHoverEffects subtle>
Click me
</Button>
);
const buttonElement = screen.getByRole('button', { name: /click me/i });
expect(buttonElement).not.toHaveClass('hover:bg-slate-200');
});
it('renders children correctly when not loading or loading done', () => {
render(<Button>Click me</Button>);
const buttonElement = screen.getByText('Click me');
expect(buttonElement).toBeInTheDocument();
});
it('does not render children when loading or loading done', () => {
const { rerender } = render(<Button isLoading>Click me</Button>);
expect(screen.queryByText('Click me')).not.toBeInTheDocument();
act(() => {
rerender(<Button isLoading={false}>Click me</Button>);
});
expect(screen.queryByText('Click me')).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,169 @@
import '@testing-library/jest-dom';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Device } from 'common/deviceList';
import { Provider, useDispatch } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import {
addSuites,
deleteAllSuites,
} from 'renderer/store/features/device-manager';
import { ReactNode } from 'react';
import { transformFile } from './utils';
import { ManageSuitesTool } from './ManageSuitesTool';
jest.mock('renderer/store/features/device-manager', () => ({
addSuites: jest.fn(() => ({ type: 'addSuites' })),
deleteAllSuites: jest.fn(() => ({ type: 'deleteAllSuites' })),
default: jest.fn((state = {}) => state), // Mock the reducer as a function
}));
jest.mock('./utils', () => ({
transformFile: jest.fn(),
}));
jest.mock('renderer/components/FileUploader', () => ({
FileUploader: ({
handleFileUpload,
}: {
handleFileUpload: (file: File) => void;
}) => (
<button
type="button"
data-testid="mock-file-uploader"
onClick={() =>
handleFileUpload(
new File(['{}'], 'test.json', { type: 'application/json' })
)
}
>
Mock File Uploader
</button>
),
}));
jest.mock('./helpers', () => ({
onFileDownload: jest.fn(),
setCustomDevices: jest.fn(),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(),
}));
const renderWithRedux = (
component:
| string
| number
| boolean
| Iterable<ReactNode>
| JSX.Element
| null
| undefined
) => {
const store = configureStore({
reducer: {
deviceManager: jest.requireMock('renderer/store/features/device-manager')
.default,
},
});
return {
...render(<Provider store={store}>{component}</Provider>),
store,
};
};
describe('ManageSuitesTool', () => {
let setCustomDevicesStateMock: jest.Mock<() => void, Device[]>;
const dispatchMock = jest.fn();
beforeEach(() => {
(useDispatch as jest.Mock).mockReturnValue(dispatchMock);
setCustomDevicesStateMock = jest.fn();
renderWithRedux(
<ManageSuitesTool setCustomDevicesState={setCustomDevicesStateMock} />
);
});
it('renders the component correctly', () => {
expect(screen.getByTestId('download-btn')).toBeInTheDocument();
expect(screen.getByTestId('upload-btn')).toBeInTheDocument();
expect(screen.getByTestId('reset-btn')).toBeInTheDocument();
});
it('opens the modal when download button is clicked', () => {
fireEvent.click(screen.getByTestId('download-btn'));
expect(screen.getByText('Import your devices')).toBeInTheDocument();
});
it('opens the reset confirmation dialog when reset button is clicked', () => {
fireEvent.click(screen.getByTestId('reset-btn'));
expect(
screen.getByText('Do you want to reset all settings?')
).toBeInTheDocument();
});
it('closes the reset confirmation dialog when the close button is clicked', () => {
fireEvent.click(screen.getByTestId('reset-btn'));
fireEvent.click(screen.getByText('Cancel'));
expect(
screen.queryByText('Do you want to reset all settings?')
).not.toBeInTheDocument();
});
it('dispatches deleteAllSuites and clears custom devices on reset confirmation', async () => {
fireEvent.click(screen.getByTestId('reset-btn'));
fireEvent.click(screen.getByText('Confirm'));
await waitFor(() => {
expect(deleteAllSuites).toHaveBeenCalled();
expect(setCustomDevicesStateMock).toHaveBeenCalledWith([]);
});
});
it('handles successful file upload and processes custom devices and suites', async () => {
const mockSuites = [
{ id: '1', name: 'first suite', devices: [] },
{ id: '2', name: 'second suite', devices: [] },
];
(transformFile as jest.Mock).mockResolvedValue({
customDevices: ['device1', 'device2'],
suites: mockSuites,
});
fireEvent.click(screen.getByTestId('download-btn'));
fireEvent.click(screen.getByTestId('mock-file-uploader'));
await waitFor(() => {
expect(transformFile).toHaveBeenCalledWith(expect.any(File));
expect(dispatchMock).toHaveBeenCalledWith(addSuites(mockSuites));
});
});
it('handles error in file upload', async () => {
(transformFile as jest.Mock).mockRejectedValue(
new Error('File upload failed')
);
fireEvent.click(screen.getByTestId('download-btn'));
fireEvent.click(screen.getByTestId('mock-file-uploader'));
await waitFor(() => {
expect(transformFile).toHaveBeenCalledWith(expect.any(File));
expect(
screen.getByText('There has been an error, please try again.')
).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Close'));
expect(
screen.queryByText('There has been an error, please try again.')
).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,138 @@
import '@testing-library/jest-dom';
// import { downloadFile, onFileDownload, setCustomDevices } from './helpers';
import { Device } from 'common/deviceList';
import { fireEvent, render } from '@testing-library/react';
import Button from 'renderer/components/Button';
import * as Helpers from './helpers';
describe('onFileDownload', () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn(() => 'mockedURL');
global.URL.revokeObjectURL = jest.fn(); // Mocking revokeObjectURL too
});
afterEach(() => jest.clearAllMocks());
it('should get customDevices and suites from the store and download the file', () => {
const mockCustomDevices = [{ name: 'Device1', width: 800, height: 600 }];
const mockSuites = [{ name: 'Suite1' }];
const spyOnDownloadFileFn = jest.spyOn(Helpers, 'downloadFile');
(window.electron.store.get as jest.Mock).mockImplementation(
(key: string) => {
if (key === 'deviceManager.customDevices') {
return mockCustomDevices;
}
if (key === 'deviceManager.previewSuites') {
return mockSuites;
}
return null;
}
);
Helpers.onFileDownload();
expect(window.electron.store.get).toHaveBeenCalledWith(
'deviceManager.customDevices'
);
expect(window.electron.store.get).toHaveBeenCalledWith(
'deviceManager.previewSuites'
);
expect(spyOnDownloadFileFn).toHaveBeenCalledWith({
customDevices: mockCustomDevices,
suites: mockSuites,
});
});
});
describe('downloadFile', () => {
it('should create and download a JSON file', () => {
const mockFileData = { key: 'value' };
const mockedUrl = 'http://localhost/#';
const createObjectURLSpy = jest
.spyOn(URL, 'createObjectURL')
.mockReturnValue(mockedUrl);
const revokeObjectURLSpy = jest.spyOn(URL, 'revokeObjectURL');
const spyOnDownloadFileFn = jest.spyOn(Helpers, 'downloadFile');
const { getByTestId } = render(
<div>
<Button
onClick={() => Helpers.downloadFile(mockFileData)}
data-testid="mockDownloadBtn"
>
Download
</Button>
</div>
);
const link = getByTestId('mockDownloadBtn');
fireEvent.click(link);
expect(spyOnDownloadFileFn).toHaveBeenCalled();
expect(createObjectURLSpy).toHaveBeenCalledWith(expect.any(Blob));
expect(revokeObjectURLSpy).toHaveBeenCalledWith(mockedUrl);
});
});
describe('setCustomDevices', () => {
it('should filter out default devices and store custom devices', () => {
const mockCustomDevices: Device[] = [
{
name: 'Device1',
width: 800,
height: 600,
id: '1',
userAgent: '',
type: '',
dpi: 0,
isTouchCapable: false,
isMobileCapable: false,
capabilities: [],
},
{
name: 'Device2',
width: 1024,
height: 768,
id: '2',
userAgent: '',
type: '',
dpi: 0,
isTouchCapable: false,
isMobileCapable: false,
capabilities: [],
},
];
const mockDefaultDevices: Device[] = [
{
name: 'Device1',
width: 800,
height: 600,
id: '0',
userAgent: '',
type: '',
dpi: 0,
isTouchCapable: false,
isMobileCapable: false,
capabilities: [],
},
];
jest.mock('common/deviceList', () => ({
defaultDevices: mockDefaultDevices,
}));
const filteredDevices = Helpers.setCustomDevices(mockCustomDevices);
expect(window.electron.store.set).not.toHaveBeenCalledWith(
'deviceManager.customDevices',
[mockCustomDevices[1]]
);
expect(filteredDevices).toEqual(mockCustomDevices);
});
});

View file

@ -26,7 +26,7 @@ export const FileUploader = ({
}, [handleFileUpload, resetUploadedFile, uploadedFile]);
return (
<div className="flex flex-row flex-wrap">
<div className="flex flex-row flex-wrap" data-testid="file-uploader">
<input
ref={fileInputRef}
type="file"