├── .github └── workflows │ ├── charity.yml │ ├── good2.yml │ └── tado.sec.yml ├── .gitignore ├── LICENSE ├── README.md ├── Sauce Con 2021.pdf ├── automated-atomic-tests ├── ATOMIC-TESTS.md ├── cypress.json ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ ├── exercise.spec.js │ │ └── solution.spec.js │ ├── page-objects │ │ ├── AppHeaderPage.js │ │ ├── CartComponent.js │ │ ├── CartSummaryPage.js │ │ ├── CheckoutCompletePage.js │ │ ├── CheckoutPersonalInfoPage.js │ │ ├── CheckoutSummaryPage.js │ │ ├── LoginPage.js │ │ ├── MenuPage.js │ │ ├── ProductsPage.js │ │ └── SwagDetailsPage.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ ├── e2eConstants.js │ │ └── index.js ├── package-lock.json └── package.json ├── graphics ├── bye.gif ├── component-diagram.jpeg ├── josh-grant.jpeg ├── me-and-mia.jpg ├── secrets.png ├── testing-good.jpeg ├── thanks.gif ├── visual-testing.png └── visual-workflow.jpeg ├── login-testing ├── LOGINS.md └── images │ ├── TokenAdded.png │ └── testing comparison chart.png ├── my-react-app ├── .gitignore ├── FULL-COVERAGE.md ├── cypress.json ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ ├── exercise.spec.js │ │ └── solution.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js ├── docs │ ├── CICD.md │ ├── FRONT-END-PERF.md │ ├── GETTING-STARTED-REACT.md │ └── VISUAL.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── __tests__ │ │ ├── Exercise.test.js │ │ └── Solution.test.js │ ├── extra-components │ │ ├── App.js │ │ ├── CheckUserAge.js │ │ ├── MagicEightBall.js │ │ └── MyToDoList.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── mia.jpg │ ├── reportWebVitals.js │ └── setupTests.js ├── test │ └── specs │ │ └── visual.solution.spec.js └── wdio.conf.js ├── package-lock.json ├── package.json ├── tado-sec ├── README.md └── my-react-app │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── solution │ ├── .gitignore │ ├── docs │ │ ├── CONCLUSIONS.md │ │ ├── CROSS-PLATFORM.md │ │ └── LOCAL-SAUCE-TESTS.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── __tests__ │ │ │ ├── App.test.js │ │ │ ├── Exercise.test.js │ │ │ └── Solution.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── mia-me.jpg │ │ ├── mia.jpg │ │ ├── mia2.jpg │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ ├── test │ │ ├── configs │ │ │ ├── wdio.cross.platform.sauce.conf.js │ │ │ ├── wdio.localhost.sauce.conf.js │ │ │ └── wdio.sanity.sauce.conf.js │ │ └── specs │ │ │ ├── cross.platform.2.spec.js │ │ │ ├── cross.platform.spec.js │ │ │ ├── localhost.spec.js │ │ │ └── sanity.spec.js │ └── wdio.local.conf.js │ ├── src │ ├── App.css │ ├── App.js │ ├── __tests__ │ │ ├── App.test.js │ │ ├── Exercise.test.js │ │ └── Solution.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── mia-me.jpg │ ├── mia.jpg │ ├── mia2.jpg │ ├── reportWebVitals.js │ └── setupTests.js │ ├── test │ ├── configs │ │ ├── wdio.localhost.sauce.conf.js │ │ ├── wdio.sanity.sauce.conf.js │ │ └── wdio.sauce.conf.js │ └── specs │ │ ├── localhost.spec.js │ │ ├── login.spec.js │ │ └── sanity.spec.js │ └── wdio.local.conf.js ├── testing-for-charity ├── README.md └── my-react-app │ ├── .gitignore │ ├── cypress.json │ ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ ├── exercise.spec.js │ │ └── solution.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js │ ├── docs │ ├── CICD.md │ ├── COMPONENT-TESTS.md │ ├── CONCLUSIONS.md │ ├── E2E-TESTS.md │ ├── FRONT-END-PERF.md │ ├── GETTING-STARTED-REACT.md │ ├── TEST-COVERAGE.md │ ├── VISUAL-COMPONENT.md │ └── VISUAL.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── __tests__ │ │ ├── App.test.js │ │ ├── Exercise.test.js │ │ └── Solution.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── mia.jpg │ ├── mia2.jpg │ ├── reportWebVitals.js │ └── setupTests.js │ ├── test │ └── specs │ │ └── visual.solution.spec.js │ └── wdio.conf.js └── visual-demo ├── README.md ├── package-lock.json ├── package.json ├── pageObject ├── home.page.js └── page.js ├── test └── specs │ └── visualChecks │ ├── uk.chrome.spec.js │ ├── uk.safari.spec.js │ ├── us.chrome.spec.js │ ├── us.v2.safariBroken.spec.js │ └── weirdBehavior.spec.js ├── wdio.chrome.conf.js └── wdio.safari.conf.js /.github/workflows/charity.yml: -------------------------------------------------------------------------------- 1 | name: Testing for Charity 2 | env: 3 | SCREENER_API_KEY: ${{ secrets.SCREENER_API_KEY }} 4 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 5 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 6 | 7 | on: 8 | push: 9 | # Only trigger if files in this path changed 10 | paths: 11 | - 'testing-for-charity/my-react-app/**' 12 | - '.github/workflows/charity.yml' 13 | # Don't run on Markdown changes 14 | - '!**.md' 15 | branches: [ main ] 16 | pull_request: 17 | # Only trigger if files in this path changed 18 | paths: 19 | - 'testing-for-charity/my-react-app/**' 20 | - '.github/workflows/charity.yml' 21 | # Don't run on Markdown changes 22 | - '!**.md' 23 | branches: [ main ] 24 | 25 | jobs: 26 | build: 27 | 28 | runs-on: ubuntu-latest 29 | 30 | strategy: 31 | matrix: 32 | node-version: [14.x] 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Use Node.js ${{ matrix.node-version }} 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | - name: Install dependencies 📦 41 | #Using npm ci is generally faster than running npm install 42 | run: | 43 | cd testing-for-charity/my-react-app 44 | npm ci 45 | - name: Build the app 🏗 46 | run: | 47 | cd testing-for-charity/my-react-app 48 | npm run build 49 | # If we had more time, at this point we can actually deploy our app 50 | # to a staging server and then run functional tests 51 | - name: Start the app 📤 52 | run: | 53 | cd testing-for-charity/my-react-app 54 | npm start & 55 | npx wait-on --timeout 60000 56 | - name: Run functional UI tests 🖥 57 | run: | 58 | cd testing-for-charity/my-react-app 59 | npm run cy:ci 60 | - name: Run visual tests 👁 61 | run: | 62 | cd testing-for-charity/my-react-app 63 | npm run test:visual 64 | -------------------------------------------------------------------------------- /.github/workflows/good2.yml: -------------------------------------------------------------------------------- 1 | name: Testing for Good 2 | env: 3 | SCREENER_API_KEY: ${{ secrets.SCREENER_API_KEY }} 4 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 5 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 6 | 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [14.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - name: Install dependencies 📦 29 | #Using npm ci is generally faster than running npm install 30 | run: | 31 | cd testing-for-charity/my-react-app 32 | npm ci 33 | - name: Build the app 🏗 34 | run: | 35 | cd testing-for-charity/my-react-app 36 | npm run build 37 | # If we had more time, at this point we can actually deploy our app 38 | # to a staging server and then run functional tests 39 | - name: Start the app 📤 40 | run: | 41 | cd testing-for-charity/my-react-app 42 | npm start & 43 | npx wait-on --timeout 60000 44 | - name: Run functional UI tests 🖥 45 | run: | 46 | cd testing-for-charity/my-react-app 47 | npm run cy:ci 48 | - name: Run visual tests 👁 49 | run: | 50 | cd testing-for-charity/my-react-app 51 | npm run test:visual -------------------------------------------------------------------------------- /.github/workflows/tado.sec.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: TADO Sec 5 | 6 | on: 7 | push: 8 | # Only trigger if files in this path changed 9 | paths: 10 | - 'tado-sec/my-react-app/**' 11 | - '.github/workflows/tado.sec.yml' 12 | # Don't run on Markdown changes 13 | - '!**.md' 14 | branches: [ main ] 15 | pull_request: 16 | # Only trigger if files in this path changed 17 | paths: 18 | - 'tado-sec/my-react-app/**' 19 | - '.github/workflows/tado.sec.yml' 20 | # Don't run on Markdown changes 21 | - '!**.md' 22 | branches: [ main ] 23 | 24 | jobs: 25 | build: 26 | 27 | name: test-${{matrix.os}}-${{matrix.node-version}} 28 | strategy: 29 | #fail-fast: false 30 | matrix: 31 | os: [windows-latest, macOS-latest] 32 | node-version: [14.x, 16.x] 33 | runs-on: ${{ matrix.os }} 34 | 35 | defaults: 36 | run: 37 | working-directory: ./tado-sec/my-react-app/ 38 | env: 39 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 40 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Use Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v2 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | cache: 'npm' 49 | - run: npm ci 50 | - name: Build the app 🏗 51 | run: npm run build 52 | - name: Start the app 📤 53 | run: | 54 | npm start & 55 | npx wait-on --timeout 60000 56 | # it's very useful to have extra logging in a ci execution 57 | - run: npm run test.sanity.ci 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | *.mp4 9 | **/cypress/videos/**.mp4 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 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 | 109 | my-react-app/cypress/screenshots/solution.spec.js/The React app -- should click link (failed).png 110 | automated-atomic-tests/cypress/screenshots/shopping-cart.spec.js/Shopping cart -- should add item to cart (failed).png 111 | tado-sec/.DS_Store 112 | tado-sec/my-react-app/package-lock.json 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 saucelabs-training 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # automation best practices workshop 2 | 3 | In this automation best practices workshop you will learn the latest and greatest tools and techniques to drastically improve your testing! 4 | 5 | We will focus on a holistic approach of testing front-end and back-end, web and APIs, functional testing, component testing, and many other things in between 😁 6 | 7 | ## 🧠You will learn 8 | 9 | ✅What is an automated atomic test 10 | 11 | ✅How to code automated atomic tests 12 | 13 | ✅How to login without a UI using a HTML web forms 14 | 15 | ✅How to login without a UI using JWT 16 | 17 | ✅How to write a component test 18 | 19 | ✅How to add a test id to our web app 20 | 21 | ✅How to correctly test a link and a tab 22 | 23 | ✅How to replace e2e tests with component tests 24 | 25 | ✅visual e2e tests 26 | 27 | ✅visual cross-browser tests 28 | 29 | ✅CICD with Github Actions 30 | 31 | ## 🔧Technologies you will use 32 | 33 | 1. ReactJS 34 | 2. Cypress 35 | 3. WebdriverIO 36 | 4. React testing library 37 | 5. Jest 38 | 6. Screener visual E2E testing 39 | 7. Sauce Labs 40 | 8. Github Actions 41 | 42 | ## Table Of Contents 43 | 44 | * [Automated atomic tests](./automated-atomic-tests/ATOMIC-TESTS.md) 45 | * [Login testing](./login-testing/LOGINS.md) 46 | * [Full coverage testing](./my-react-app/FULL-COVERAGE.md) 47 | * [Visual E2E testing](./my-react-app/docs/VISUAL.md) 48 | * [CICD](./my-react-app/docs/CICD.md) 49 | 50 | 51 | ## ⚙️ Setup 52 | 53 | ### 1. Install Node 14 LTS 54 | - Use NVM for this installation by [following instructions](https://github.com/nvm-sh/nvm#install--update-script) 55 | - Pre-requisites that make the process work 56 | - Make sure that a `~/.bash_profile` exists. Check with `open -e ~/.bash_profile`. If nothing opens then create with `touch ~/.bash_profile` 57 | - With VS Code, make sure that your terminal corresponds to the installation location of NVM. For example, if NVM was installed to `~/.bash_profile` then make sure that you are using the `bash` terminal and not `zsh` or another one 58 | - It should be just a single command to run in our terminal 59 | - **!Don't forget to restart your terminal!** 60 | - After installation, confirm install was correct by running `nvm` and seeing an output 61 | - Intall Node 14 with `nvm install 14` 62 | 63 | Here's what the output would look like: 64 | ``` 65 | Downloading and installing node v14.16.1... 66 | Downloading https://nodejs.org/dist/v14.16.1/node-v14.16.1-darwin-x64.tar.xz... 67 | ######################################################################### 100.0% 68 | Computing checksum with shasum -a 256 69 | Checksums matched! 70 | Now using node v14.16.1 (npm v6.14.12) 71 | Creating default alias: default -> 14 (-> v14.16.1) 72 | ``` 73 | 74 | * Confirm node installation with `node --version` and seeing `v14.16.1` or similar 75 | * Confirm NVM is set to 14 for default by running the following commands: 76 | 77 | ```bash 78 | nvm list #will show all versions 79 | nvm use 14 #to use 14 80 | nvm alias default 14.16.x #to set it to the default 81 | ``` 82 | 83 | 84 | 85 | ### 2.Clone and fork the repo 86 | 1. Sign up for a free [Github account](https://github.com/) 87 | 2. Fork this respository 88 | * Make sure you are logged into Github 89 | * click the Fork in the upper right of the Github. 90 | 3. Clone your fork of the repository to your machine. Must have [Git installed](https://git-scm.com/downloads) 91 | 92 | ```bash 93 | git clone URL_OF_YOUR_FORK 94 | ``` 95 | 4. **Navigate to the directory of where you cloned your repo** 96 | 97 | `cd YOUR_FORK_DIR/automation-best-practices` 98 | 99 | ### 3.Install the app 100 | ```bash 101 | cd my-react-app 102 | npm install 103 | npm run start 104 | ``` 105 | 106 | Expected Output: 107 | 108 | Your output should look similar to this 109 | ``` 110 | Compiled successfully! 111 | 112 | You can now view my-react-app in the browser. 113 | 114 | Local: http://localhost:3000 115 | On Your Network: http://172.20.10.2:3000 116 | 117 | Note that the development build is not optimized. 118 | To create a production build, use npm run build. 119 | ``` 120 | **Don't worry about fixing any warnings that may appear during `npm install`** 121 | 122 | Once the app is running, kill the server by executing `Ctrl + C` in command line. We don't need the app running right now. 123 | 124 | ### 4.Have an IDE installed that can handle NodeJS (We will use [VSCode](https://code.visualstudio.com/Download)) 125 | 126 | #### ✅👏Congratulations, you're 90% ready! 127 | 128 | ### 5.Fork, clone, install the cypress-examples repo 129 | 130 | We will have a 2nd repo from which we work from only for login testing. 131 | 132 | 1. Fork https://github.com/nadvolod/cypress-example-recipes 133 | 2. Clone this repo to another directory (we will open it as a separate project later in the workshop) 134 | 3. Install everything 135 | 136 | 137 | ```bash 138 | cd cypress-example-recipes 139 | npm i 140 | ``` 141 | 142 | ### 5.Sign up for a free [Sauce account](https://saucelabs.com/sign-up) 143 | 144 | ### 6.Set Your Sauce Labs Credentials 145 | 1. Copy your Sauce Labs **username** and **accessKey** in the [User Settings](https://app.saucelabs.com/user-settings) section of the [Sauce Labs Dashboard](https://app.saucelabs.com/dashboard/builds). 146 | 2. Open a Terminal window (command prompt for Windows) and set your Sauce Labs Environment variables: 147 | ###### Mac OSX: 148 | ``` 149 | $ export SAUCE_USERNAME="username" 150 | $ export SAUCE_ACCESS_KEY="accessKey" 151 | ``` 152 | ###### Windows: 153 | ``` 154 | > set SAUCE_USERNAME="username" 155 | > set SAUCE_ACCESS_KEY="accessKey" 156 | ``` 157 | > To set an environment variables permanently in Windows, you must append it to the `PATH` variable. 158 | 159 | > Go to **Control Panel > System > Windows version > Advanced System Settings > Environment Variables > System Variables > Edit > New** 160 | 161 | > Then set the "Name" and "Value" for each variable 162 | 163 | 9. Test the environment variables 164 | ###### Mac OSX: 165 | ``` 166 | $ echo $SAUCE_USERNAME 167 | $ echo $SAUCE_ACCESS_KEY 168 | ``` 169 | > ***WARNING FOR UNIX USERS!***: 170 | > If you have problems setting your environment variables, run the following commands in your terminal: 171 | ``` 172 | $ launchctl setenv SAUCE_USERNAME $SAUCE_USERNAME 173 | $ launchctl setenv SAUCE_ACCESS_KEY $SAUCE_ACCESS_KEY 174 | ``` 175 | ###### Windows: 176 | ``` 177 | > echo %SAUCE_USERNAME% 178 | > echo %SAUCE_ACCESS_KEY% 179 | ``` 180 | 181 | 182 | 183 | 184 | 185 | ## Key 186 | 187 | 💡 this is a tip 188 | 189 | 🏋️‍♀️ this is an exercise for you to do 190 | 191 | ❓ this is a question for us to think and talk about. Try not to scroll beyond this question before we discuss 192 | -------------------------------------------------------------------------------- /Sauce Con 2021.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/Sauce Con 2021.pdf -------------------------------------------------------------------------------- /automated-atomic-tests/ATOMIC-TESTS.md: -------------------------------------------------------------------------------- 1 | # Automated Atomic Tests 2 | 3 | ## 🧠You will learn: 4 | 5 | ✅What is an automated atomic test 6 | 7 | ✅How to code automated atomic tests 8 | 9 | ## SUT 10 | 11 | Let's quickly explore the software that we will test. 12 | 13 | 1. Download Edge browser (our demo app is not working correctly in chrome) 14 | 2. Open the demo app at `https://www.saucedemo.com/v1` 15 | * **The app has some old issues and you will need to open in Incognito Chrome or on Edge. The same thing will apply when we test it with Cypress.** 16 | 4. Try a login (the login works by setting session storage) 17 | 5. Try to add a product 18 | 19 | ## Automated atomic tests 20 | 21 | An automated atomic test (AAT) is one that tests only a single feature or component. AAT have very few UI interactions and typically touch a maximum of two screens. The "typical" UI end-to-end tests break the AAT pattern. 22 | 23 | Furthermore, AATs meet several requirements of [good tests as specified by Kent Beck](https://github.com/nadvolod/testing-best-practices/blob/main/README.md#what-is-a-good-test-1) 24 | 25 | ✅ Isolated 26 | 27 | ✅ Composable 28 | 29 | ✅ Fast 30 | 31 | 32 | 33 | As an aside, this concept is already well understood in unit and integration tests, but UI tests continue to lag behind. 34 | 35 | ## ❓Is this test atomic❓ 36 | 37 | ```js 38 | 39 | /// 40 | import ProductsPage from '../page-objects/ProductsPage'; 41 | import AppHeader from '../page-objects/AppHeaderPage'; 42 | import LoginPage from '../page-objects/LoginPage' 43 | import {LOGIN_USERS} from "../support/e2eConstants"; 44 | 45 | describe('Shopping cart', () => { 46 | beforeEach(() => { 47 | cy.visit('https://www.saucedemo.com/v1'); 48 | cy.window().then((win) => { 49 | win.sessionStorage.clear() 50 | }); 51 | }); 52 | 53 | it('should add item to cart', () => { 54 | LoginPage.signIn(LOGIN_USERS.STANDARD); 55 | ProductsPage.screen.should('be.visible'); 56 | ProductsPage.addItemToCart(0); 57 | AppHeader.cart.should('have.text', '1'); 58 | }); 59 | }); 60 | 61 | ``` 62 | 63 | ❓So how many tests is this really❓ 64 | 65 | > Keep in mind that this non-atomic test is really small and larger tests will be even 66 | > more of a hinderance to your testing 67 | 68 | ### 🏋️‍♀️ Get started with Cypress 69 | 70 | 1. `cd automated-atomic-tests` 71 | 2. `npm install` 72 | 3. `npx cypress open` 73 | 4. Open `exercise.spec.js` in Cypress UI and make sure that Edge browser is selected. Please don't peek at the `solution.spec.js`🙏. The workshop is more fun when we struggle together 😁 74 | 1. The test files are also in the directory `automated-atomic-tests/cypress/integration` 75 | 76 | #### 👀Cypress and code overview 77 | 78 | ### 🏋️‍♀️ Automated atomic tests exercise 79 | 80 | We're going to break down this test into atomic ones. 81 | 82 | 🏋️‍♀️ Code a suite of atomic tests 83 | 84 | 1. Go to the `cypress/integration/exercise.spec.js` 85 | 2. Create AATs for all of the features 86 | 87 | 💡 Use this command to bypass the UI login 88 | 89 | ```js 90 | //setTestContext() defined in support/commands.js 91 | cy.setTestContext({ 92 | user: LOGIN_USERS.STANDARD, 93 | path: PAGES.INVENTORY 94 | }); 95 | 96 | ``` 97 | 98 | ## 📔Summary 99 | 100 | ✅ Automated atomic tests validate a single feature 101 | 102 | ✅ Testing a login through the UI is only necessary once 103 | 104 | ✅ We can bypass a login by directly modifying sessionStorage in the browser (although most apps don't actually function like this) 105 | 106 | 🏃‍♀️Let's learn more about [automating logins](../login-testing/LOGINS.md) 107 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "https://www.saucedemo.com/v1", 3 | "//":"disable chromeWebSecurity because our demo app has some insecure content", 4 | "chromeWebSecurity": false 5 | } 6 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/integration/exercise.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | import LoginPage from '../page-objects/LoginPage'; 3 | import ProductsPage from '../page-objects/ProductsPage'; 4 | import AppHeader from '../page-objects/AppHeaderPage'; 5 | import { LOGIN_USERS, PAGES } from '../support/e2eConstants'; 6 | 7 | describe('Exercise for automated atomic tests', () => { 8 | context('Home page', ()=>{ 9 | //runs before each test 10 | beforeEach(() => { 11 | // Open the baseUrl that comes from cypress.json 12 | cy.visit(''); 13 | // it's important to clear session storage so that it doesn't persist 14 | // between tests 15 | cy.window().then((win) => { 16 | win.sessionStorage.clear() 17 | }); 18 | }); 19 | 20 | it('should be visible', () => { 21 | /** Your code below */ 22 | //Use the LoginPage page to check that the `screen.should('be.visible')` 23 | /** Your code above */ 24 | }); 25 | 26 | it('allows UI login with a standard user', () => { 27 | /** Your code below */ 28 | 29 | //1. Use the LoginPage page to signIn(LOGIN_USERS.STANDARD) 30 | //2. Now check that the ProductsPage.screen is visible. 💡 the same as the test above, only with a different page object 31 | 32 | /** Your code above */ 33 | }); 34 | }); 35 | 36 | // typically we would actually put this into a separate file like Producs.spec.js 37 | // and have another context(). However, 2 contexts() exist only for demonstration 38 | context('Products', () => { 39 | it('should be added to cart without UI login', () => { 40 | /** Your code below */ 41 | 42 | //1. Use the setTestContext() to set the sessionStorage first 43 | //2. Use the ProductsPage to addItemToCart(0); 44 | //3. Now AppHeader.cart should('have.text', '1'); 45 | 46 | /** Your code above */ 47 | }); 48 | }); 49 | }); -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/integration/solution.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | import LoginPage from '../page-objects/LoginPage'; 3 | import ProductsPage from '../page-objects/ProductsPage'; 4 | import AppHeader from '../page-objects/AppHeaderPage'; 5 | import { LOGIN_USERS, PAGES } from '../support/e2eConstants'; 6 | 7 | describe('Solution for automated atomic tests', () => { 8 | context('Home page', ()=>{ 9 | beforeEach(() => { 10 | // the url comes from cypress.json 11 | cy.visit(''); 12 | // it's important to clear session storage so that it doesn't persist 13 | // between tests 14 | cy.window().then((win) => { 15 | win.sessionStorage.clear() 16 | }); 17 | }); 18 | 19 | it('should be visible', () => { 20 | LoginPage.screen.should('be.visible'); 21 | }); 22 | 23 | it('allows login with a standard user', () => { 24 | LoginPage.signIn(LOGIN_USERS.STANDARD); 25 | ProductsPage.screen.should('be.visible'); 26 | }); 27 | }); 28 | 29 | // typically we would actually put this into a separate file 30 | // and have another context(). However, we put it here for demonstration 31 | context('Product items', () => { 32 | it('should be added to cart without UI login', () => { 33 | //setTestContext() defined in support/commands.js 34 | cy.setTestContext({ 35 | user: LOGIN_USERS.STANDARD, 36 | path: PAGES.INVENTORY 37 | }); 38 | ProductsPage.addItemToCart(0); 39 | AppHeader.cart.should('have.text', '1'); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/AppHeaderPage.js: -------------------------------------------------------------------------------- 1 | class AppHeader { 2 | get cart() { 3 | return cy.get('.shopping_cart_link'); 4 | } 5 | 6 | /** 7 | * Open the cart 8 | */ 9 | openCart() { 10 | this.cart.click(); 11 | } 12 | } 13 | 14 | export default new AppHeader(); 15 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/CartComponent.js: -------------------------------------------------------------------------------- 1 | class CartComponent { 2 | get itemCount() { 3 | return cy.get('.fa-layers-counter'); 4 | } 5 | } 6 | 7 | export default new CartComponent(); 8 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/CartSummaryPage.js: -------------------------------------------------------------------------------- 1 | class CartSummaryPage { 2 | get screen() { 3 | return cy.get('#cart_contents_container'); 4 | } 5 | 6 | get checkoutButton() { 7 | return cy.get('.checkout_button'); 8 | } 9 | 10 | get continueShoppingButton() { 11 | return cy.get('.btn_secondary'); 12 | } 13 | 14 | get items() { 15 | return cy.get('.cart_item'); 16 | } 17 | 18 | /** 19 | * Get a cart Item based on a search string or a number of the visible items 20 | * 21 | * @param {number|string} needle 22 | * 23 | * @return the selected cart swag 24 | */ 25 | swag(needle) { 26 | if (typeof needle === 'string') { 27 | return this.items.contains(needle); 28 | } 29 | 30 | return this.items.eq(needle); 31 | } 32 | 33 | /** 34 | * Remove an swag from the cart 35 | * 36 | * @param {number|string} needle 37 | */ 38 | removeSwag(needle) { 39 | this.swag(needle).find('.btn_secondary.cart_button').click(); 40 | } 41 | 42 | /** 43 | * Continue shopping 44 | */ 45 | continueShopping() { 46 | this.continueShoppingButton.click(); 47 | } 48 | 49 | /** 50 | * Go to the checkout process 51 | */ 52 | goToCheckout() { 53 | this.checkoutButton.click(); 54 | } 55 | } 56 | 57 | export default new CartSummaryPage(); 58 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/CheckoutCompletePage.js: -------------------------------------------------------------------------------- 1 | class CheckoutCompletePage { 2 | get screen() { 3 | return cy.get('#checkout_complete_container'); 4 | } 5 | } 6 | 7 | export default new CheckoutCompletePage(); 8 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/CheckoutPersonalInfoPage.js: -------------------------------------------------------------------------------- 1 | class CheckoutPersonalInfoPage { 2 | get screen() { 3 | return cy.get('#checkout_info_container'); 4 | } 5 | 6 | get cancelButton() { 7 | return cy.get('.cart_cancel_link'); 8 | } 9 | 10 | get continueCheckoutButton() { 11 | return cy.get('.cart_button'); 12 | } 13 | 14 | get firstName() { 15 | return cy.get('[data-test="firstName"]'); 16 | } 17 | 18 | get lastName() { 19 | return cy.get('[data-test="lastName"]'); 20 | } 21 | 22 | get postalCode() { 23 | return cy.get('[data-test="postalCode"]'); 24 | } 25 | 26 | get errorMessage() { 27 | return cy.get('[data-test="error"]'); 28 | } 29 | 30 | /** 31 | * Submit personal info 32 | * 33 | * @param {object} personalInfo 34 | * @param {string} personalInfo.firstName 35 | * @param {string} personalInfo.lastName 36 | * @param {string} personalInfo.zip 37 | */ 38 | submitPersonalInfo(personalInfo) { 39 | const {firstName, lastName, zip} = personalInfo; 40 | 41 | if (firstName) { 42 | this.firstName.type(firstName); 43 | } 44 | if (lastName) { 45 | this.lastName.type(lastName); 46 | } 47 | if (zip) { 48 | this.postalCode.type(zip); 49 | } 50 | this.continueCheckoutButton.click(); 51 | } 52 | 53 | /** 54 | * Cancel checkout 55 | */ 56 | cancelCheckout() { 57 | this.cancelButton.click(); 58 | } 59 | } 60 | 61 | export default new CheckoutPersonalInfoPage(); 62 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/CheckoutSummaryPage.js: -------------------------------------------------------------------------------- 1 | class CheckoutSummaryPage { 2 | get screen() { 3 | return cy.get('#checkout_summary_container'); 4 | } 5 | 6 | title(needle) { 7 | return this.swag(needle).find('.inventory_item_name'); 8 | } 9 | 10 | description(needle) { 11 | return this.swag(needle).find('.inventory_item_desc'); 12 | } 13 | 14 | price(needle) { 15 | return this.swag(needle).find('.inventory_item_price'); 16 | } 17 | 18 | get cancelButton() { 19 | return cy.get('.cart_cancel_link'); 20 | } 21 | 22 | get finishButton() { 23 | return cy.get('.cart_button'); 24 | } 25 | 26 | get items() { 27 | return cy.get('.cart_item'); 28 | } 29 | 30 | /** 31 | * Get a cart Item based on a search string or a number of the visible items 32 | * 33 | * @param {number|string} needle 34 | * 35 | * @return the selected cart swag 36 | */ 37 | swag(needle) { 38 | if (typeof needle === 'string') { 39 | return this.items.contains(needle); 40 | } 41 | 42 | return this.items.eq(needle); 43 | } 44 | 45 | /** 46 | * Cancel checkout 47 | */ 48 | cancelCheckout() { 49 | this.cancelButton.click(); 50 | } 51 | 52 | /** 53 | * Finish checkout 54 | */ 55 | finishCheckout() { 56 | this.finishButton.click(); 57 | } 58 | } 59 | 60 | export default new CheckoutSummaryPage(); 61 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/LoginPage.js: -------------------------------------------------------------------------------- 1 | class LoginPage { 2 | get screen() { 3 | return cy.get('#login_button_container'); 4 | } 5 | 6 | get username() { 7 | return cy.get('#user-name'); 8 | } 9 | 10 | get password() { 11 | return cy.get('#password'); 12 | } 13 | 14 | get loginButton() { 15 | return cy.get('.btn_action'); 16 | } 17 | 18 | get errorMessage() { 19 | return cy.get('[data-test="error"]'); 20 | } 21 | 22 | /** 23 | * Sign in 24 | * 25 | * @param {object} userDetails 26 | * @param {string} userDetails.username 27 | * @param {string} userDetails.password 28 | */ 29 | signIn(userDetails) { 30 | const {password, username} = userDetails; 31 | 32 | if (username) { 33 | this.username.type(username); 34 | } 35 | if (password) { 36 | this.password.type(password); 37 | } 38 | 39 | this.loginButton.click(); 40 | } 41 | } 42 | 43 | export default new LoginPage(); 44 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/MenuPage.js: -------------------------------------------------------------------------------- 1 | class MenuPage { 2 | get menu() { 3 | return cy.get('.bm-burger-button'); 4 | } 5 | 6 | get inventoryListButton() { 7 | return cy.get('#inventory_sidebar_link'); 8 | } 9 | 10 | get aboutButton() { 11 | return cy.get('#about_sidebar_link'); 12 | } 13 | 14 | get logoutButton() { 15 | return cy.get('#logout_sidebar_link'); 16 | } 17 | 18 | get resetButton() { 19 | return cy.get('#reset_sidebar_link'); 20 | } 21 | 22 | /** 23 | * Open the menu 24 | */ 25 | open() { 26 | this.menu.click(); 27 | } 28 | 29 | /** 30 | * Open the inventory list page 31 | */ 32 | openInventoryList() { 33 | this.inventoryListButton.click(); 34 | } 35 | 36 | /** 37 | * Open the about page 38 | */ 39 | openAboutPage() { 40 | this.aboutButton.click(); 41 | } 42 | 43 | /** 44 | * Logout 45 | */ 46 | logout() { 47 | this.logoutButton.click(); 48 | } 49 | 50 | /** 51 | * Reset the app state 52 | */ 53 | restAppState() { 54 | this.resetButton.click(); 55 | } 56 | } 57 | 58 | export default new MenuPage(); 59 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/ProductsPage.js: -------------------------------------------------------------------------------- 1 | class ProductsPage { 2 | // Make it private so people can't mess with it 3 | // Source: https://github.com/tc39/proposal-class-fields#private-fields 4 | get screen() { 5 | return cy.get('.inventory_list'); 6 | } 7 | 8 | get swagItems() { 9 | return cy.get('.inventory_item'); 10 | } 11 | 12 | /** 13 | * Get the amount of swag items listed on the page 14 | * @returns {number} 15 | */ 16 | getAmount() { 17 | return this.swagItems.length; 18 | } 19 | 20 | /** 21 | * Get a swag Item based on a search string or a number of the visible items 22 | * 23 | * @param {number|string} needle 24 | * 25 | * @return {Element[]} the selected swag 26 | */ 27 | swag(needle) { 28 | if (typeof needle === 'string') { 29 | return this.swagItems.contains(needle); 30 | } 31 | 32 | return this.swagItems.eq(needle); 33 | } 34 | 35 | /** 36 | * Add a swag items to the cart 37 | * 38 | * @param {number|string} itemIdentifier 39 | */ 40 | addItemToCart(itemIdentifier) { 41 | this.swag(itemIdentifier).find('.btn_primary.btn_inventory').click(); 42 | } 43 | 44 | addFirstItemToCart() 45 | { 46 | cy.get('.btn_primary.btn_inventory').click(); 47 | } 48 | 49 | /** 50 | * Remove swag items from the cart 51 | * 52 | * @param {number|string} needle 53 | */ 54 | removeSwagFromCart(needle) { 55 | this.swag(needle).find('.btn_secondary.btn_inventory').click(); 56 | } 57 | 58 | /** 59 | * Open the details of a swag swag 60 | * 61 | * @param {number|string} needle 62 | */ 63 | openSwagDetails(needle) { 64 | this.swag(needle).find('.inventory_item_name').click(); 65 | } 66 | } 67 | 68 | export default new ProductsPage(); 69 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/page-objects/SwagDetailsPage.js: -------------------------------------------------------------------------------- 1 | class SwagDetailsPage { 2 | get screen() { 3 | return cy.get('.inventory_details'); 4 | } 5 | 6 | get title() { 7 | return cy.get('.inventory_details_name'); 8 | } 9 | 10 | get description() { 11 | return cy.get('.inventory_details_desc'); 12 | } 13 | 14 | get price() { 15 | return cy.get('.inventory_details_price'); 16 | } 17 | 18 | get addButton() { 19 | return cy.get('.btn_primary.btn_inventory'); 20 | } 21 | 22 | get removeButton() { 23 | return cy.get('.btn_secondary.btn_inventory'); 24 | } 25 | 26 | get goBackButton() { 27 | return cy.get('.inventory_details_back_button'); 28 | } 29 | 30 | /** 31 | * Add a swag items to the cart 32 | */ 33 | addToCart() { 34 | this.addButton.click(); 35 | } 36 | 37 | /** 38 | * Remove a swag items from the cart 39 | */ 40 | removeFromCart() { 41 | this.removeButton.click(); 42 | } 43 | 44 | /** 45 | * Go back to the inventory list 46 | */ 47 | goBack() { 48 | this.goBackButton.click({force: true}); 49 | } 50 | } 51 | 52 | export default new SwagDetailsPage(); 53 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set the test context 3 | * 4 | * @param {object} data 5 | * @param {object} data.user 6 | * @param {string} data.user.username 7 | * @param {string} data.user.password 8 | * @param {string} data.path 9 | * @param {array} data.products 10 | */ 11 | Cypress.Commands.add("setTestContext", (data = {}) => { 12 | const {path, products = [], user} = data; 13 | const {username} = user; 14 | const productStorage = products.length > 0 ? `[${products.toString()}]` : '[]'; 15 | 16 | // Go to the domain and set the storage 17 | cy.visit(''); 18 | cy.window().then((win) => { 19 | win.sessionStorage.clear() 20 | }); 21 | window.sessionStorage.setItem("session-username", username); 22 | window.sessionStorage.setItem("cart-contents", productStorage); 23 | 24 | // Now got to the page 25 | cy.visit(path); 26 | }); 27 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/support/e2eConstants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TIMEOUT = 30000; 2 | export const PAGES = { 3 | CART: '/cart.html', 4 | CHECKOUT_COMPLETE: '/checkout-complete.html', 5 | CHECKOUT_PERSONAL_INFO: '/checkout-step-one.html', 6 | CHECKOUT_SUMMARY: '/checkout-step-two.html', 7 | LOGIN: '', 8 | SWAG_DETAILS: '/inventory-item.html', 9 | INVENTORY: '/inventory.html', 10 | }; 11 | export const PRODUCTS = { 12 | BIKE_LIGHT: 0, 13 | BOLT_SHIRT: 1, 14 | ONE_SIE: 2, 15 | TATT_SHIRT: 3, 16 | BACKPACK: 4, 17 | FLEECE_JACKET: 5, 18 | }; 19 | export const LOGIN_USERS = { 20 | LOCKED: { 21 | username: 'locked_out_user', 22 | password: 'secret_sauce', 23 | }, 24 | NO_MATCH: { 25 | username: 'd', 26 | password: 'd', 27 | }, 28 | NO_USER_DETAILS: { 29 | username: '', 30 | password: '', 31 | }, 32 | NO_PASSWORD: { 33 | username: 'standard_user', 34 | password: '', 35 | }, 36 | PERFORMANCE: { 37 | username: 'performance_glitch_user', 38 | password: 'secret_sauce', 39 | }, 40 | STANDARD: { 41 | username: 'standard_user', 42 | password: 'secret_sauce', 43 | }, 44 | }; 45 | export const PERSONAL_INFO = { 46 | STANDARD: { 47 | firstName: 'Sauce', 48 | lastName: 'Bot', 49 | zip: '94105', 50 | }, 51 | NO_FIRSTNAME: { 52 | firstName: '', 53 | lastName: 'Bot', 54 | zip: '94105', 55 | }, 56 | NO_LAST_NAME: { 57 | firstName: 'Sauce', 58 | lastName: '', 59 | zip: '94105', 60 | }, 61 | NO_POSTAL_CODE: { 62 | firstName: 'Sauce', 63 | lastName: 'Bot', 64 | zip: '', 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /automated-atomic-tests/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /automated-atomic-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automated-atomic-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "cy:open": "npx cypress open", 8 | "test.local": "npx cypress run", 9 | "test.local.headfull": "npx cypress run --headed" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "cypress": "^7.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphics/bye.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/bye.gif -------------------------------------------------------------------------------- /graphics/component-diagram.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/component-diagram.jpeg -------------------------------------------------------------------------------- /graphics/josh-grant.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/josh-grant.jpeg -------------------------------------------------------------------------------- /graphics/me-and-mia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/me-and-mia.jpg -------------------------------------------------------------------------------- /graphics/secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/secrets.png -------------------------------------------------------------------------------- /graphics/testing-good.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/testing-good.jpeg -------------------------------------------------------------------------------- /graphics/thanks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/thanks.gif -------------------------------------------------------------------------------- /graphics/visual-testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/visual-testing.png -------------------------------------------------------------------------------- /graphics/visual-workflow.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/graphics/visual-workflow.jpeg -------------------------------------------------------------------------------- /login-testing/LOGINS.md: -------------------------------------------------------------------------------- 1 | # Testing Logins Efficiently 2 | 3 | ## 🧠You will learn: 4 | 5 | ✅How to login without a UI using a HTML web forms 6 | 7 | ✅How to login without a UI using JWT 8 | 9 | ## HTML Web Form 10 | 11 | ### ⚙️Setup 12 | 13 | >We will use https://github.com/nadvolod/cypress-example-recipes only for login testing 14 | 15 | 1. Stop the other processes from previous exercises `Ctrl + C` 16 | 2. Fork https://github.com/nadvolod/cypress-example-recipes 17 | 3. Clone your fork to your machine 18 | 1. Start cypress and the app 19 | 20 | 21 | ```bash 22 | cd cypress-example-recipes 23 | npm install 24 | cd examples/logging-in__html-web-forms/ 25 | npm run dev 26 | ``` 27 | 28 | >We will use https://github.com/nadvolod/cypress-example-recipes only for login testing 29 | 30 | ### Exploring the app 31 | 32 | Our simple web app is protected by a HTML web form 33 | 34 | 1. Try to open the URL `http://localhost:7077/` 35 | 2. Try to login with valid credentials `jane.lane` and `password123`. Pay attention to the requests and behavior of the application 36 | 3. Also try to login with invalid credentials. 37 | 38 | Some expected app behaviors 39 | 1. /admin only allowed for authenticated users 40 | 2. /users only allowed for authenticated users 41 | 2. /dashboard only allowed for authenticated users 42 | 3. Only user `jane.lane` and `password123` can access the app 43 | 44 | 45 | Open `inneficient.spec.js` 46 | 47 | ### ❓What are the problems with these tests❓ 48 | 49 | UI tests in general are inneficient! Hence, so many models exist to discourage us from writing so many UI tests (autoamtion pyramid, automation trophy, automation diamond...) 50 | 51 | ![Testing types comparison](./images/testing%20comparison%20chart.png) 52 | 53 | So let's improve our tests... 54 | 55 | ### 🏋️‍♀️Exercise 56 | 57 | 1. Open `exercise.spec.js` 58 | 1. Create a test that can visit `/dashboard` without a UI login (We'll do this together) 59 | 2. Create a test that can visit `/users` without a UI login 60 | 3. Create a test that can visit `/admin` without a UI login 61 | 62 | 💡 You already have some helpful code to make your life easier 63 | 64 | ### Advantages/Disadvantages 65 | ✅Fast 66 | 67 | ✅Reliable 68 | 69 | ✖️Need to learn how your API works (maybe a good thing) 70 | 71 | ## 📔Summary 72 | 73 | 1. We bypassed one login using `sessionStorage` in a browser. This case is not typical as most websites don't have authentication code that happens in the front-end. 74 | 2. We bypassed another login using an HTML Web Form. This is a common approach that we can use to tackle other types of logins 75 | 76 | --- 77 | 78 | ## JSON Web Token (JWT) 79 | 80 | ### ⚙️Setup 81 | 1. `Ctrl + C` to kill all of the processess running from previous session (server and cypress) 82 | 2. `cd ../logging-in_jwt` 83 | 3. `npm run dev` 84 | 4. App runs on http://localhost:8081/ 85 | 86 | ### The SUT 87 | 88 | *Authenticate User:* 89 | 90 | 91 | POST to http://localhost:4000/users/authenticate 92 | 93 | *Get list of users:* 94 | 95 | GET to http://localhost:4000/users with a bearer token as auth 96 | 97 | Explore the application at `http://localhost:8081/` and notice the behavior of authentication 98 | 99 | ### How does [JWT](https://jwt.io/introduction) work? 100 | 1. Form fires `handleSubmit()` 101 | 2. Reads form values 102 | 3. Fires a web request for authentication 103 | 104 | ```js 105 | //LoginPage.vue 106 | if (username && password) { 107 | dispatch('authentication/login', { username, password }); 108 | } 109 | ``` 110 | 4. Save JWT token to local storage. No cookies. 111 | 112 | ```js 113 | //user.service.js 114 | .then((user) => { 115 | // login successful if there's a jwt token in the response 116 | if (user.token) { 117 | // store user details and jwt token in local storage to keep user logged in between page refreshes 118 | localStorage.setItem('user', JSON.stringify(user)) 119 | } 120 | ``` 121 | ![tokenAdded](./images/TokenAdded.png ) 122 | 123 | [Building and testing an auth API with JWT tutorial](https://www.youtube.com/watch?v=klIAT82UtVs) 124 | 125 | ### 🏋️‍♀️Atomic login tests (25min) 126 | 127 | Open `cypress/integration/exercise.spec.js` 128 | Your challenge is to create 2 tests: 129 | 1. Create a test to login with a JWT and assert that user is successfully logged in 130 | 2. Assert that a user can successfully log out. You're not allowed to login through the UI 131 | 132 | 💡 JWT authentication is already implemented for you 133 | 134 | ## 📝Summary 135 | 136 | ✅Pretty much all authentication can be bypassed without using a UI 137 | 138 | ✅Authenticating using non-UI methods is more stable and efficient 139 | 140 | ✅We learned 3 types of authentication: HTML Web forms, JWT, directly setting session storage (not realistic) 141 | 142 | ## 📔More Resources 143 | 144 | There are numerous other ways that we can authenticate: 145 | 1. SSO 146 | 2. Using app code 147 | 3. XHR web forms 148 | 4. CSRF tokens 149 | 5. Basic auth 150 | 151 | [Learn more by looking at the logging-in examples folders](https://github.com/nadvolod/cypress-example-recipes/tree/master/examples) -------------------------------------------------------------------------------- /login-testing/images/TokenAdded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/login-testing/images/TokenAdded.png -------------------------------------------------------------------------------- /login-testing/images/testing comparison chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/login-testing/images/testing comparison chart.png -------------------------------------------------------------------------------- /my-react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /my-react-app/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000" 3 | } 4 | -------------------------------------------------------------------------------- /my-react-app/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /my-react-app/cypress/integration/exercise.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads', ()=> { 3 | /** Your code below */ 4 | 5 | //1. Use cy.visit('') to go to the app url 6 | //2. Use cy.get('element locator').should('be.visible') to assert valid state 7 | /** Your code above */ 8 | }) 9 | 10 | it('should click link',()=>{ 11 | /** Your code below */ 12 | 13 | //1. Use cy.visit('') to go to the app url 14 | //2. Use cy.get('element locator') for the link 15 | // But now we will .click() the link and then assert that the 16 | // .url().should('not.contain','ultimateqa.com'); 17 | /** Your code above */ 18 | }) -------------------------------------------------------------------------------- /my-react-app/cypress/integration/solution.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('The React app', () => { 4 | it('loads', ()=> { 5 | cy.visit('/') 6 | cy.get('.App-link').should('be.visible') 7 | }) 8 | 9 | it('should click link',()=>{ 10 | cy.visit('/'); 11 | cy.get('[data-testid=learn-link]').click().url().should('not.contain','ultimateqa.com'); 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /my-react-app/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /my-react-app/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /my-react-app/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /my-react-app/docs/CICD.md: -------------------------------------------------------------------------------- 1 | # CICD 2 | 3 | ## 🏋️‍♀️Let's add this code to our CI system. 4 | 5 | 1. Create a a file in this folder structure `.github/workflows/ci.yml` in the root of your directory 6 | 2. Paste in the following configuration 7 | 8 | ```yml 9 | name: Node.js CI 10 | env: 11 | SCREENER_API_KEY: ${{ secrets.SCREENER_API_KEY }} 12 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 13 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 14 | 15 | on: 16 | push: 17 | branches: [ main ] 18 | pull_request: 19 | branches: [ main ] 20 | 21 | jobs: 22 | build: 23 | 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | matrix: 28 | node-version: [14.x] 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - name: Install dependencies 📦 37 | #Using npm ci is generally faster than running npm install 38 | run: | 39 | cd my-react-app 40 | npm ci 41 | - name: Build the app 🏗 42 | run: | 43 | cd my-react-app 44 | npm run build 45 | - name: Run component tests 🔸 46 | run: | 47 | cd my-react-app 48 | npm run test 49 | # If we had more time, at this point we can actually deploy our app 50 | # to a staging server and then run functional tests 51 | - name: Start the app 📤 52 | run: | 53 | cd my-react-app 54 | npm start & 55 | npx wait-on --timeout 60000 56 | - name: Run functional UI tests 🖥 57 | run: | 58 | cd my-react-app 59 | npm run cy:ci 60 | - name: Run visual tests 👁 61 | run: | 62 | cd my-react-app 63 | echo $SAUCE_USERNAME 64 | npm run test:visual 65 | ``` 66 | 3. Add New repository secrets for the repo 67 | 68 | ![adding secrets](../../graphics/secrets.png) 69 | 70 | 4. `git push` and watch it run -------------------------------------------------------------------------------- /my-react-app/docs/FRONT-END-PERF.md: -------------------------------------------------------------------------------- 1 | ## Front-end performance 2 | 3 | * Use the task tracker application that we build in React JS crash course 4 | * Get the performance metrics by using in the About and Footer components 5 | * Then change those to a Link component 6 | * Then recapture the front-end perf metrics 7 | * The expected result is that with the instant DOM refresh, the latter will be faster -------------------------------------------------------------------------------- /my-react-app/docs/GETTING-STARTED-REACT.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /my-react-app/docs/VISUAL.md: -------------------------------------------------------------------------------- 1 | # Visual E2E Testing 2 | 3 | ## 🧠You will learn 4 | 5 | ✅What is visual E2E testing? 6 | 7 | ✅How to implement visual e2e using WebdriverIO + Screener 8 | 9 | ## How do we check to make sure that the app looks as expected on web and mobile? 10 | 11 | [What is visual testing?](https://docs.google.com/presentation/d/13jYXXoKb36aFt1HLnNnAmsPqw9yaFhVrB4iFH_5_WkI/edit#slide=id.gcc181d5a54_0_284) 12 | 13 | ## A bit about webdriverIO 14 | 15 | * [WebdrverIO](https://webdriver.io/) 16 | * [Screener](https://screener.io/) 17 | 18 | ## Set up a visual test 19 | 20 | 1. Create a new file `my-react-app/test/specs/exercise.spec.js` 21 | 2. Paste the following code 22 | 23 | ```js 24 | describe('My React application', () => { 25 | it('should look correct', () => { 26 | browser.url(`/`); 27 | browser.execute('/*@visual.init*/', 'My React App'); 28 | browser.execute('/*@visual.snapshot*/', 'Home Page'); 29 | }); 30 | }); 31 | ``` 32 | 3. `npm run test:visual` 33 | 4. View your results in Screener.io 34 | 35 | ### Expand the config to cover iOS and Android 36 | 37 | In today's day and age, everything must be responsive, so let's make sure that our app looks good on iOS web. 38 | Hint, use these capabilities in `wdio.conf.js`: 39 | 40 | ```js 41 | //iphone X 42 | { 43 | browserName: 'safari', 44 | platformName: 'macOS 10.15', 45 | browserVersion: 'latest', 46 | 'sauce:options': { 47 | ...sauceOptions, 48 | }, 49 | 'sauce:visual': { 50 | ...visualOptions, 51 | viewportSize: '375x812' 52 | } 53 | } 54 | ``` 55 | --- 56 | ### 🏋️‍♀️❓ Let's change our image, what tests should that break❓ 57 | --- 58 | 59 | We're going to update the React image to something better. What tests should break? 60 | 61 | * Drag n drop a new image to the `/src` 62 | * Fix the path to be correct here `import logo from './mia.jpg';` in `App.js` 63 | * Save all files 64 | * Stop the app `ctrl + C` 65 | * Restart the app with `npm start` 66 | * Rerun the visual tests with `npm run test:visual` 67 | * Analyze the results in Screener dashboard 68 | 69 | ❓Is our app fully tested now❓ 70 | 71 | | Expected Behavior | Tested? | Test Type | Technologies | 72 | |---|---|---|---| 73 | | Application renders | ✅ | Component | React testing library, Jest | 74 | | Learn React link goes to correct location | ✅ | Component | React testing library, Jest | 75 | | Learn React link opens in new tab | ✅ | Component | React testing library, Jest | 76 | | App looks as expected on web and mobile | ✅ | Visual | Screener,WebdriverIO | 77 | | Front-end performance is at least a B | 🙅‍♂️ | | | 78 | | App is secure | 🙅‍♂️ | | | 79 | 80 | ## 📝Summary 81 | 82 | ✅Visual e2e testing is a simple and efficient way to test your web app cross-platform 83 | 84 | ✅We used WebdriverIO + Screener.io to write our visual e2e tests 85 | 86 | Wouldn't it be great to have this tested automatically through CI? 87 | 88 | [Let's setup up CI](./CICD.md) -------------------------------------------------------------------------------- /my-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.7.3", 9 | "@wdio/cli": "^7.0.7", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-scripts": "4.0.3", 13 | "web-vitals": "^1.1.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 | "cy:open": "cypress open", 21 | "cy:ci": "cypress run", 22 | "test:visual": "wdio run ./wdio.conf.js", 23 | "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watch" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@wdio/local-runner": "^7.0.7", 45 | "@wdio/mocha-framework": "^7.0.7", 46 | "@wdio/sauce-service": "^7.0.7", 47 | "@wdio/spec-reporter": "^7.0.7", 48 | "@wdio/sync": "^7.0.7", 49 | "chromedriver": "^88.0.0", 50 | "cypress": "7.1.0", 51 | "wdio-chromedriver-service": "^7.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /my-react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/my-react-app/public/favicon.ico -------------------------------------------------------------------------------- /my-react-app/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 | -------------------------------------------------------------------------------- /my-react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/my-react-app/public/logo192.png -------------------------------------------------------------------------------- /my-react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices/82c7279cfa1bc88858b0d4be69332d0a29092a9e/my-react-app/public/logo512.png -------------------------------------------------------------------------------- /my-react-app/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 | -------------------------------------------------------------------------------- /my-react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /my-react-app/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 | -------------------------------------------------------------------------------- /my-react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import logo from './mia.jpg'; 3 | 4 | function App() { 5 | return ( 6 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /my-react-app/src/__tests__/Exercise.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from '../App'; 4 | 5 | //Using Jest matchers: https://jestjs.io/docs/using-matchers 6 | test('renders learn react link', () => { 7 | //render our App component in a virtual DOM 8 | render(); 9 | //search for an element by text 10 | const linkElement = screen.getByTestId('learn-link'); 11 | //expect this element to be present in the HTML 12 | expect(linkElement).toBeInTheDocument(); 13 | }); 14 | 15 | test('link has correct url', () => { 16 | //render our App component in a virtual DOM 17 | render(); 18 | const linkElement = screen.getByTestId('learn-link') 19 | expect(linkElement.href).toContain('ultimateqa'); 20 | }) 21 | 22 | -------------------------------------------------------------------------------- /my-react-app/src/__tests__/Solution.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from '../App'; 4 | 5 | //Using Jest matchers: https://jestjs.io/docs/using-matchers 6 | 7 | test('renders learn react link', () => { 8 | //render our App component in a virtual DOM 9 | render(); 10 | //search for an element by text 11 | const linkElement = screen.getByText('Learn Testing with Mia') 12 | 13 | //Search for element by test id 14 | //const linkElement = screen.getByTestId('learn-link'); 15 | 16 | //Using Jest matchers: https://jestjs.io/docs/using-matchers 17 | //expect this element to be present in the HTML 18 | expect(linkElement).toBeInTheDocument(); 19 | }) 20 | 21 | test('link has correct url', () => { 22 | //render our App component in a virtual DOM 23 | render(); 24 | const linkElement = screen.getByText('Learn Testing with Mia') 25 | expect(linkElement.href).toContain('ultimateqa'); 26 | }) 27 | 28 | test('link opens in new tab', () => { 29 | //render our App component in a virtual DOM 30 | render(); 31 | const linkElement = screen.getByText('Learn Testing with Mia') 32 | //Link should open a new tab 33 | expect(linkElement.target).toBe('_blank') 34 | }) 35 | -------------------------------------------------------------------------------- /my-react-app/src/extra-components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | 5 | constructor(props){ 6 | super(props); 7 | this.state = { 8 | foo: 'bar', 9 | resumeData: {} 10 | }; 11 | } 12 | 13 | 14 | render() { 15 | return ( 16 |
17 |
18 | ); 19 | } 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /my-react-app/src/extra-components/CheckUserAge.js: -------------------------------------------------------------------------------- 1 | //https://www.freecodecamp.org/learn/front-end-libraries/react/use-a-ternary-expression-for-conditional-rendering 2 | const inputStyle = { 3 | width: 235, 4 | margin: 5 5 | }; 6 | 7 | class CheckUserAge extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | // Setting a property called 'state' 11 | this.state = { 12 | input:'', 13 | userAge:'' 14 | } 15 | this.submit = this.submit.bind(this); 16 | this.handleChange = this.handleChange.bind(this); 17 | } 18 | handleChange(e) { 19 | this.setState({ 20 | input: e.target.value, 21 | userAge: '' 22 | }); 23 | } 24 | submit() { 25 | this.setState(state => ({ 26 | userAge: state.input 27 | })); 28 | } 29 | render() { 30 | const buttonOne = ; 31 | const buttonTwo = ; 32 | const buttonThree = ; 33 | return ( 34 |
35 |

