Adds snapshot and begins to add login component tests
All checks were successful
gitea-deepak/gog_frontend/pipeline/head This commit looks good

This commit is contained in:
2021-01-23 18:53:53 -06:00
parent ea710c3845
commit 6da0c2b84a
22 changed files with 397 additions and 277 deletions

2
do.sh
View File

@@ -11,7 +11,7 @@ build() {
run() { run() {
echo "I am ${FUNCNAME[0]}ning" echo "I am ${FUNCNAME[0]}ning"
npx webpack serve API_ROOT=http://localhost:3000/api npx webpack serve
} }
fmt() { fmt() {

121
package-lock.json generated
View File

@@ -1105,6 +1105,16 @@
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
}, },
"@babel/runtime-corejs3": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz",
"integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.4"
}
},
"@babel/template": { "@babel/template": {
"version": "7.12.7", "version": "7.12.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz",
@@ -1919,6 +1929,89 @@
"@sinonjs/commons": "^1.7.0" "@sinonjs/commons": "^1.7.0"
} }
}, },
"@testing-library/dom": {
"version": "7.29.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.4.tgz",
"integrity": "sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^4.2.0",
"aria-query": "^4.2.2",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.4",
"lz-string": "^1.4.4",
"pretty-format": "^26.6.2"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@testing-library/react": {
"version": "11.2.3",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.3.tgz",
"integrity": "sha512-BirBUGPkTW28ULuCwIbYo0y2+0aavHczBT6N9r3LrsswEW3pg25l1wgoE7I8QBIy1upXWkwKpYdWY7NYYP0Bxw==",
"dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^7.28.1"
}
},
"@types/aria-query": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz",
"integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==",
"dev": true
},
"@types/babel__core": { "@types/babel__core": {
"version": "7.1.12", "version": "7.1.12",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz",
@@ -2552,6 +2645,16 @@
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"@babel/runtime-corejs3": "^7.10.2"
}
},
"arr-diff": { "arr-diff": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -3574,6 +3677,12 @@
} }
} }
}, },
"core-js-pure": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.3.tgz",
"integrity": "sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA==",
"dev": true
},
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -4001,6 +4110,12 @@
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
}, },
"dom-accessibility-api": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz",
"integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==",
"dev": true
},
"dom-walk": { "dom-walk": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
@@ -8100,6 +8215,12 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true
},
"make-dir": { "make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View File

@@ -35,6 +35,7 @@
"@babel/core": "^7.12.10", "@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10", "@babel/preset-react": "^7.12.10",
"@testing-library/react": "^11.2.3",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"css-loader": "^5.0.1", "css-loader": "^5.0.1",
"eslint": "^7.17.0", "eslint": "^7.17.0",
@@ -59,5 +60,8 @@
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-hot-loader": "^4.13.0" "react-hot-loader": "^4.13.0"
} },
"browserslist": [
"since 2017-06"
]
} }

View File

@@ -1,19 +1,15 @@
import React from "react"; import React from "react";
import { hot } from "react-hot-loader"; import { hot } from "react-hot-loader";
import "./App.css"; import "./App.css";
import AllPlansComponent from "./components/AllPlansComponent.jsx"; import UnauthenticatedApp from "./components/UnauthenticatedApp";
class App extends React.Component { function App() {
render() { return (
return ( <div className="App">
<div className="App"> <h1>Frontend</h1>
<h1> Hello, World! </h1> <UnauthenticatedApp />
<div className="mainContent"> </div>
<AllPlansComponent /> );
</div>
</div>
);
}
} }
export default hot(module)(App); export default hot(module)(App);

View File

@@ -1,30 +0,0 @@
.actionID,
.actionDescription,
.actionChunks {
margin-left: 1rem;
margin-right: 1rem;
}
.actionCompleted {
flex: 1;
}
.actionChunks {
flex: 4;
}
.actionID {
flex: 4;
}
.actionDescription {
flex: 8;
}
.actionWrapper {
align-items: flex-end;
display: flex;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
padding-top: 1rem;
}

View File

@@ -1,29 +0,0 @@
import React from "react";
import { action } from "../types";
import "./Action.css";
class Action extends React.Component {
render() {
const action = this.props.action;
const completed = "completed_on" in action;
console.log([action.completed_on, completed]);
return (
<div className="actionWrapper">
<div className="actionCompleted">{completed ? "X" : " "}</div>
<div className="actionID">
ID: {action.action_id} | {action.plan_id}
</div>
<div className="actionDescription">{action.action_description}</div>
<div className="actionChunks">
{action.completed_chunks}/{action.estimated_chunks}
</div>
</div>
);
}
}
Action.propTypes = {
action: action,
};
export default Action;

View File

