├── .gitignore ├── Assets ├── Confirmation.png ├── Form.png ├── Homepage.png └── Pizza.jpg ├── README.md ├── Wireframes ├── Form.png ├── HomePage.png └── Pizza.gif ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── _tests_ └── mvp.test.js ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /Assets/Confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Assets/Confirmation.png -------------------------------------------------------------------------------- /Assets/Form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Assets/Form.png -------------------------------------------------------------------------------- /Assets/Homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Assets/Homepage.png -------------------------------------------------------------------------------- /Assets/Pizza.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Assets/Pizza.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Single Page Applications Sprint Challenge 2 | 3 | **Read these instructions carefully. Understand exactly what is expected _before_ starting this Sprint Challenge.** 4 | 5 | **READ ALL INSTRUCTIONS BEFORE REACHING OUT TO ASK A QUESTION!!** 6 | 7 | This challenge allows you to practice the concepts and techniques learned over the past sprint and apply them in a concrete project. This sprint explored **single page applications**. During this sprint, you studied **routing, forms, and testing in cypress**. In your challenge this sprint, you will demonstrate your mastery of these skills by creating **Bloomtech Eats**, a website designed to bring food to hungry coders. 8 | 9 | This is an individual assessment. All work must be your own. You are not allowed to collaborate during the sprint challenge. 10 | 11 | ## Introduction 12 | 13 | In this challenge you will be working from the `Bloomtech Eats` homepage to create a functional button that leads to a build your own pizza custom form. 14 | 15 | You may use the following wireframes as guidance as you design your site but it is not required that you do so. 16 | 17 | [Form](https://github.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/blob/main/Assets/Form.png) 18 | 19 | [Home Page](https://github.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/blob/main/Assets/Homepage.png) 20 | 21 | [Confirmation](https://github.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/blob/main/Assets/Confirmation.png) 22 | 23 | ### Commits 24 | 25 | Commit your code regularly and meaningfully. This helps you in case you ever need to return to old code for any number of reasons. 26 | 27 | ## Instructions 28 | 29 | ### Task 1: Project Set Up 30 | 31 | - [ ] Create a forked copy of this project 32 | - [ ] Clone your OWN version of the repository (Not BloomTech's by mistake!) 33 | - [ ] Implement the project on the main branch, committing changes regularly 34 | - [ ] Push commits: `git push origin main` 35 | 36 | **Note on React Router versions:** 37 | 38 | This project comes with React Router V6 **already installed**. 39 | 40 | If you wish to tackle the challenge in V5, although not advised, you can: 41 | 42 | ```bash 43 | npm i react-router-dom@5.3.4 44 | ``` 45 | 46 | ### Task 2: Project Requirements 47 | 48 | Your finished project must include all of the following requirements: 49 | 50 | - [ ] The `App` component is wrapped in `BrowserRouter` - complete this requirement in the `index.js` module 51 | - [ ] A homepage that has a "/" route and links to your form (button, nav bar, or any other type of link is acceptable but must have an id of "order-pizza") 52 | - [ ] A order form that has a "/pizza" route and shows the form 53 | - [ ] A form with an id of "pizza-form" 54 | - [ ] A name text input field with an id of "name-input" 55 | - [ ] Validation for name and the error message is "name must be at least 2 characters" (Use this exact error message to make sure tests pass) ::: VERY IMPORTANT TO USE THAT EXACT ERROR MESSAGE (casing included!) 56 | - [ ] A dropdown for pizza size with an id of "size-dropdown" 57 | - [ ] A checklist for toppings - at least 4 (hint: name each separately!) 58 | - [ ] Text input for special instructions with an id of "special-text" 59 | - [ ] An Add to Order button that has an id of "order-button" and that submits the form and returns a database record of name, size, toppings and special instructions 60 | 61 | Data should look something like 62 | ``` 63 | { 64 | name: string, 65 | size: string, 66 | topping1: bool, 67 | topping2: bool, 68 | special: string, 69 | } 70 | ``` 71 | Note - your payload should look similar to the above data 72 | 73 | #### Testing MVP 74 | 75 | Implement the following tests in Cypress: 76 | 77 | - [ ] test that you can add text to the box 78 | - [ ] test that you can select multiple toppings 79 | - [ ] test that you can submit the form 80 | 81 | In your solution, it is essential that you follow best practices and produce clean and professional results. You will be scored on your adherence to proper code style and good organization. Schedule time to review, refine, and assess your work and perform basic professional polishing including spell-checking and grammar-checking on your work. It is better to submit a challenge that meets MVP than one that attempts too much and does not. 82 | 83 | Remember for cypress setup to run `npx cypress open`. That will open up a dialogue that has a `Welcome to Cypress 10!` banner and a button below that says `Continue to Cypress 10`. Click that button, then scroll down to the bottom of the next screen and click the three `migrate` buttons one after the other (after clicking one the next section will expand and allow you to click the next migrate selection). On the next screen select the `e2e` option, then select a browser of your choice and click the `Start E2E Testing` button. 84 | 85 | From there in your newly created `cypress` folder in VSCode, navigate to the `e2e` folder and create a new file called `pizza.cy.js` and you're all set to write your tests! 86 | 87 | ## Submission format 88 | 89 | Follow these steps for completing your project. 90 | 91 | Set up your fork on Github to codegrade following the instructions [here](https://bloomtech-1.wistia.com/medias/mpf3xru99v), pushing commits to your main branch. At this point you should be complete with your sprint challenge requirements! 92 | 93 | ### Task 3: Stretch Goals 94 | 95 | After finishing your required elements, you can push your work further. These goals may or may not be things you have learned in this module but they build on the material you just studied. Time allowing, stretch your limits and see if you can deliver on the following optional goals: 96 | 97 | - [ ] Toggle form component for gluten free crust 98 | - [ ] Turn form element sections into nested routes 99 | - [ ] Test more of the application with Cypress 100 | - [ ] Build UI for the eventuality of a network error when POSTing the order 101 | - [ ] Add functionality to your order button that it leads to a Congrats! Pizza is on it's way! page **and** returns a database record of the whole order 102 | 103 | ## Stretch Interview Questions 104 | 105 | Demonstrate your understanding of this sprint's concepts by answering the following free-form questions. Edit this document to include your answers after each question. 106 | 107 | 1. How would you explain form validation to someone who has never programmed before? 108 | 1. In 1-2 sentences, define end to end testing. 109 | 110 | ## FAQs 111 | 112 | **How do I return a database record of the order?** 113 | 114 | One of your goals is to return a database record of the order - for this you'll need to write a post request. For more detailed steps, use the below: 115 | 116 | 1. Create a new state variable + hook 117 | 2. Post to [reqres](https://reqres.in/) with `axios` (the link you should use is step 4) 118 | 3. Log data in console 119 | 4. The URL you should use is `https://reqres.in/api/orders`. The tests are based on this URL. 120 | -------------------------------------------------------------------------------- /Wireframes/Form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Wireframes/Form.png -------------------------------------------------------------------------------- /Wireframes/HomePage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Wireframes/HomePage.png -------------------------------------------------------------------------------- /Wireframes/Pizza.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/Wireframes/Pizza.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-eats", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.2", 7 | "@testing-library/react": "^12.1.3", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^0.26.0", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-router-dom": "^6.8.1", 13 | "react-scripts": "5.0.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-single-page-applications/94e09fc72b9cbb0fcbf64313bbf7cb5016f3be68/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const App = () => { 4 | return ( 5 | <> 6 |

