diff --git a/frontend/components/App.test.js b/frontend/components/App.test.js index 96965c559..eba7fb0b6 100644 --- a/frontend/components/App.test.js +++ b/frontend/components/App.test.js @@ -1,4 +1,72 @@ +import React from 'react' +import AppFunctional from './AppFunctional' +import { render, fireEvent, screen } from '@testing-library/react' +import '@testing-library/jest-dom/extend-expect' // Write your tests here test('sanity', () => { - expect(true).toBe(false) + expect(true).toBe(true) }) + + +test('header renders', () => { + render() + + const coordinates = screen.queryByText(/coordinates/i) + const upButton = screen.queryByText('UP') + const leftButton = screen.queryByText('LEFT') + const rightButton = screen.queryByText('RIGHT') + const downButton = screen.queryByText('DOWN') + const resetButton = screen.queryByText('reset') + + expect(coordinates).toBeInTheDocument() + expect(leftButton).toBeInTheDocument() + expect(rightButton).toBeInTheDocument() + expect(upButton).toBeInTheDocument() + expect(downButton).toBeInTheDocument() + expect(resetButton).toBeInTheDocument() +}) + +test('typing in input results in text entered', () => { + render() + + const inputBox = screen.getByRole('textbox', {id:'email'}) + + expect(inputBox) + .toBeInTheDocument() + fireEvent.change(inputBox, { target: {value: 'pizzatime'}}) + expect(inputBox) + .toHaveValue('pizzatime') +}) + +test('clicking reset clears input box', () => { + render() + + const inputBox = screen.getByRole('textbox', {id:'email'}) + const resetButton = screen.getByTestId('reset') + + fireEvent.change(inputBox, { target: {value: 'pizzatime'}}) + expect(inputBox) + .toHaveValue('pizzatime') + fireEvent.click(resetButton) + expect(inputBox) + .toHaveValue('') +}) + +test('cannot go up past bounds', () => { + render() + + const upButton = screen.getByTestId('up') + + fireEvent.click(upButton) + fireEvent.click(upButton) + expect(screen.getByText("You can't go up")).toBeInTheDocument() +}) + +test('displays moves', () => { + render() + + const upButton = screen.getByTestId('up') + + fireEvent.click(upButton) + expect(screen.getByText("You moved 1 time")).toBeInTheDocument() +}) \ No newline at end of file diff --git a/frontend/components/AppClass.js b/frontend/components/AppClass.js index 5b964060a..ce73c61ab 100644 --- a/frontend/components/AppClass.js +++ b/frontend/components/AppClass.js @@ -1,10 +1,23 @@ -import React from 'react' +import React, {useState} from 'react' +import axios from 'axios' +import * as yup from 'yup' + + +const formSchema = yup.object().shape({ + formValue: yup + .string() + .email('Ouch: email must be a valid email') + .required('Ouch: email is required') + .notOneOf(['foo@bar.baz'],'foo@bar.baz failure #71') +}) // Suggested initial states const initialMessage = '' const initialEmail = '' const initialSteps = 0 const initialIndex = 4 // the index the "B" is at +const initialX = 2 +const initialY = 2 const initialState = { message: initialMessage, @@ -14,41 +27,110 @@ const initialState = { } export default class AppClass extends React.Component { + constructor(){ + super() + this.state= { + x: initialX, + y: initialY, + steps: initialSteps, + xy: initialIndex, + message: initialMessage, + formValues: '' + } + + } // THE FOLLOWING HELPERS ARE JUST RECOMMENDATIONS. // You can delete them and build your own logic from scratch. getXY = () => { + return(`(${this.state.x},${this.state.y})`) + // It it not necessary to have a state to track the coordinates. // It's enough to know what index the "B" is at, to be able to calculate them. } - getXYMessage = () => { - // It it not necessary to have a state to track the "Coordinates (2, 2)" message for the user. - // You can use the `getXY` helper above to obtain the coordinates, and then `getXYMessage` - // returns the fully constructed string. - } - reset = () => { - // Use this helper to reset all states to their initial values. + this.setState({ + x: initialX, + y: initialY, + steps: initialSteps, + message: initialMessage, + xy: initialIndex, + formValues: '' + }) } getNextIndex = (direction) => { + if(direction === 'left'){ + if(this.state.x - 1 === 0){ + return ({"x": this.state.x, "y":this.state.y}) + } + return ({"x": this.state.x - 1, "y":this.state.y,"xy":this.state.xy -1,"steps": this.state.steps + 1}) + } + if(direction === 'right'){ + if(this.state.x + 1 === 4){ + return ({"x": this.state.x, "y":this.state.y}) + } + return ({"x": this.state.x + 1, "y":this.state.y,"xy":this.state.xy + 1,"steps": this.state.steps + 1}) + } + if(direction === 'up'){ + if(this.state.y - 1 === 0){ + return ({"x": this.state.x, "y":this.state.y}) + } + return ({"x": this.state.x, "y":this.state.y - 1,"xy":this.state.xy - 3,"steps": this.state.steps + 1}) + } + if(direction === 'down'){ + if(this.state.y + 1 === 4){ + return ({"x": this.state.x, "y":this.state.y}) + } + return ({"x": this.state.x, "y":this.state.y + 1,"xy":this.state.xy + 3,"steps": this.state.steps + 1}) + } // This helper takes a direction ("left", "up", etc) and calculates what the next index // of the "B" would be. If the move is impossible because we are at the edge of the grid, // this helper should return the current index unchanged. } move = (evt) => { + let nextMove = this.getNextIndex(evt.target.id) + if (`(${nextMove.x},${nextMove.y})` === this.getXY()){ + return this.setState({message: `You can't go ${evt.target.id}`}) + } + this.setState({...this.state, + message: initialMessage, + x: nextMove.x, + y: nextMove.y, + steps: nextMove.steps, + xy: nextMove.xy}) // This event handler can use the helper above to obtain a new index for the "B", // and change any states accordingly. } onChange = (evt) => { - // You will need this to update the value of the input. + this.setState({formValues: evt.target.value}) + } + + validate = (name,value) => { + yup.reach(formSchema, name) + .validate(value) + .then(() => this.post()) + .catch(err => this.setState({message:err.errors[0]})) + } + + post = () => { + const toSend = { + "x": this.state.x, + "y": this.state.y, + "steps": this.state.steps, + "email": this.state.formValues + } + axios.post('http://localhost:9000/api/result', toSend) + .then(({data}) => {this.setState({message: data.message})}) + .finally(this.setState({formValues: ''})) } onSubmit = (evt) => { - // Use a POST request to send a payload to the server. + evt.preventDefault() + this.validate('formValue', this.state.formValues) } render() { @@ -56,33 +138,33 @@ export default class AppClass extends React.Component { return (
-