@@ -1,3 +0,0 @@
test("trying it out", () => {
expect(true).toEqual(true);
});

View File

@@ -1,28 +0,0 @@
import React from "react";
import Action from "./Action.jsx";
import PropTypes from "prop-types";
import { action } from "../types";
class ActionsContainer extends React.Component {
render() {
return (
<div className="ActionsListWrapper">
<ul className="ActionsList">
{this.props.actions.map((action) => {
return (
<li className="Action" key={action.action_id}>
<Action action={action} />
</li>
);
})}
</ul>
</div>
);
}
}
ActionsContainer.propTypes = {
actions: PropTypes.arrayOf(action),
};
export default ActionsContainer;

View File

@@ -1,40 +0,0 @@
import React from "react";
import PlanList from "./PlanList.jsx";
class AllPlansComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
plans: [],
};
}
getPlans() {
fetch("http://localhost:3000/api/plans", {
headers: {
Accept: "application/json",
},
})
.then((response) => response.json())
.then((data) => {
this.setState({
plans: data,
});
})
.catch((error) => console.error(error));
}
componentDidMount() {
this.getPlans();
}
render() {
return (
<div className="AllPlans">
<PlanList plans={this.state.plans} />
</div>
);
}
}
export default AllPlansComponent;

View File

@@ -0,0 +1,15 @@
import React from "react";
import renderer from "react-test-renderer";
import Login from "./Login";
test("Login Snapshot", () => {
const loginFunc = jest.fn();
const login = renderer.create(<Login login={loginFunc} />);
const tree = login.toJSON();
expect(tree).toMatchSnapshot();
});

44
src/components/Login.jsx Normal file
View File