Lambda Eats

7 |

You can remove this code and create your own header

8 | 9 | ); 10 | }; 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/_tests_/mvp.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import axios from 'axios' 3 | import App from "../App"; 4 | import { Router, MemoryRouter, BrowserRouter, Route } from 'react-router-dom' 5 | import { render, screen, fireEvent, waitFor } from "@testing-library/react"; 6 | import userEvent from '@testing-library/user-event' 7 | import "@testing-library/jest-dom"; 8 | 9 | jest.mock('axios') 10 | 11 | const routerVersion = require('react-router-dom/package.json').version 12 | const routerVersionNum = Number(routerVersion.split('.')[0]) 13 | console.log('routerVersion ->', routerVersion); 14 | 15 | if (routerVersionNum === 6) { 16 | it('running react router V6', () => { 17 | expect(routerVersionNum).toBe(6) 18 | }) 19 | describe("Pizza test, sprint 3 challenge", () => { 20 | it('Homepage at "/" route, has link or button with #order-pizza', () => { 21 | render( 22 | 23 | 24 | 25 | ); 26 | expect(document.location.pathname).toBe('/') 27 | const orderPizza = document.querySelector('#order-pizza') 28 | expect(orderPizza).toBeInTheDocument(); 29 | }) 30 | 31 | it('From homepage "/" route, click #order-pizza, navigate to "/pizza" route', () => { 32 | render( 33 | 34 | 35 | 36 | ); 37 | expect(document.location.pathname).toBe('/') 38 | const orderPizza = document.querySelector('#order-pizza') 39 | expect(orderPizza).toBeInTheDocument(); 40 | fireEvent.click(orderPizza) 41 | console.log(document.body.innerHTML) 42 | expect(document.location.pathname).toBe('/pizza') 43 | }); 44 | 45 | it('The "/pizza" route has a form with #pizza-form', () => { 46 | window.history.pushState({}, '', '/pizza') 47 | render( 48 | 49 | 50 | 51 | ); 52 | expect(document.location.pathname).toBe('/pizza') 53 | const pizzaForm = document.querySelector('#pizza-form') 54 | expect(pizzaForm).toBeInTheDocument() 55 | }); 56 | 57 | it('Form has name text input with #name-input', () => { 58 | window.history.pushState({}, '', '/pizza') 59 | render( 60 | 61 | 62 | 63 | ); 64 | expect(document.location.pathname).toBe('/pizza') 65 | const nameInput = document.querySelector('#name-input') 66 | expect(nameInput).toBeInTheDocument() 67 | }); 68 | 69 | it('Form has validation for #name-input with error message "name must be at least 2 characters"', async () => { 70 | window.history.pushState({}, '', '/pizza') 71 | render( 72 | 73 | 74 | 75 | ); 76 | expect(document.location.pathname).toBe('/pizza') 77 | const nameInput = document.querySelector('#name-input') 78 | expect(nameInput).toBeInTheDocument() 79 | fireEvent.input(nameInput, { target: { value: 'a' } }) 80 | await waitFor(() => { 81 | expect(screen.getByText('name must be at least 2 characters')).toBeInTheDocument() 82 | }) 83 | nameInput.value = '' 84 | }); 85 | 86 | it('Form has pizza size dropdown with #size-dropdown', () => { 87 | window.history.pushState({}, '', '/pizza') 88 | render( 89 | 90 | 91 | 92 | ); 93 | expect(document.location.pathname).toBe('/pizza') 94 | const sizeDropdown = document.querySelector('#size-dropdown') 95 | expect(sizeDropdown).toBeInTheDocument() 96 | }); 97 | 98 | it('Form has toppings checklist with at least 4 options', () => { 99 | window.history.pushState({}, '', '/pizza') 100 | render( 101 | 102 | 103 | 104 | ); 105 | expect(document.location.pathname).toBe('/pizza') 106 | const toppingsChecklist = document.querySelectorAll('[type="checkbox"]') 107 | expect(toppingsChecklist.length).toBeGreaterThanOrEqual(4) 108 | }); 109 | 110 | it('Form has special instructions input with #special-text', () => { 111 | window.history.pushState({}, '', '/pizza') 112 | render( 113 | 114 | 115 | 116 | ); 117 | expect(document.location.pathname).toBe('/pizza') 118 | const specialInstructions = document.querySelector('#special-text') 119 | expect(specialInstructions).toBeInTheDocument() 120 | }); 121 | 122 | it("Fill out #pizza-form, submit #pizza-form with data to https://reqres.in/api/orders", async () => { 123 | window.history.pushState({}, '', '/pizza') 124 | render( 125 | 126 | 127 | 128 | ); 129 | expect(document.location.pathname).toBe('/pizza') 130 | const pizzaForm = document.querySelector('#pizza-form') 131 | expect(pizzaForm).toBeInTheDocument() 132 | 133 | const nameInput = document.querySelector('#name-input') 134 | const sizeDropdown = document.querySelector('#size-dropdown') 135 | const toppingsChecklist = document.querySelectorAll('[type="checkbox"]') 136 | const specialInstructions = document.querySelector('#special-text') 137 | const sizeOptions = screen.getAllByRole('option') 138 | 139 | userEvent.type(nameInput, 'Tony Stark') 140 | userEvent.selectOptions(sizeDropdown, sizeOptions[1]) 141 | 142 | userEvent.click(toppingsChecklist[1]) 143 | userEvent.click(toppingsChecklist[2]) 144 | userEvent.type(specialInstructions, 'Here are the special instructions') 145 | 146 | await waitFor(() => { 147 | expect(screen.getByDisplayValue(/tony stark/i)).toBeInTheDocument() 148 | }) 149 | expect(sizeOptions[1].selected).toBe(true) 150 | expect(toppingsChecklist[0]).not.toBeChecked() 151 | expect(toppingsChecklist[1]).toBeChecked() 152 | expect(toppingsChecklist[2]).toBeChecked() 153 | expect(screen.getByDisplayValue(/Here are the special instructions/i)).toBeInTheDocument() 154 | 155 | const testOrder = {} 156 | testOrder[nameInput.name] = nameInput.value 157 | testOrder[sizeDropdown.name] = sizeDropdown.value 158 | testOrder[specialInstructions.name] = specialInstructions.value 159 | toppingsChecklist.forEach(top => { 160 | testOrder[top.name] = top.checked ? true : false 161 | }) 162 | 163 | axios.post.mockImplementationOnce(() => 164 | Promise.resolve({ testOrder }) 165 | ); 166 | const orderButton = document.querySelector('#order-button') 167 | userEvent.click(orderButton) 168 | expect(axios.post("https://reqres.in/api/orders", testOrder)); 169 | }); 170 | }) 171 | } else { 172 | const { createMemoryHistory } = require('history') 173 | it('running react router V5', () => { 174 | expect(routerVersionNum).toBe(5) 175 | }) 176 | describe("Pizza test, sprint 3 challenge", () => { 177 | it('Homepage at "/" route, has link or button with #order-pizza', () => { 178 | const history = createMemoryHistory() 179 | render( 180 | 181 | 182 | 183 | ); 184 | expect(history.location.pathname).toBe('/') 185 | const orderPizza = document.querySelector('#order-pizza') 186 | expect(orderPizza).toBeInTheDocument(); 187 | }) 188 | 189 | it('From homepage "/" route, click #order-pizza, navigate to "/pizza" route', () => { 190 | const history = createMemoryHistory() 191 | render( 192 | 193 | 194 | 195 | ); 196 | expect(history.location.pathname).toBe('/') 197 | const orderPizza = document.querySelector('#order-pizza') 198 | expect(orderPizza).toBeInTheDocument(); 199 | fireEvent.click(orderPizza) 200 | expect(history.location.pathname).toBe('/pizza') 201 | }); 202 | 203 | it('The "/pizza" route has a form with #pizza-form', () => { 204 | let testLocation 205 | render( 206 | 207 | 208 | { 211 | testLocation = location; 212 | return null; 213 | }} 214 | /> 215 | 216 | ); 217 | expect(testLocation.pathname).toBe('/pizza') 218 | const pizzaForm = document.querySelector('#pizza-form') 219 | expect(pizzaForm).toBeInTheDocument() 220 | }); 221 | 222 | it('Form has name text input with #name-input', () => { 223 | let testLocation 224 | render( 225 | 226 | 227 | { 230 | testLocation = location; 231 | return null; 232 | }} 233 | /> 234 | 235 | ); 236 | expect(testLocation.pathname).toBe('/pizza') 237 | const nameInput = document.querySelector('#name-input') 238 | expect(nameInput).toBeInTheDocument() 239 | }); 240 | 241 | it('Form has validation for #name-input with error message "name must be at least 2 characters"', async () => { 242 | let testLocation 243 | render( 244 | 245 | 246 | { 249 | testLocation = location; 250 | return null; 251 | }} 252 | /> 253 | 254 | ); 255 | expect(testLocation.pathname).toBe('/pizza') 256 | const nameInput = document.querySelector('#name-input') 257 | expect(nameInput).toBeInTheDocument() 258 | fireEvent.input(nameInput, { 259 | target: { value: 'a' } 260 | }) 261 | await waitFor(() => { 262 | expect(screen.getByText('name must be at least 2 characters')).toBeInTheDocument() 263 | }) 264 | nameInput.value = '' 265 | }); 266 | 267 | it('Form has pizza size dropdown with #size-dropdown', () => { 268 | let testLocation 269 | render( 270 | 271 | 272 | { 275 | testLocation = location; 276 | return null; 277 | }} 278 | /> 279 | 280 | ); 281 | expect(testLocation.pathname).toBe('/pizza') 282 | const sizeDropdown = document.querySelector('#size-dropdown') 283 | expect(sizeDropdown).toBeInTheDocument() 284 | }); 285 | 286 | it('Form has toppings checklist with at least 4 options', () => { 287 | let testLocation 288 | render( 289 | 290 | 291 | { 294 | testLocation = location; 295 | return null; 296 | }} 297 | /> 298 | 299 | ); 300 | expect(testLocation.pathname).toBe('/pizza') 301 | const toppingsChecklist = document.querySelectorAll('[type="checkbox"]') 302 | expect(toppingsChecklist.length).toBeGreaterThanOrEqual(4) 303 | }); 304 | 305 | it('Form has special instructions input with #special-text', () => { 306 | let testLocation 307 | render( 308 | 309 | 310 | { 313 | testLocation = location; 314 | return null; 315 | }} 316 | /> 317 | 318 | ); 319 | expect(testLocation.pathname).toBe('/pizza') 320 | const specialInstructions = document.querySelector('#special-text') 321 | expect(specialInstructions).toBeInTheDocument() 322 | }); 323 | 324 | it("Fill out #pizza-form, submit #pizza-form with data to https://reqres.in/api/orders", async () => { 325 | let testLocation 326 | render( 327 | 328 | 329 | { 332 | testLocation = location; 333 | return null; 334 | }} 335 | /> 336 | 337 | ); 338 | 339 | expect(testLocation.pathname).toBe('/pizza') 340 | const pizzaForm = document.querySelector('#pizza-form') 341 | expect(pizzaForm).toBeInTheDocument() 342 | 343 | const nameInput = document.querySelector('#name-input') 344 | const sizeDropdown = document.querySelector('#size-dropdown') 345 | const toppingsChecklist = document.querySelectorAll('[type="checkbox"]') 346 | const specialInstructions = document.querySelector('#special-text') 347 | const sizeOptions = screen.getAllByRole('option') 348 | 349 | userEvent.type(nameInput, 'Tony Stark') 350 | userEvent.selectOptions(sizeDropdown, sizeOptions[1]) 351 | 352 | userEvent.click(toppingsChecklist[1]) 353 | userEvent.click(toppingsChecklist[2]) 354 | userEvent.type(specialInstructions, 'Here are the special instructions') 355 | 356 | await waitFor(() => { 357 | expect(screen.getByDisplayValue(/tony stark/i)).toBeInTheDocument() 358 | }) 359 | expect(sizeOptions[1].selected).toBe(true) 360 | expect(toppingsChecklist[0]).not.toBeChecked() 361 | expect(toppingsChecklist[1]).toBeChecked() 362 | expect(toppingsChecklist[2]).toBeChecked() 363 | expect(screen.getByDisplayValue(/Here are the special instructions/i)).toBeInTheDocument() 364 | 365 | const testOrder = {} 366 | testOrder[nameInput.name] = nameInput.value 367 | testOrder[sizeDropdown.name] = sizeDropdown.value 368 | testOrder[specialInstructions.name] = specialInstructions.value 369 | toppingsChecklist.forEach(top => { 370 | testOrder[top.name] = top.checked ? true : false 371 | }) 372 | 373 | axios.post.mockImplementationOnce(() => 374 | Promise.resolve({ testOrder }) 375 | ); 376 | const orderButton = document.querySelector('#order-button') 377 | userEvent.click(orderButton) 378 | expect(axios.post("https://reqres.in/api/orders", testOrder)); 379 | }); 380 | }); 381 | } 382 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | --------------------------------------------------------------------------------