Skip to content

Commit

Permalink
Merge pull request #5120 from kobotoolbox/task-1046-add-unit-test-for…
Browse files Browse the repository at this point in the history
…-component

[TASK-1046] Introduce unit tests for UI components
p2edwards authored Oct 1, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 307228b + bb8b161 commit 2c01598
Showing 10 changed files with 24,208 additions and 16,814 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ const jsRules = {
'react/no-multi-comp': 0,
'react/no-unknown-property': 0,
'react/prop-types': 0,
'react/react-in-jsx-scope': 2,
// 'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/wrap-multilines': 0,
strict: 1,
3 changes: 3 additions & 0 deletions .github/workflows/npm-test.yml
Original file line number Diff line number Diff line change
@@ -78,3 +78,6 @@ jobs:
# Timeout early to make it easier to manually re-run jobs.
# Tracking issue: https://github.com/kobotoolbox/kpi/issues/4337
timeout-minutes: 1

- name: Run components tests with Jest
run: npm run jest
15 changes: 15 additions & 0 deletions jsapp/jest/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {Config} from 'jest';

const config: Config = {
verbose: true,
testEnvironment: 'jsdom',
roots: ['<rootDir>/../js', '<rootDir>'],
moduleNameMapper: {
'^js/(.*)$': '<rootDir>/../js/$1',
'\\.(css|scss)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/setupJestTest.ts'],
transform: {'^.+\\.(t|j)sx?$': '@swc/jest'},
};

export default config;
4 changes: 4 additions & 0 deletions jsapp/jest/setupJestTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import '@testing-library/jest-dom/jest-globals';
import '@testing-library/jest-dom';

global.t = (str: string) => str;
66 changes: 66 additions & 0 deletions jsapp/js/components/common/button.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Button from './button';

import {render, screen} from '@testing-library/react';
import {describe, it, expect, jest} from '@jest/globals';
import userEvent from '@testing-library/user-event';

const user = userEvent.setup();

describe('Enabled button', () => {
// Mock
const handleClickFunction = jest.fn();

beforeEach(() => {
render(
<Button
type='primary'
size='l'
label='Button Label'
onClick={handleClickFunction}
/>
);
});

it('should render', async () => {
// Assert
expect(screen.getByLabelText('Button Label')).toBeInTheDocument();
});

it('should be clickable', async () => {
// Act
const button = screen.getByLabelText('Button Label');
await user.click(button);

expect(handleClickFunction).toHaveBeenCalledTimes(1);
});
});

describe('Disabled button', () => {
// Mock
const handleClickFunction = jest.fn();

beforeEach(() => {
render(
<Button
type='primary'
size='l'
label='Button Label'
onClick={handleClickFunction}
isDisabled
/>
);
});

it('should render', async () => {
// Assert
expect(screen.getByLabelText('Button Label')).toBeInTheDocument();
});

it('should not be clickable', async () => {
// Act
const button = screen.getByLabelText('Button Label');
await user.click(button);

expect(handleClickFunction).not.toHaveBeenCalled();
});
});
136 changes: 136 additions & 0 deletions jsapp/js/components/special/koboAccessibleSelect.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {fireEvent, render, screen} from '@testing-library/react';
import {describe, it, expect, jest} from '@jest/globals';
import userEvent from '@testing-library/user-event';
import type {KoboSelectOption} from './koboAccessibleSelect';
import KoboSelect3 from './koboAccessibleSelect';
import {useState} from 'react';

const options: KoboSelectOption[] = [
{value: '1', label: 'Apple'},
{value: '2', label: 'Banana'},
{value: '3', label: 'Avocado'},
];

// A wrapper is needed for the component to retain value changes
const Wrapper = ({onChange}: {onChange: (newValue: string) => void}) => {
const [value, setValue] = useState<string>('');

const handleChange = (newValue: string | null = '') => {
setValue(newValue || '');
onChange(newValue || '');
};

return (
<KoboSelect3
name='testSelect'
options={options}
value={value}
onChange={handleChange}
/>
);
};


describe('KoboSelect3', () => {
const user = userEvent.setup();

// Mock
const scrollIntoViewMock = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
const onChangeMock = jest.fn();

beforeEach(() => {
render(<Wrapper onChange={onChangeMock} />);
});

it('should render with proper placeholder', async () => {
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');
expect(trigger).toBeInTheDocument();
expect(triggerLabel).toHaveTextContent('Select…');
});

it('should have the list closed on start', async () => {
const list = screen.getByRole('listbox');
expect(list.dataset.expanded).toBe('false');
});

it('should have a list with the correct items count', async () => {
const listOptions = screen.getAllByRole('option');
expect(listOptions).toHaveLength(3);
});

it('should be selectable by mouse click', async () => {
const trigger = screen.getByRole('combobox');
const list = screen.getByRole('listbox');
const listOptions = screen.getAllByRole('option');

// Clicks the trigger
await user.click(trigger);
expect(list.dataset.expanded).toBe('true');

// Select first option
await user.click(listOptions[0]);

// Onchange should be called with the correct value
expect(onChangeMock).lastCalledWith(options[0].value);

expect(list.dataset.expanded).toBe('false');
});

it('should be selectable by keyboard arrows', async () => {
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');

// No item selected
expect(triggerLabel).toHaveTextContent('Select…');

// Increase option on arrow down
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).lastCalledWith(options[0].value);
expect(triggerLabel).toHaveTextContent(options[0].label);

// Increase option on arrow right
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
expect(onChangeMock).lastCalledWith(options[1].value);
expect(triggerLabel).toHaveTextContent(options[1].label);
fireEvent.keyDown(trigger, {key: 'ArrowRight'});
expect(onChangeMock).lastCalledWith(options[2].value);
expect(triggerLabel).toHaveTextContent(options[2].label);

// Don't go past the last one
onChangeMock.mockReset();
fireEvent.keyDown(trigger, {key: 'ArrowDown'});
expect(onChangeMock).not.toHaveBeenCalled();
expect(triggerLabel).toHaveTextContent(options[2].label);

// Decrease option on arrow up
fireEvent.keyDown(trigger, {key: 'ArrowUp'});
expect(onChangeMock).lastCalledWith(options[1].value);
expect(triggerLabel).toHaveTextContent(options[1].label);

// Decrease option on arrow left
fireEvent.keyDown(trigger, {key: 'ArrowLeft'});
expect(onChangeMock).lastCalledWith(options[0].value);
expect(triggerLabel).toHaveTextContent(options[0].label);

// Don't go past the first one
onChangeMock.mockReset();
fireEvent.keyDown(trigger, {key: 'ArrowUp'});
expect(onChangeMock).not.toHaveBeenCalled();
expect(triggerLabel).toHaveTextContent(options[0].label);
});

it('should be selectable by typing', async () => {
const trigger = screen.getByRole('combobox');
const triggerLabel = trigger.querySelector('label');

// No item selected
expect(triggerLabel).toHaveTextContent('Select…');

// Type 'b' to select Banana
fireEvent.keyDown(trigger, {key: 'b'});
expect(onChangeMock).lastCalledWith(options[1].value);
expect(triggerLabel).toHaveTextContent(options[1].label);
});
});
3 changes: 2 additions & 1 deletion jsapp/js/components/special/koboAccessibleSelect.tsx
Original file line number Diff line number Diff line change
@@ -381,7 +381,8 @@ interface KoboSelect3Props {
// 'data-cy'?: string; // not yet needed
}

interface KoboSelectOption {
/** Needs to be exported to be referenced in the test file. */
export interface KoboSelectOption {
/** Must be unique! */
value: string;
/** Should be unique, too! */
Loading

0 comments on commit 2c01598

Please sign in to comment.