@@ -0,0 +1,44 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
export default function Login({ login }) {
const [username, setUserName] = useState();
const [password, setPassword] = useState();
const handleSubmit = async (e) => {
e.preventDefault();
login(username, password);
};
return (
<div className="login-wrapper">
<h1>Please Log In</h1>
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input
id="username"
type="text"
onChange={(e) => setUserName(e.target.value)}
/>
</label>
<label>
<p>Password</p>
<input
id="password"
type="password"
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
);
}
Login.propTypes = {
login: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,17 @@
import React from "react";
import { render } from '@testing-library/react'
import { logRoles } from "@testing-library/dom"
// import '@testing-library/jest-dom/extend-expect'
import Login from "./Login";
test("Login inputs", () => {
const login = render(<Login login={ jest.fn } />)
const usernameInput = login.getByRole("textbox", {name: /Username/i})
expect(usernameInput).toBeTruthy();
const passwordInput = login.getByLabelText("Password")
expect(passwordInput).toBeTruthy();
logRoles(login.container)
expect(true).toBeTruthy();
})

View File

@@ -1,54 +0,0 @@
import React from "react";
import ActionsContainer from "./ActionsContainer.jsx";
import { plan } from "../types";
class Plan extends React.Component {
constructor(props) {
super(props);
this.state = {
actions: [],
};
}
getActions() {
fetch(
"http://localhost:3000/api/actions?" +
new URLSearchParams({
plan_id: this.props.plan.plan_id,
}),
{
headers: {
Accept: "application/json",
},
}
)
.then((response) => response.json())
.then((data) => {
this.setState({
actions: data,
});
})
.catch((error) => console.error(error));
}
componentDidMount() {
this.getActions();
}
render() {
return (
<div className="PlanActionsWrapper">
<div>
<h3>Plan for {this.props.plan.plan_date}</h3>
</div>
<ActionsContainer actions={this.state.actions.slice()} />
</div>
);
}
}
Plan.propTypes = {
plan: plan,
};
export default Plan;

View File

@@ -1,28 +0,0 @@
import React from "react";
import Plan from "./Plan.jsx";
import { plan } from "../types";
import PropTypes from "prop-types";
class PlanList extends React.Component {
render() {
return (
<div className="PlanListWrapper">
<ul className="PlanList">
{this.props.plans.map((plan) => {
return (
<li className="Plan" key={plan.plan_id}>
<Plan plan={plan} />
</li>
);
})}
</ul>
</div>
);
}
}
PlanList.propTypes = {
plans: PropTypes.arrayOf(plan),
};
export default PlanList;

View File

@@ -0,0 +1,53 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
export default function Register({ register }) {
const [username, setUserName] = useState();
const [displayName, setDisplayName] = useState();
const [password, setPassword] = useState();
const handleSubmit = async (e) => {
e.preventDefault();
register(username, displayName, password);
};
return (
<div className="register-wrapper">
<h1>Enter your data to register.</h1>
<form onSubmit={handleSubmit}>
<label>
<p>Username</p>
<input
id="username"
type="text"
onChange={(e) => setUserName(e.target.value)}
/>
</label>
<label>
<p>Name</p>
<input
id="displayName"
type="text"
onChange={(e) => setDisplayName(e.target.value)}
/>
</label>
<label>
<p>Password</p>
<input
id="password"
type="password"
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<div>
<button type="submit">Sign me up</button>
</div>
</form>
</div>
);
}
Register.propTypes = {
register: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,30 @@
import React, { useState } from "react";
import { useAuth } from "../context/AuthContext";
import Login from "./Login";
import Register from "./Register";
function UnauthenticatedApp() {
const { login, register } = useAuth();
const [showRegister, setShowRegister] = useState(false);
function toggle() {
setShowRegister(!showRegister);
}
const form = showRegister ? (
<Register register={register} />
) : (
<Login login={login} />
);
return (
<div>
<button type="submit" onClick={toggle}>
toggle signup
</button>
{form}
</div>
);
}
export default UnauthenticatedApp;

View File

@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Login Snapshot 1`] = `
<div
className="login-wrapper"
>
<h1>
Please Log In
</h1>
<form
onSubmit={[Function]}
>
<label>
<p>
Username
</p>
<input
id="username"
onChange={[Function]}
type="text"
/>
</label>
<label>
<p>
Password
</p>
<input
id="password"
onChange={[Function]}
type="password"
/>
</label>
<div>
<button
type="submit"
>
Submit
</button>
</div>
</form>
</div>
`;

View File

@@ -1,55 +1,11 @@
import React from "react"; import React from "react";
import { useAsync } from "react-async"; import { register, login } from "../services/auth-service";
import { bootstrapAppData } from "../utils/bootstrap";
import * as authClient from "../utils/auth-client";
import { FullPageSpinner } from "../components/lib";
const AuthContext = React.createContext(); const AuthContext = React.createContext();
function AuthProvider(props) { const AuthProvider = (props) => {
const [firstAttemptFinished, setFirstAttemptFinished] = React.useState(false); return <AuthContext.Provider value={{ login, register }} {...props} />;
const { };
data = { user: null, listItems: [] },
error,
isRejected,
isPending,
isSettled,
reload,
} = useAsync({
promiseFn: bootstrapAppData,
});
React.useLayoutEffect(() => {
if (isSettled) {
setFirstAttemptFinished(true);
}
}, [isSettled]);
if (!firstAttemptFinished) {
if (isPending) {
return <FullPageSpinner />;
}
if (isRejected) {
return (
<div css={{ color: "red" }}>
<p>Uh oh... There's a problem. Try refreshing the app.</p>
<pre>{error.message}</pre>
</div>
);
}
}
const login = (form) => authClient.login(form).then(reload);
const register = (form) => authClient.register(form).then(reload);
const logout = () => authClient.logout().then(reload);
return (
<AuthContext.Provider
value={{ data, login, logout, register }}
{...props}
/>
);
}
function useAuth() { function useAuth() {
const context = React.useContext(AuthContext); const context = React.useContext(AuthContext);

View File

@@ -1,8 +1,13 @@
import React from "react"; import React from "react";
import { AuthProvider } from "./AuthContext.jsx"; import { AuthProvider } from "./AuthContext.jsx";
import PropTypes from "prop-types";
function AppProviders({ children }) { function AppProviders({ children }) {
return <AuthProvider>{children}</AuthProvider>; return <AuthProvider>{children}</AuthProvider>;
} }
export default AppProviders; export default AppProviders;
AppProviders.propTypes = {
children: PropTypes.node,
};

View File

@@ -1,9 +1,8 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import App from "./App.jsx"; import App from "./App.jsx";
ReactDOM.render(<App />, document.getElementById("root"));
import AppProviders from "./context"; import AppProviders from "./context";
ReactDOM.render( ReactDOM.render(
<AppProviders> <AppProviders>
<App /> <App />

View File

@@ -0,0 +1,50 @@
import { API_ROOT } from "./config";
export const register = (username, displayName, password) => {
const url = API_ROOT + "auth/register";
const body = {
username: username,
display_name: displayName,
password: password,
};
return fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
};
export const login = (username, password) => {
const url = API_ROOT + "auth/tokens";
const body = {
username: username,
password: password,
};
return fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
})
.then((data) => {
console.log(data);
if (data.token) {
console.log(data.token);
return data.token;
}
throw Error("where's my token");
});
};

View File

@@ -1,4 +1,4 @@
import { API_ROOT } from "./config" import { API_ROOT } from "./config";
test("testing config api root", () => { test("testing config api root", () => {
expect(API_ROOT).toEqual("http://localhost:8080/"); expect(API_ROOT).toEqual("http://localhost:8080/");