@stanlemon/app-template 0.3.19 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +22 -2
- package/package.json +12 -8
- package/src/App.test.tsx +65 -58
- package/src/App.tsx +60 -211
- package/src/Session.tsx +124 -49
- package/src/components/Column.tsx +24 -0
- package/src/components/ErrorMessage.tsx +16 -0
- package/src/components/Header.jsx +14 -0
- package/src/{Input.tsx → components/Input.tsx} +10 -7
- package/src/components/Row.tsx +24 -0
- package/src/components/Spacer.tsx +5 -0
- package/src/components/SuccessMessage.tsx +12 -0
- package/src/components/index.tsx +7 -0
- package/src/helpers/fetchApi.tsx +73 -0
- package/src/index.tsx +2 -0
- package/src/views/Account.test.tsx +87 -0
- package/src/views/Account.tsx +22 -0
- package/src/views/Items.test.tsx +53 -0
- package/src/views/Items.tsx +84 -0
- package/src/views/Login.tsx +57 -0
- package/src/views/Password.tsx +89 -0
- package/src/views/Profile.tsx +66 -0
- package/src/views/SignUp.tsx +82 -0
- package/src/views/Verify.tsx +24 -0
- package/src/views/index.tsx +7 -0
- package/src/Header.jsx +0 -4
- package/src/Login.tsx +0 -82
- package/src/Register.tsx +0 -82
package/src/Session.tsx
CHANGED
|
@@ -1,66 +1,103 @@
|
|
|
1
|
-
import React, { useState, useEffect, createContext } from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import React, { useState, useEffect, useContext, createContext } from "react";
|
|
2
|
+
import { useCookies } from "react-cookie";
|
|
3
|
+
import { ErrorMessage } from "./components/";
|
|
4
|
+
import fetchApi from "./helpers/fetchApi";
|
|
5
|
+
|
|
6
|
+
type SessionContextProperties = {
|
|
7
|
+
initialized: boolean;
|
|
8
|
+
token: string | null;
|
|
9
|
+
user: ProfileData | null;
|
|
10
|
+
error: string | null;
|
|
11
|
+
message: string | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type SessionContextData = SessionContextProperties & {
|
|
15
|
+
setInitialized: React.Dispatch<React.SetStateAction<boolean>>;
|
|
16
|
+
setToken: React.Dispatch<React.SetStateAction<string | null>>;
|
|
17
|
+
setUser: (user: ProfileData | null) => void;
|
|
18
|
+
setError: React.Dispatch<React.SetStateAction<string | null>>;
|
|
19
|
+
setMessage: React.Dispatch<React.SetStateAction<string | null>>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SESSION_CONTEXT_DATA: SessionContextData = {
|
|
23
|
+
initialized: false,
|
|
24
|
+
setInitialized: () => {},
|
|
25
|
+
token: null,
|
|
26
|
+
setToken: () => {},
|
|
27
|
+
user: null,
|
|
28
|
+
setUser: () => {},
|
|
29
|
+
error: null,
|
|
30
|
+
setError: () => {},
|
|
31
|
+
message: null,
|
|
32
|
+
setMessage: () => {},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const SessionContext = createContext<SessionContextData>(
|
|
36
|
+
DEFAULT_SESSION_CONTEXT_DATA
|
|
37
|
+
);
|
|
14
38
|
|
|
15
39
|
export type SessionData = {
|
|
16
40
|
token: string | null;
|
|
17
|
-
user:
|
|
41
|
+
user: ProfileData | null;
|
|
18
42
|
};
|
|
19
43
|
|
|
20
|
-
export type
|
|
21
|
-
name: string | null;
|
|
22
|
-
email: string | null;
|
|
44
|
+
export type ProfileData = {
|
|
23
45
|
username: string;
|
|
24
|
-
|
|
46
|
+
name: string;
|
|
47
|
+
email: string;
|
|
25
48
|
};
|
|
26
49
|
|
|
27
50
|
export default function Session({ children }: { children: React.ReactNode }) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
return (
|
|
52
|
+
<SessionAware>
|
|
53
|
+
<SessionLoader>{children}</SessionLoader>
|
|
54
|
+
</SessionAware>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
export function SessionLoader({ children }: { children: React.ReactNode }) {
|
|
59
|
+
const {
|
|
60
|
+
initialized,
|
|
61
|
+
setInitialized,
|
|
62
|
+
token,
|
|
63
|
+
setToken,
|
|
64
|
+
setUser,
|
|
65
|
+
error,
|
|
66
|
+
setError,
|
|
67
|
+
} = useContext(SessionContext);
|
|
68
|
+
const [cookies, setCookie] = useCookies(["session_token"]);
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
return response;
|
|
50
|
-
})
|
|
51
|
-
.then((response) => response.json())
|
|
70
|
+
const checkSession = () => {
|
|
71
|
+
fetchApi<SessionData, null>("/auth/session", token || cookies.session_token)
|
|
52
72
|
.then((session: SessionData) => {
|
|
53
|
-
|
|
73
|
+
if (session) {
|
|
74
|
+
setCookie("session_token", session.token, { path: "/" });
|
|
75
|
+
setToken(session.token);
|
|
76
|
+
setUser(session.user);
|
|
77
|
+
}
|
|
54
78
|
})
|
|
55
|
-
.catch((err
|
|
56
|
-
if (err.message
|
|
57
|
-
|
|
79
|
+
.catch((err) => {
|
|
80
|
+
if (err.message !== "Unauthorized") {
|
|
81
|
+
setError(err.message);
|
|
58
82
|
}
|
|
59
|
-
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
setInitialized(true);
|
|
60
86
|
});
|
|
61
|
-
}
|
|
87
|
+
};
|
|
62
88
|
|
|
63
|
-
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!initialized) {
|
|
91
|
+
checkSession();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Refresh the session every 30 seconds
|
|
95
|
+
const intervalId = setInterval(checkSession, 1000 * 30);
|
|
96
|
+
|
|
97
|
+
return () => clearInterval(intervalId);
|
|
98
|
+
}, [token]);
|
|
99
|
+
|
|
100
|
+
if (!initialized && error) {
|
|
64
101
|
return <ErrorMessage error={error} />;
|
|
65
102
|
}
|
|
66
103
|
|
|
@@ -72,10 +109,48 @@ export default function Session({ children }: { children: React.ReactNode }) {
|
|
|
72
109
|
);
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
|
|
112
|
+
return children;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function SessionAware({
|
|
116
|
+
initialized: defaultInitialized = false,
|
|
117
|
+
token: defaultToken = null,
|
|
118
|
+
user: defaultUser = null,
|
|
119
|
+
error: defaultError = null,
|
|
120
|
+
message: defaultMessage = null,
|
|
121
|
+
children,
|
|
122
|
+
}: Partial<SessionContextProperties> & { children: React.ReactNode }) {
|
|
123
|
+
const [initialized, setInitialized] = useState<boolean>(defaultInitialized);
|
|
124
|
+
const [token, setToken] = useState<string | null>(defaultToken);
|
|
125
|
+
const [user, setUser] = useState<ProfileData | null>(defaultUser);
|
|
126
|
+
const [error, setError] = useState<string | null>(defaultError);
|
|
127
|
+
const [message, setMessage] = useState<string | null>(defaultMessage);
|
|
76
128
|
|
|
77
129
|
return (
|
|
78
|
-
<SessionContext.Provider
|
|
130
|
+
<SessionContext.Provider
|
|
131
|
+
value={{
|
|
132
|
+
initialized,
|
|
133
|
+
setInitialized,
|
|
134
|
+
token,
|
|
135
|
+
setToken,
|
|
136
|
+
user,
|
|
137
|
+
// Allow for partial setting of user data
|
|
138
|
+
setUser: (user: ProfileData | null) => {
|
|
139
|
+
if (!user) {
|
|
140
|
+
setUser(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setUser((oldUser: React.SetStateAction<ProfileData | null>) => ({
|
|
144
|
+
...oldUser,
|
|
145
|
+
...user,
|
|
146
|
+
}));
|
|
147
|
+
},
|
|
148
|
+
error,
|
|
149
|
+
setError,
|
|
150
|
+
message,
|
|
151
|
+
setMessage,
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
79
154
|
{children}
|
|
80
155
|
</SessionContext.Provider>
|
|
81
156
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export function Column({
|
|
4
|
+
as = "div",
|
|
5
|
+
children,
|
|
6
|
+
}: {
|
|
7
|
+
as?: keyof React.JSX.IntrinsicElements;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}) {
|
|
10
|
+
return React.createElement(
|
|
11
|
+
as,
|
|
12
|
+
{
|
|
13
|
+
style: {
|
|
14
|
+
display: "flex",
|
|
15
|
+
flexDirection: "column",
|
|
16
|
+
flexBasis: "100%",
|
|
17
|
+
flex: "1 1 0",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
children
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default Column;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function ErrorMessage({
|
|
2
|
+
error,
|
|
3
|
+
}: {
|
|
4
|
+
error: string | boolean | undefined | null;
|
|
5
|
+
}) {
|
|
6
|
+
if (error) {
|
|
7
|
+
return (
|
|
8
|
+
<p style={{ color: "red" }}>
|
|
9
|
+
<strong>An error has occurred:</strong> {error}
|
|
10
|
+
</p>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return <></>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default ErrorMessage;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// While typescript is preferred, you can also use good ol javascript too!
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} props
|
|
5
|
+
* @param {number} [props.level]
|
|
6
|
+
* @param {React.ReactNode} [props.children]
|
|
7
|
+
* @returns {JSX.Element}
|
|
8
|
+
*/
|
|
9
|
+
export function Header({ level = 1, children = "Hello World!" }) {
|
|
10
|
+
const Tag = `h${level}`;
|
|
11
|
+
return <Tag>{children}</Tag>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default Header;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
export interface Props
|
|
2
3
|
// Inherit everything from input except onChange which we simplify here
|
|
3
4
|
extends Omit<React.ComponentPropsWithRef<"input">, "onChange"> {
|
|
@@ -8,10 +9,10 @@ export interface Props
|
|
|
8
9
|
placeholder?: string;
|
|
9
10
|
onChange?: (value: string) => void;
|
|
10
11
|
onEnter?: () => void;
|
|
11
|
-
error?: string;
|
|
12
|
+
error?: string | null | undefined;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export
|
|
15
|
+
export function Input({
|
|
15
16
|
type = "text",
|
|
16
17
|
name,
|
|
17
18
|
value = "",
|
|
@@ -29,7 +30,7 @@ export default function Input({
|
|
|
29
30
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
30
31
|
onChange(e.currentTarget.value);
|
|
31
32
|
};
|
|
32
|
-
const
|
|
33
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
33
34
|
if (e.key === "Enter") {
|
|
34
35
|
e.preventDefault();
|
|
35
36
|
onEnter();
|
|
@@ -37,18 +38,20 @@ export default function Input({
|
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
return (
|
|
40
|
-
|
|
41
|
+
<div style={{ marginBottom: ".75em" }}>
|
|
41
42
|
{label && <label htmlFor={name}>{label}</label>}
|
|
42
43
|
<input
|
|
43
44
|
type={type}
|
|
44
45
|
id={name}
|
|
45
46
|
onChange={handleChange}
|
|
46
|
-
|
|
47
|
+
onKeyDown={handleKeyDown}
|
|
47
48
|
placeholder={placeholder}
|
|
48
49
|
value={value}
|
|
49
50
|
{...attrs}
|
|
50
51
|
/>
|
|
51
|
-
{error && <div>{error}</div>}
|
|
52
|
-
|
|
52
|
+
{error && <div style={{ color: "red" }}>{error}</div>}
|
|
53
|
+
</div>
|
|
53
54
|
);
|
|
54
55
|
}
|
|
56
|
+
|
|
57
|
+
export default Input;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export function Row({
|
|
4
|
+
as = "div",
|
|
5
|
+
children,
|
|
6
|
+
}: {
|
|
7
|
+
as?: keyof React.JSX.IntrinsicElements;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}) {
|
|
10
|
+
return React.createElement(
|
|
11
|
+
as,
|
|
12
|
+
{
|
|
13
|
+
style: {
|
|
14
|
+
display: "flex",
|
|
15
|
+
flexDirection: "row",
|
|
16
|
+
flexWrap: "wrap",
|
|
17
|
+
width: "100%",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
children
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default Row;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function fetchApi<T, P>(
|
|
2
|
+
url: string,
|
|
3
|
+
token: string | null | undefined = null,
|
|
4
|
+
method = "get",
|
|
5
|
+
data?: P
|
|
6
|
+
): Promise<T> {
|
|
7
|
+
return (
|
|
8
|
+
fetch(url, {
|
|
9
|
+
method: method,
|
|
10
|
+
headers: {
|
|
11
|
+
Accept: "application/json",
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
// Only send the authorization header if we have a bearer token
|
|
14
|
+
...(token ? { Authorization: `Bearer ${token ?? ""}` } : {}),
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify(data),
|
|
17
|
+
})
|
|
18
|
+
// This allows us to have both the status code and the body
|
|
19
|
+
.then((response) =>
|
|
20
|
+
response.text().then((body) => ({
|
|
21
|
+
ok: response.ok,
|
|
22
|
+
status: response.status,
|
|
23
|
+
statusText: response.statusText,
|
|
24
|
+
body,
|
|
25
|
+
}))
|
|
26
|
+
)
|
|
27
|
+
.then(({ ok, status, statusText, body }) => {
|
|
28
|
+
if (!ok) {
|
|
29
|
+
throw new ApiError(status, statusText, quietJSONParse(body));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return JSON.parse(body) as T;
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse JSON and eat any parse errors.
|
|
38
|
+
* @param body text
|
|
39
|
+
* @returns json or null
|
|
40
|
+
*/
|
|
41
|
+
function quietJSONParse<T>(body: string): T | null {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(body);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default fetchApi;
|
|
50
|
+
|
|
51
|
+
export class ApiError extends Error {
|
|
52
|
+
#code: number;
|
|
53
|
+
|
|
54
|
+
get code(): number {
|
|
55
|
+
return this.#code;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#body: Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
get body(): Record<string, unknown> {
|
|
61
|
+
return this.#body;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
code: number,
|
|
66
|
+
message: string,
|
|
67
|
+
body: Record<string, unknown> | null
|
|
68
|
+
) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.#code = code;
|
|
71
|
+
this.#body = body ?? {};
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Account, PasswordRequest, ProfileForm } from "./";
|
|
5
|
+
import { SessionAware } from "../Session";
|
|
6
|
+
import fetchApi from "../helpers/fetchApi";
|
|
7
|
+
|
|
8
|
+
jest.mock("../helpers/fetchApi");
|
|
9
|
+
|
|
10
|
+
describe("<Account/>", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.resetAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("profile updated", async () => {
|
|
16
|
+
const mockedFetchApi = fetchApi as jest.MockedFunction<
|
|
17
|
+
typeof fetchApi<ProfileForm, ProfileForm>
|
|
18
|
+
>;
|
|
19
|
+
mockedFetchApi.mockResolvedValue({
|
|
20
|
+
name: "Jean Luc Picard",
|
|
21
|
+
email: "jeanluc@starfleet.com",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
render(
|
|
25
|
+
<SessionAware>
|
|
26
|
+
<Account />
|
|
27
|
+
</SessionAware>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText('Account for ""')).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText("Profile")).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText("Password")).toBeInTheDocument();
|
|
33
|
+
|
|
34
|
+
await userEvent.type(screen.getByLabelText("Name"), "Jean Luc Picard");
|
|
35
|
+
|
|
36
|
+
fireEvent.click(screen.getByText("Save", { selector: "button" }));
|
|
37
|
+
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
expect(
|
|
40
|
+
screen.getByText('Account for "Jean Luc Picard"')
|
|
41
|
+
).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("password changed", async () => {
|
|
46
|
+
const mockedFetchApi = fetchApi as jest.MockedFunction<
|
|
47
|
+
typeof fetchApi<null, PasswordRequest>
|
|
48
|
+
>;
|
|
49
|
+
mockedFetchApi.mockResolvedValue(null);
|
|
50
|
+
|
|
51
|
+
render(
|
|
52
|
+
<SessionAware>
|
|
53
|
+
<Account />
|
|
54
|
+
</SessionAware>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('Account for ""')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText("Profile")).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText("Password")).toBeInTheDocument();
|
|
60
|
+
|
|
61
|
+
const current_password = "P@ssw0rd1";
|
|
62
|
+
const password = "P@ssw0rd2";
|
|
63
|
+
|
|
64
|
+
await userEvent.type(
|
|
65
|
+
screen.getByLabelText("Current Password"),
|
|
66
|
+
current_password
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await userEvent.type(screen.getByLabelText("New Password"), password);
|
|
70
|
+
|
|
71
|
+
await userEvent.type(
|
|
72
|
+
screen.getByLabelText("Repeat New Password"),
|
|
73
|
+
password
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fireEvent.click(screen.getByText("Update", { selector: "button" }));
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(mockedFetchApi.mock.calls).toHaveLength(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(mockedFetchApi.mock.calls[0][3]).toEqual({
|
|
83
|
+
current_password,
|
|
84
|
+
password,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { SessionContext } from "../Session";
|
|
3
|
+
import { Header, Spacer } from "../components";
|
|
4
|
+
import { Profile, Password } from "./";
|
|
5
|
+
import { Link } from "wouter";
|
|
6
|
+
|
|
7
|
+
export function Account() {
|
|
8
|
+
const { user } = useContext(SessionContext);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<Header>Account for "{user?.name ?? user?.username ?? ""}"</Header>
|
|
13
|
+
<Profile />
|
|
14
|
+
<Spacer />
|
|
15
|
+
<Password />
|
|
16
|
+
<Spacer />
|
|
17
|
+
<Link href="/">Return Home</Link>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default Account;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import Items, { ItemData } from "./Items";
|
|
5
|
+
import { SessionAware } from "../Session";
|
|
6
|
+
import fetchApi from "../helpers/fetchApi";
|
|
7
|
+
|
|
8
|
+
jest.mock("../helpers/fetchApi");
|
|
9
|
+
|
|
10
|
+
describe("<Items/>", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.resetAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("renders", async () => {
|
|
16
|
+
const output = [
|
|
17
|
+
{ id: "1", item: "item one" },
|
|
18
|
+
{ id: "2", item: "item two" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const mockedFetchApi = fetchApi as jest.MockedFunction<
|
|
22
|
+
typeof fetchApi<ItemData[], null>
|
|
23
|
+
>;
|
|
24
|
+
mockedFetchApi.mockResolvedValue(output);
|
|
25
|
+
|
|
26
|
+
render(
|
|
27
|
+
<SessionAware>
|
|
28
|
+
<Items />
|
|
29
|
+
</SessionAware>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(await screen.findByText("item one")).toBeInTheDocument();
|
|
33
|
+
|
|
34
|
+
expect(await screen.findByText("item two")).toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
// Type some data into the input
|
|
37
|
+
await userEvent.type(screen.getByLabelText("Item"), "item three");
|
|
38
|
+
|
|
39
|
+
// This is the response that will be provided on the add call
|
|
40
|
+
mockedFetchApi.mockResolvedValue([
|
|
41
|
+
...output,
|
|
42
|
+
{ id: "3", item: "item three" },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Click the add button
|
|
46
|
+
fireEvent.click(screen.getByText("Add", { selector: "button" }));
|
|
47
|
+
|
|
48
|
+
expect(mockedFetchApi.mock.calls[1][3]).toEqual({ item: "item three" });
|
|
49
|
+
|
|
50
|
+
// Now we should have a list item with the text we entered
|
|
51
|
+
expect(await screen.findByText("item three")).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|