Enter Your Age to Continue

36 | 42 |
43 | {/* if the userAge is empty, render buttonOne 44 | Otherwise, if the user age is greater than 18 then render buttonTwo, otherwise render buttonThree. 45 | Notice how we are referencing the values in our HTML using this.state */} 46 | { 47 | this.state.userAge === '' 48 | ? buttonOne 49 | : this.state.userAge >= 18 50 | ? buttonTwo 51 | : buttonThree 52 | } 53 |
54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /my-react-app/src/extra-components/MagicEightBall.js: -------------------------------------------------------------------------------- 1 | import React from 'React'; 2 | 3 | const inputStyle = { 4 | width: 235, 5 | margin: 5 6 | }; 7 | 8 | class MagicEightBall extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | userInput: '', 13 | randomIndex: '' 14 | }; 15 | this.ask = this.ask.bind(this); 16 | this.handleChange = this.handleChange.bind(this); 17 | } 18 | ask() { 19 | if (this.state.userInput) { 20 | this.setState({ 21 | //a randomIndex every time the ask() is invoked here 66 |
67 |

Answer:

68 |

69 | {answer} 70 |

71 | 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /my-react-app/src/extra-components/MyToDoList.js: -------------------------------------------------------------------------------- 1 | //https://www.freecodecamp.org/learn/front-end-libraries/react/use-array-map-to-dynamically-render-elements 2 | import React, { Component } from 'react'; 3 | 4 | const textAreaStyles = { 5 | width: 235, 6 | margin: 5 7 | }; 8 | 9 | class MyToDoList extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | userInput: '', 14 | toDoList: [] 15 | } 16 | this.handleSubmit = this.handleSubmit.bind(this); 17 | this.handleChange = this.handleChange.bind(this); 18 | } 19 | handleSubmit() { 20 | const itemsArray = this.state.userInput.split(','); 21 | this.setState({ 22 | toDoList: itemsArray 23 | }); 24 | } 25 | handleChange(e) { 26 | this.setState({ 27 | userInput: e.target.value 28 | }); 29 | } 30 | render() { 31 | // Loop over the toDoList array and for each item, returh a
  • with a key and the name of the item 32 | const items = this.state.toDoList.map((item,i) =>
  • {item}
  • ) 33 | return ( 34 |
    35 |