Coordinates (2, 2)

-

You moved 0 times

+

{`Coordinates ${this.getXY()}`}

+

{`You moved ${this.state.steps} ${this.state.steps === 1 ? 'time' : 'times'}`}

{/*Stateful move tracker*/}
{ [0, 1, 2, 3, 4, 5, 6, 7, 8].map(idx => ( -
- {idx === 4 ? 'B' : null} +
+ {idx === this.state.xy ? 'B' : null}
)) }
-

+

{this.state.message}

{/*display message from API call*/}
- - - - - + + + + +
-
- + this.onSubmit(e)}> + this.onChange(e)}>
) } -} +} \ No newline at end of file diff --git a/frontend/components/AppFunctional.js b/frontend/components/AppFunctional.js index 4c2b53a98..691d1cd63 100644 --- a/frontend/components/AppFunctional.js +++ b/frontend/components/AppFunctional.js @@ -1,78 +1,164 @@ -import React from 'react' +import React, {useState} from 'react' +import axios from 'axios' +import * as yup from 'yup' + +const formSchema = yup.object().shape({ + formValue: yup + .string() + .email('Ouch: email must be a valid email') + .required('Ouch: email is required') + .notOneOf(['foo@bar.baz'],'foo@bar.baz failure #71') +}) // Suggested initial states const initialMessage = '' const initialEmail = '' const initialSteps = 0 const initialIndex = 4 // the index the "B" is at +const initialX = 2 +const initialY = 2 + + export default function AppFunctional(props) { // THE FOLLOWING HELPERS ARE JUST RECOMMENDATIONS. // You can delete them and build your own logic from scratch. + const [x, setX] = useState(initialX) + const [y, setY] = useState(initialY) + const [xy, setXY] = useState(initialIndex) + const [moves, setMoves] = useState(0) + const [messages, setMessages] = useState(initialMessage) + const [formValue, setFormValue] = useState('') - function getXY() { + function getXY(){ + return (`(${x},${y})`) // It it not necessary to have a state to track the coordinates. // It's enough to know what index the "B" is at, to be able to calculate them. } - function getXYMessage() { - // It it not necessary to have a state to track the "Coordinates (2, 2)" message for the user. - // You can use the `getXY` helper above to obtain the coordinates, and then `getXYMessage` - // returns the fully constructed string. - } - function reset() { // Use this helper to reset all states to their initial values. + setMoves(0) + setXY(initialIndex) + setX(initialX) + setY(initialY) + setMessages(initialMessage) + setFormValue('') } function getNextIndex(direction) { + if(direction === 'left'){ + if (x - 1 === 0){ + setMessages("You can't go left") + return xy + } + setX(x-1) + setXY(xy - 1) + setMoves(moves + 1) + setMessages(initialMessage) + } + if(direction === 'down'){ + if (y + 1 === 4){ + setMessages("You can't go down") + return xy + } + setY(y+1) + setXY(xy + 3) + setMoves(moves + 1) + setMessages(initialMessage) + } + if(direction === 'right'){ + if (x + 1 === 4){ + setMessages("You can't go right") + return xy + } + setX(x+1) + setXY(xy + 1) + setMoves(moves + 1) + setMessages(initialMessage) + } + if(direction === 'up'){ + if (y - 1 === 0){ + setMessages("You can't go up") + return xy + } + setY(y-1) + setXY(xy - 3) + setMoves(moves + 1) + setMessages(initialMessage) + } + + + // This helper takes a direction ("left", "up", etc) and calculates what the next index // of the "B" would be. If the move is impossible because we are at the edge of the grid, // this helper should return the current index unchanged. } function move(evt) { + getNextIndex(evt) + // This event handler can use the helper above to obtain a new index for the "B", // and change any states accordingly. } function onChange(evt) { - // You will need this to update the value of the input. + setFormValue(evt.target.value) + } + + const validate = (name,value) => { + yup.reach(formSchema, name) + .validate(value) + .then(() => post()) + .catch(err => setMessages(err.errors[0])) } function onSubmit(evt) { - // Use a POST request to send a payload to the server. + evt.preventDefault() + validate('formValue', formValue); + } + + function post(){ + const toSend = { + "x": x, + "y": y, + "steps": moves, + "email": formValue + } + axios.post('http://localhost:9000/api/result', toSend) + .then(({data}) => {setMessages(data.message)}) + .finally(setFormValue('')) } return (
-

Coordinates (2, 2)

-

You moved 0 times

+

{`Coordinates ${getXY()}`}

+

{`You moved ${moves} ${moves === 1 ? 'time' : 'times'}`}

{ [0, 1, 2, 3, 4, 5, 6, 7, 8].map(idx => ( -
- {idx === 4 ? 'B' : null} +
+ {idx === xy ? 'B' : null}
)) }
-

+

{messages}

- - - - - + + + + +
-
- - + onSubmit(e)}> + onChange(e)}> +
) -} +} \ No newline at end of file