Skip to content

Commit 5821ea4

Browse files
committed
Add unit tests
1 parent 290bf6f commit 5821ea4

File tree

12 files changed

+1945
-216
lines changed

12 files changed

+1945
-216
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ The most important features of this component are:
5353
- ✅ Type definitions and type safety
5454
- ✅ DSV format detection
5555
- ✅ Fully compositable
56-
- Automatic testing with >90% coverage
56+
- Automatic testing with >90% coverage
5757
- ❌ Input validation
5858
-[Material UI](https://material-ui.com/) integration
5959
-[ant.design](https://ant.design/) integration

jest.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
roots: ['./src'],
3+
setupFilesAfterEnv: ['./jest.setup.ts'],
4+
moduleFileExtensions: ['ts', 'tsx', 'js'],
5+
testPathIgnorePatterns: ['node_modules/'],
6+
transform: {
7+
'^.+\\.tsx?$': 'ts-jest'
8+
},
9+
testMatch: ['**/*.test.(ts|tsx)'],
10+
moduleNameMapper: {}
11+
};

jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom';

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
"@storybook/addons": "^5.3.18",
2424
"@storybook/preset-typescript": "^3.0.0",
2525
"@storybook/react": "^5.3.18",
26+
"@testing-library/jest-dom": "^5.5.0",
27+
"@testing-library/react": "^10.0.2",
28+
"@types/jest": "^25.2.1",
2629
"@types/node": "^13.11.1",
2730
"@types/react": "^16.9.34",
2831
"@types/react-dom": "^16.9.6",
@@ -34,18 +37,21 @@
3437
"eslint-config-prettier": "^6.10.1",
3538
"eslint-plugin-prettier": "^3.1.3",
3639
"eslint-plugin-react": "^7.19.0",
40+
"jest": "^25.3.0",
3741
"prettier": "^2.0.4",
3842
"react-is": "^16.13.1",
3943
"rollup": "^2.6.1",
44+
"ts-jest": "^25.4.0",
4045
"ts-node": "^8.8.2",
4146
"tslib": "^1.11.1",
4247
"typescript": "^3.8.3"
4348
},
4449
"scripts": {
4550
"build": "yarn build:rollup",
4651
"dev": "yarn dev:storybook",
47-
"test": "ts-node test/test.ts",
52+
"test": "jest",
4853
"pretest": "yarn build",
54+
"dev:test": "jest --watch",
4955
"dev:rollup": "rollup -c --w",
5056
"dev:storybook": "start-storybook -p 6006",
5157
"build:rollup": "rollup -c",

src/DSVImport.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ColumnsType } from './models/column';
2+
import { DSVImport } from '.';
3+
import React from 'react';
4+
import { render, fireEvent } from '@testing-library/react';
5+
6+
describe('DSVImport', () => {
7+
type TestType = { forename: string; surname: string; email: string };
8+
9+
const columns: ColumnsType<TestType> = [
10+
{ key: 'forename', label: 'Forename' },
11+
{ key: 'surname', label: 'Surname' },
12+
{ key: 'email', label: 'Email' }
13+
];
14+
15+
const onChangeMock = jest.fn();
16+
17+
const renderComponent = () => {
18+
return render(
19+
<DSVImport<TestType> columns={columns} onChange={onChangeMock}>
20+
<DSVImport.TextareaInput />
21+
<DSVImport.TablePreview />
22+
</DSVImport>
23+
);
24+
};
25+
26+
it('should invoke the onChange callback on context change', () => {
27+
const { container } = renderComponent();
28+
const textarea = container.querySelector('textarea');
29+
30+
if (textarea) {
31+
fireEvent.change(textarea, { target: { value: 'Max' } });
32+
}
33+
34+
expect(onChangeMock).toBeCalledTimes(2);
35+
expect(onChangeMock).toBeCalledWith([{ email: undefined, forename: 'Max', surname: undefined }]);
36+
});
37+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { render, fireEvent } from '@testing-library/react';
2+
import { TextareaInput } from './TextareaInput';
3+
import React from 'react';
4+
import { ColumnsType } from '../../models/column';
5+
import { getDSVImportContext } from '../../features/context';
6+
import { State } from '../../models/state';
7+
8+
describe('TextareaInput', () => {
9+
type TestType = {};
10+
const columns: ColumnsType<TestType> = [];
11+
const state: State<TestType> = { columns };
12+
const Context = getDSVImportContext<TestType>();
13+
14+
const dispatchMock = jest.fn(() => state);
15+
16+
const renderComponent = () => {
17+
return render(
18+
<Context.Provider value={[state, dispatchMock]}>
19+
<TextareaInput />
20+
</Context.Provider>
21+
);
22+
};
23+
24+
it('should dispatch the setRaw action on input change', () => {
25+
const { container } = renderComponent();
26+
const textarea = container.querySelector('textarea');
27+
28+
if (textarea) {
29+
fireEvent.change(textarea, { target: { value: 'Change' } });
30+
}
31+
32+
expect(dispatchMock).toBeCalledTimes(1);
33+
expect(dispatchMock).toBeCalledWith({ raw: 'Change', type: 'setRaw' });
34+
});
35+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { render } from '@testing-library/react';
2+
import React from 'react';
3+
import { ColumnsType } from '../../models/column';
4+
import { TablePreview } from './TablePreview';
5+
import { State } from '../../models/state';
6+
import { getDSVImportContext } from '../../features/context';
7+
8+
describe('TablePreview', () => {
9+
type TestType = { forename: string; surname: string; email: string };
10+
const columns: ColumnsType<TestType> = [
11+
{ key: 'forename', label: 'Forename' },
12+
{ key: 'surname', label: 'Surname' },
13+
{ key: 'email', label: 'Email' }
14+
];
15+
const defaultState: State<TestType> = { columns };
16+
const Context = getDSVImportContext<TestType>();
17+
18+
const dispatchMock = jest.fn(() => defaultState);
19+
20+
const renderComponent = (state = defaultState) => {
21+
return render(
22+
<Context.Provider value={[state, dispatchMock]}>
23+
<TablePreview />
24+
</Context.Provider>
25+
);
26+
};
27+
28+
it('should render the column labels in the table head', () => {
29+
const { container } = renderComponent();
30+
const tableHeadRow = container.querySelector('thead tr');
31+
32+
expect(tableHeadRow?.childElementCount).toBe(3);
33+
34+
expect(tableHeadRow?.children[0]).toHaveTextContent('Forename');
35+
expect(tableHeadRow?.children[1]).toHaveTextContent('Surname');
36+
expect(tableHeadRow?.children[2]).toHaveTextContent('Email');
37+
});
38+
39+
it('should render the parsed content in the table body', () => {
40+
const { container } = renderComponent({
41+
...defaultState,
42+
parsed: [
43+
{ forename: 'Max', surname: 'Muster', email: '[email protected]' },
44+
{ forename: '', surname: '', email: '[email protected]' }
45+
]
46+
});
47+
const tableBody = container.querySelector('tbody');
48+
49+
expect(tableBody?.childElementCount).toBe(2);
50+
51+
expect(tableBody?.children[0].children[0]).toHaveTextContent('Max');
52+
expect(tableBody?.children[1].children[2]).toHaveTextContent('[email protected]');
53+
});
54+
});

src/features/context.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { reducer, getDSVImportContext } from './context';
2+
import { State } from '../models/state';
3+
4+
describe('context', () => {
5+
type TestType = {};
6+
const defaultState: State<TestType> = { columns: [] };
7+
8+
it('should reduce raw data to state', () => {
9+
const newState = reducer<TestType>(defaultState, { type: 'setRaw', raw: 'Raw data' });
10+
expect(newState).toStrictEqual({ ...defaultState, raw: 'Raw data' });
11+
});
12+
13+
it('should reduce parsed data to state', () => {
14+
const newState = reducer<TestType>(defaultState, { type: 'setParsed', parsed: ['Parsed data'] });
15+
expect(newState).toStrictEqual({ ...defaultState, parsed: ['Parsed data'] });
16+
});
17+
18+
it('should create the context as singleton', () => {
19+
const first = getDSVImportContext<TestType>();
20+
const second = getDSVImportContext<TestType>();
21+
22+
expect(first).toStrictEqual(second);
23+
});
24+
});

src/features/context.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
import { State, emptyState } from '../models/state';
22
import { createContext, Dispatch, useContext } from 'react';
33

4-
export type Actions = { type: 'setRaw'; raw: string };
4+
export type Actions<T> = { type: 'setRaw'; raw: string } | { type: 'setParsed'; parsed: T[] };
55

6-
export const reducer = <T>(state: State<T>, action: Actions) => {
6+
export const reducer = <T>(state: State<T>, action: Actions<T>) => {
77
switch (action.type) {
88
case 'setRaw':
99
return { ...state, raw: action.raw };
10+
case 'setParsed':
11+
return { ...state, parsed: action.parsed };
1012
default:
1113
return state;
1214
}
1315
};
1416

1517
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16-
let contextSingleton: React.Context<[State<any>, Dispatch<Actions>]>;
18+
let contextSingleton: React.Context<[State<any>, Dispatch<Actions<any>>]>;
1719
export const getDSVImportContext = <T>() => {
1820
if (!contextSingleton) {
19-
contextSingleton = createContext<[State<T>, Dispatch<Actions>]>([
21+
contextSingleton = createContext<[State<T>, Dispatch<Actions<T>>]>([
2022
(emptyState as unknown) as State<T>,
2123
() => {
2224
throw new Error('Not initialized');
2325
}
2426
]);
2527
}
26-
return contextSingleton as React.Context<[State<T>, Dispatch<Actions>]>;
28+
return contextSingleton as React.Context<[State<T>, Dispatch<Actions<T>>]>;
2729
};
2830
export const useDSVImport = <T = { [key: string]: string }>() => useContext(getDSVImportContext<T>());
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createSimpleParserMiddleware } from './simpleParserMiddleware';
2+
import { State } from '../models/state';
3+
import { ColumnsType } from '../models/column';
4+
import { Delimiter } from '../models/delimiter';
5+
6+
describe('simpleParserMiddleware', () => {
7+
type TestType = { forename: string; surname: string; email: string };
8+
const columns: ColumnsType<TestType> = [
9+
{ key: 'forename', label: 'Forename' },
10+
{ key: 'surname', label: 'Surname' },
11+
{ key: 'email', label: 'Email' }
12+
];
13+
const defaultState: State<TestType> = { columns };
14+
const middleware = createSimpleParserMiddleware<TestType>();
15+
16+
17+
18+
it('should set new parsed data when raw data is set', () => {
19+
const newState = middleware(defaultState, { type: 'setRaw', raw: 'Max' });
20+
21+
expect(newState.parsed).toStrictEqual([{ forename: 'Max', surname: undefined, email: undefined }]);
22+
});
23+
24+
it('should detect the correct delimiter from raw data', () => {
25+
Object.values(Delimiter).forEach((d) => {
26+
const newState = middleware(defaultState, { type: 'setRaw', raw: rawData.replace(/!/g, d) });
27+
28+
expect(newState.parsed?.length).toBe(2);
29+
expect(newState.parsed?.[0]).toStrictEqual({ forename: 'Max', surname: 'Muster', email: '[email protected]' });
30+
expect(newState.parsed?.[1]).toStrictEqual({
31+
forename: '',
32+
surname: '',
33+
34+
});
35+
});
36+
});
37+
38+
it('should invoke the onChange callback', () => {
39+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
40+
const onChangeMock = jest.fn((_value: TestType[]) => {});
41+
const middlewareWithOnChange = createSimpleParserMiddleware<TestType>(onChangeMock);
42+
43+
middlewareWithOnChange(defaultState, { type: 'setRaw', raw: 'Max' });
44+
45+
expect(onChangeMock).toBeCalledTimes(1);
46+
expect(onChangeMock).toBeCalledWith([{ forename: 'Max', surname: undefined, email: undefined }]);
47+
});
48+
});

0 commit comments

Comments
 (0)