Skip to content
This repository has been archived by the owner on Jun 12, 2022. It is now read-only.

Commit

Permalink
feat: add async fetch flow to Counter component
Browse files Browse the repository at this point in the history
  • Loading branch information
rrebase committed Jan 25, 2020
1 parent c4de2e5 commit a078b0d
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 39 deletions.
8 changes: 7 additions & 1 deletion template/src/features/counter/Counter.module.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.button {
margin: 0 0.75rem;
margin: 0.5rem 0.75rem;
font-size: 1.25rem;
background: transparent;
border-color: #5fd0ae;
Expand All @@ -18,6 +18,12 @@
}
}

.loading {
font-size: 2rem;
color: #fba300;
margin: 3.75rem 0;
}

.value {
font-size: 5rem;
color: #5fd0ae;
Expand Down
149 changes: 125 additions & 24 deletions template/src/features/counter/Counter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,135 @@
import { screen, fireEvent } from "@testing-library/react";
import { screen, fireEvent, wait } from "@testing-library/react";
import Counter from "./Counter";
import React from "react";
import counterReducer, { increment, decrement } from "./CounterSlice";
import counterReducer, {
increment,
decrement,
fetchValueStart,
fetchValueSuccess,
fetchValueError
} from "./CounterSlice";
import { renderWithRedux, rootInitialState } from "utils/test-helpers";
import axios from "axios";

test("displays zero as initial value", () => {
renderWithRedux(<Counter />);
expect(screen.getByText("0")).toBeVisible();
});
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;

test("calls increment on click", () => {
const { mockStore } = renderWithRedux(<Counter />);
fireEvent.click(screen.getByText(/increment/i));
expect(mockStore.getActions()).toEqual([{ type: increment.type }]);
});
describe("<Counter />", () => {
test("shows zero as initial value", () => {
renderWithRedux(<Counter />);
expect(screen.getByText("0")).toBeVisible();
});

test("calls decrement on click", () => {
const { mockStore } = renderWithRedux(<Counter />);
fireEvent.click(screen.getByText(/decrement/i));
expect(mockStore.getActions()).toEqual([{ type: decrement.type }]);
});
test("shows loading message", () => {
renderWithRedux(<Counter />, {
...rootInitialState,
counter: { ...rootInitialState.counter, loading: true }
});
expect(screen.getByText(/loading/i)).toBeVisible();
});

test("shows error message", () => {
const errorMessage = "An error occured";
renderWithRedux(<Counter />, {
...rootInitialState,
counter: { ...rootInitialState.counter, error: errorMessage }
});
expect(screen.getByText(errorMessage)).toBeVisible();
});

test("calls increment on click", () => {
const { mockStore } = renderWithRedux(<Counter />);
fireEvent.click(screen.getByText(/increment/i));
expect(mockStore.getActions()).toEqual([{ type: increment.type }]);
});

test("calls decrement on click", () => {
const { mockStore } = renderWithRedux(<Counter />);
fireEvent.click(screen.getByText(/decrement/i));
expect(mockStore.getActions()).toEqual([{ type: decrement.type }]);
});

test("slow fetch success", async () => {
const name = "cra-template-typekit";
mockedAxios.get.mockResolvedValueOnce({ status: 200, data: { name } });
jest.useFakeTimers();

const { mockStore } = renderWithRedux(<Counter />);

fireEvent.click(screen.getByText(/slow fetch/i));

jest.runAllTimers();
// Normally we would wait for an element to show up
// https://github.com/testing-library/react-testing-library#complex-example
await wait(() => null, { timeout: 500 });

test("increases number by one", () => {
expect(
counterReducer({ ...rootInitialState.counter, value: 1 }, increment)
).toEqual({ ...rootInitialState.counter, value: 2 });
expect(mockStore.getActions()).toEqual([
{ type: fetchValueStart.type, payload: undefined },
{ type: fetchValueSuccess.type, payload: name.length }
]);
});

test("slow fetch error", async () => {
mockedAxios.get.mockResolvedValueOnce({ status: 500 });
jest.useFakeTimers();

const { mockStore } = renderWithRedux(<Counter />);

fireEvent.click(screen.getByText(/slow fetch/i));

jest.runAllTimers();
// Normally we would wait for an element to show up
// https://github.com/testing-library/react-testing-library#complex-example
await wait(() => null, { timeout: 500 });

expect(mockStore.getActions()).toEqual([
{ type: fetchValueStart.type, payload: undefined },
{ type: fetchValueError.type, payload: "Something went wrong." }
]);
});
});

test("decreases number by one", () => {
expect(
counterReducer({ ...rootInitialState.counter, value: 1 }, decrement)
).toEqual({ ...rootInitialState.counter, value: 0 });
describe("CounterSlice", () => {
test("sets loading on fetch start", () => {
expect(
counterReducer(
{ ...rootInitialState.counter, loading: false },
fetchValueStart
)
).toEqual({ ...rootInitialState.counter, loading: true });
});

test("sets value and stop loading on fetch success", () => {
expect(
counterReducer(
{ ...rootInitialState.counter, loading: true },
{ type: fetchValueSuccess, payload: 100 }
)
).toEqual({ ...rootInitialState.counter, loading: false, value: 100 });
});

test("sets error and stop loading on fetch error", () => {
expect(
counterReducer(
{ ...rootInitialState.counter, loading: true },
{ type: fetchValueError, payload: "Some error message." }
)
).toEqual({
...rootInitialState.counter,
loading: false,
error: "Some error message."
});
});

test("increases number by one", () => {
expect(
counterReducer({ ...rootInitialState.counter, value: 1 }, increment)
).toEqual({ ...rootInitialState.counter, value: 2 });
});

test("decreases number by one", () => {
expect(
counterReducer({ ...rootInitialState.counter, value: 1 }, decrement)
).toEqual({ ...rootInitialState.counter, value: 0 });
});
});
48 changes: 37 additions & 11 deletions template/src/features/counter/Counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,54 @@ import React from "react";
import { useDispatch, useSelector } from "react-redux";
import styles from "./Counter.module.scss";
import { increment, decrement } from "./CounterSlice";
import { fetchInitialValue } from "./CounterActions";
import { RootState } from "store";

const Counter: React.FC = () => {
const count = useSelector((state: RootState) => state.counter.value);
const loading = useSelector((state: RootState) => state.counter.loading);
const error = useSelector((state: RootState) => state.counter.error);
const dispatch = useDispatch();

return (
<div>
<p className={styles.value}>{count}</p>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
{error && <p>{error}</p>}

{loading ? (
<p className={styles.loading} data-testid="counter-loading">
Loading...
</p>
) : (
<p className={styles.value} data-testid="counter-value">
{count}
</p>
)}

<div>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
disabled={loading}
>
Increment
</button>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
disabled={loading}
>
Decrement
</button>
</div>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
aria-label="Slow fetch"
onClick={() => dispatch(fetchInitialValue())}
disabled={loading}
>
Decrement
Slow fetch
</button>
</div>
);
Expand Down
26 changes: 26 additions & 0 deletions template/src/features/counter/CounterActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AppDispatch, AppThunk } from "../../store";
import {
fetchValueStart,
fetchValueSuccess,
fetchValueError
} from "./CounterSlice";
import axios from "axios";

export const sleep = (t = Math.random() * 200 + 300) =>
new Promise(resolve => setTimeout(resolve, t));

export const fetchInitialValue = (): AppThunk => async (
dispatch: AppDispatch
) => {
dispatch(fetchValueStart());
try {
// For demo purpose let's say out value is the length
// of project name in manifest and the api call is slow
await sleep();
const response = await axios.get("/manifest.json");
const value = response.data.name.length;
dispatch(fetchValueSuccess(value));
} catch (e) {
dispatch(fetchValueError("Something went wrong."));
}
};
27 changes: 24 additions & 3 deletions template/src/features/counter/CounterSlice.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { createSlice } from "@reduxjs/toolkit";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

interface InitialState {
value: number;
loading: boolean;
error: string | null;
}

export const initialState: InitialState = {
value: 0
value: 0,
loading: false,
error: null
};

export const slice = createSlice({
name: "counter",
initialState,
reducers: {
fetchValueStart: state => {
state.loading = true;
},
fetchValueSuccess: (state, action: PayloadAction<number>) => {
state.value = action.payload;
state.loading = false;
},
fetchValueError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.loading = false;
},
increment: state => {
state.value += 1;
},
Expand All @@ -21,6 +36,12 @@ export const slice = createSlice({
}
});

export const { increment, decrement } = slice.actions;
export const {
fetchValueStart,
fetchValueSuccess,
fetchValueError,
increment,
decrement
} = slice.actions;

export default slice.reducer;
3 changes: 3 additions & 0 deletions template/src/utils/sleep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* istanbul ignore file */
export const sleep = (t = Math.random() * 200 + 300) =>
new Promise(resolve => setTimeout(resolve, t));

0 comments on commit a078b0d

Please sign in to comment.