├── .circleci └── config.yml ├── .github └── workflows │ ├── badges.yml │ ├── ci.yml │ └── slides.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── README.md ├── cypress.ci-config.js ├── cypress.config.js ├── cypress.slides-config.js ├── cypress ├── ci-tests │ ├── api-spec.js │ ├── store-spec.js │ └── ui-spec.js ├── custom-commands.d.ts ├── e2e │ ├── 01-basic │ │ ├── answer.js │ │ └── spec.js │ ├── 02-adding-items │ │ ├── answer-delete.js │ │ ├── answer-many-items.js │ │ ├── answer.js │ │ ├── demo.js │ │ └── spec.js │ ├── 03-selector-playground │ │ ├── answer.js │ │ └── spec.js │ ├── 04-reset-state │ │ ├── answer.js │ │ └── spec.js │ ├── 05-network │ │ ├── answer-clock.js │ │ ├── answer-idle.js │ │ ├── answer-json.js │ │ ├── answer-loader.js │ │ ├── answer-refactor.js │ │ ├── answer.js │ │ └── spec.js │ ├── 06-app-data-store │ │ ├── answer.js │ │ └── spec.js │ ├── 08-retry-ability │ │ ├── answer.js │ │ └── spec.js │ └── 09-custom-commands │ │ ├── answer.js │ │ └── spec.js ├── fixtures │ ├── empty-list.json │ ├── three-items.json │ └── two-items.json ├── slides-tests │ ├── slides.js │ └── utils.js └── support │ ├── commands.js │ ├── e2e.js │ └── utils.js ├── img ├── app.png ├── cypress-desktop.png └── fails-to-find-text.png ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── renovate.json ├── slides-utils.js ├── slides ├── 00-start │ ├── PITCHME.md │ └── img │ │ ├── cy-get-intellisense.jpeg │ │ ├── cypress-examples.png │ │ ├── cypress-scaffold.png │ │ ├── cypress-tips-search.png │ │ ├── docs-search.png │ │ ├── should-intellisense.jpeg │ │ ├── start1.png │ │ ├── start2.png │ │ ├── start3.png │ │ ├── start4.png │ │ ├── switch-browser.png │ │ └── vscode-icons.png ├── 01-basic │ └── PITCHME.md ├── 02-adding-items │ └── PITCHME.md ├── 03-selector-playground │ ├── PITCHME.md │ └── img │ │ ├── best-practice.png │ │ ├── chrome-copy-js-path.png │ │ ├── default-suggestion.png │ │ ├── selector-button.png │ │ ├── selector-playground.png │ │ ├── selectors.png │ │ └── start-studio.png ├── 04-reset-state │ ├── PITCHME.md │ └── img │ │ ├── failing-test.png │ │ ├── inspect-first-get-todos.png │ │ ├── passing-test.png │ │ └── write-file-path.png ├── 05-network │ ├── PITCHME.md │ └── img │ │ ├── get-todos.png │ │ ├── post-item-response.png │ │ ├── post-item.png │ │ ├── response-body.png │ │ ├── test-passes-but-this-is-wrong.png │ │ └── waiting.png ├── 06-app-data-store │ ├── PITCHME.md │ └── img │ │ ├── app-in-window.png │ │ ├── contexts.png │ │ ├── new-todo.png │ │ └── window-app.png ├── 07-ci │ ├── PITCHME.md │ └── img │ │ ├── add-project.png │ │ ├── cypress-cloud-badge.png │ │ ├── replay-button.png │ │ ├── replay.png │ │ └── workshop-tests.png ├── 08-retry-ability │ ├── PITCHME.md │ └── img │ │ ├── alias-does-not-exist.png │ │ ├── assertion-intellisense.png │ │ ├── bdd.png │ │ ├── chai-intellisense.png │ │ ├── one-label.png │ │ ├── retry.png │ │ ├── tdd.png │ │ ├── test-retries.png │ │ ├── two-labels.png │ │ └── waiting.png ├── 09-custom-commands │ ├── PITCHME.md │ └── img │ │ ├── create-todo-intellisense.jpeg │ │ ├── create-todo-log.png │ │ ├── intellisense.jpeg │ │ ├── jsdoc.png │ │ └── to-match-snapshot.png ├── 10-component-testing │ ├── PITCHME.md │ └── img │ │ └── setup-type.png ├── end │ └── PITCHME.md ├── intro │ ├── PITCHME.md │ └── img │ │ ├── DOM.png │ │ ├── app.png │ │ ├── courses.png │ │ ├── docs-search.png │ │ ├── network.png │ │ ├── todomvc.png │ │ ├── vue-devtools.png │ │ └── vue-vuex-rest.png ├── quizes │ └── PITCHME.md └── style.css ├── todomvc ├── .npmrc ├── README.md ├── analytics.js ├── app.js ├── data.json ├── img │ ├── DOM.png │ ├── app.png │ ├── docs-search.png │ ├── network.png │ ├── todomvc.png │ ├── vue-devtools.png │ └── vue-vuex-rest.png ├── index.html ├── package-lock.json ├── package.json ├── reset-db.js └── vendor │ ├── axios.min.js │ ├── dark.css │ ├── director.js │ ├── index.css │ ├── polyfill.min.js │ ├── vue.js │ └── vuex.js └── vite.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | # https://github.com/cypress-io/circleci-orb 4 | cypress: cypress-io/cypress@4.1.0 5 | workflows: 6 | build: 7 | jobs: 8 | # see examples in https://github.com/cypress-io/circleci-orb#contents 9 | - cypress/run: 10 | name: Cypress E2E Tests 11 | start-command: 'npm start' 12 | cypress-command: 'npx cypress run --config-file cypress.ci-config.js' 13 | -------------------------------------------------------------------------------- /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | push: 4 | # update README badge only if the README file changes 5 | # or if the package.json file changes, or this file changes 6 | branches: 7 | - main 8 | paths: 9 | - README.md 10 | - package.json 11 | - .github/workflows/badges.yml 12 | schedule: 13 | # update badges every night 14 | # because we have a few badges that are linked 15 | # to the external repositories 16 | - cron: '0 3 * * *' 17 | 18 | jobs: 19 | badges: 20 | name: Badges 21 | runs-on: ubuntu-24.04 22 | steps: 23 | - name: Checkout 🛎 24 | uses: actions/checkout@v4 25 | 26 | - name: Update version badges 🏷 27 | run: npx -p dependency-version-badge update-badge cypress 28 | 29 | - name: Commit any changed files 💾 30 | uses: stefanzweifel/git-auto-commit-action@v5 31 | with: 32 | commit_message: Updated badges 33 | branch: main 34 | file_pattern: README.md 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | # test main CI specs in one job 5 | # and test the answers in another job 6 | test: 7 | runs-on: ubuntu-24.04 8 | steps: 9 | - name: Checkout 🛎 10 | uses: actions/checkout@v4 11 | 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 16 15 | 16 | - name: Print versions 🖨️ 17 | run: node -v 18 | 19 | # https://github.com/cypress-io/github-action 20 | - name: Run Cypress tests 🧪 21 | uses: cypress-io/github-action@v5 22 | with: 23 | config-file: 'cypress.ci-config.js' 24 | start: 'npm start' 25 | wait-on: 'http://localhost:3000' 26 | 27 | test-answers: 28 | runs-on: ubuntu-24.04 29 | strategy: 30 | # when one test fails, DO NOT cancel the other 31 | # containers, because this will kill Cypress processes 32 | # leaving the Dashboard hanging ... 33 | # https://github.com/cypress-io/github-action/issues/48 34 | fail-fast: false 35 | matrix: 36 | # run N copies of the current job in parallel 37 | containers: [1, 2, 3, 4, 5, 6, 7] 38 | steps: 39 | - name: Checkout 🛎 40 | uses: actions/checkout@v4 41 | 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version: 16 45 | 46 | # https://github.com/cypress-io/github-action 47 | - name: Test answers 🤔 48 | uses: cypress-io/github-action@v6 49 | with: 50 | start: 'npm start' 51 | wait-on: 'http://localhost:3000' 52 | # run every answer spec file 53 | config: 'specPattern=cypress/e2e/*/answer*.js' 54 | # record the test results to Cypress Dashboard 55 | record: true 56 | parallel: true 57 | group: Answers 58 | tag: answers 59 | env: 60 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 61 | -------------------------------------------------------------------------------- /.github/workflows/slides.yml: -------------------------------------------------------------------------------- 1 | name: slides 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | test: 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - name: Checkout 🛎 11 | uses: actions/checkout@v4 12 | 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 16 16 | 17 | # https://github.com/cypress-io/github-action 18 | - name: Test slides 🎞 19 | uses: cypress-io/github-action@v6 20 | with: 21 | config-file: 'cypress.slides-config.js' 22 | start: 'npm run slides' 23 | wait-on: 'http://localhost:3100' 24 | 25 | # build and deploy new version of the slides 26 | - name: Build slides 🏗 27 | run: npm run slides:build -- --base /cypress-workshop-basics/ 28 | 29 | - name: Show the built folder 📋 30 | run: ls -la dist 31 | 32 | # if the tests passed, publish the application 33 | # https://github.com/peaceiris/actions-gh-pages 34 | - name: Publish slides 🌐 35 | if: github.ref == 'refs/heads/main' 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: ./dist 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | */cypress/videos 3 | */cypress/screenshots 4 | cypress/videos 5 | cypress/screenshots 6 | cypress/results 7 | mochawesome-report/ 8 | mochawesome.json 9 | cypress/logs 10 | dist 11 | .nyc_output 12 | coverage 13 | npm-debug.log 14 | posted.json 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /cypress.ci-config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const debug = require('debug')('cypress-workshop-basics') 4 | const { defineConfig } = require('cypress') 5 | 6 | module.exports = defineConfig({ 7 | viewportWidth: 600, 8 | viewportHeight: 800, 9 | e2e: { 10 | baseUrl: 'http://localhost:3000', 11 | env: {}, 12 | specPattern: 'cypress/ci-tests/*-spec.js', 13 | setupNodeEvents(on, config) { 14 | // `on` is used to hook into various events Cypress emits 15 | // "cy.task" can be used from specs to "jump" into Node environment 16 | // and doing anything you might want. For example, checking "data.json" file! 17 | // https://on.cypress.io/task 18 | on('task', { 19 | // saves given or default empty data object into todomvc/data.json file 20 | // if the server is watching this file, next reload should show the updated values 21 | async resetData(dataToSet = DEFAULT_DATA) { 22 | resetData(dataToSet) 23 | 24 | // add a small delay for the server to "notice" 25 | // the changed JSON file and reload 26 | await delay(100) 27 | 28 | // cy.task handlers should always return something 29 | // otherwise it might be an accidental return 30 | return null 31 | }, 32 | 33 | hasSavedRecord(title, ms = 3000) { 34 | debug('inside task') 35 | console.log( 36 | 'looking for title "%s" in the database (time limit %dms)', 37 | title, 38 | ms 39 | ) 40 | return hasRecordAsync(title, ms) 41 | }, 42 | 43 | /** 44 | * Call this method using cy.task('getSavedTodos') command. 45 | * Make sure the backend had plenty of time to save the data. 46 | */ 47 | getSavedTodos() { 48 | const s = fs.readFileSync(getDbFilename(), 'utf8') 49 | const data = JSON.parse(s) 50 | console.log('returning %d saved todos', data.todos.length) 51 | return data.todos 52 | } 53 | }) 54 | 55 | on('before:spec', (spec) => { 56 | console.log('resetting DB before spec %s', spec.name) 57 | resetData() 58 | }) 59 | } 60 | }, 61 | projectId: '89mmxs' 62 | }) 63 | 64 | const getDbFilename = () => path.join(__dirname, 'todomvc', 'data.json') 65 | 66 | const findRecord = (title) => { 67 | const dbFilename = getDbFilename() 68 | const contents = JSON.parse(fs.readFileSync(dbFilename)) 69 | const todos = contents.todos 70 | return todos.find((record) => record.title === title) 71 | } 72 | 73 | const hasRecordAsync = (title, ms) => { 74 | const delay = 50 75 | return new Promise((resolve, reject) => { 76 | if (ms < 0) { 77 | return reject(new Error(`Could not find record with title "${title}"`)) 78 | } 79 | const found = findRecord(title) 80 | if (found) { 81 | return resolve(found) 82 | } 83 | setTimeout(() => { 84 | hasRecordAsync(title, ms - delay).then(resolve, reject) 85 | }, 50) 86 | }) 87 | } 88 | 89 | /** 90 | * Default object representing our "database" file in "todomvc/data.json" 91 | */ 92 | const DEFAULT_DATA = { 93 | todos: [] 94 | } 95 | 96 | const resetData = (dataToSet = DEFAULT_DATA) => { 97 | const dbFilename = getDbFilename() 98 | debug('reset data file %s with %o', dbFilename, dataToSet) 99 | if (!dataToSet) { 100 | console.error('Cannot save empty object in %s', dbFilename) 101 | throw new Error('Cannot save empty object in resetData') 102 | } 103 | const str = JSON.stringify(dataToSet, null, 2) + '\n' 104 | fs.writeFileSync(dbFilename, str, 'utf8') 105 | } 106 | 107 | async function delay(ms) { 108 | return new Promise((resolve) => { 109 | setTimeout(resolve, ms) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const debug = require('debug')('cypress-workshop-basics') 4 | const { defineConfig } = require('cypress') 5 | 6 | module.exports = defineConfig({ 7 | viewportWidth: 600, 8 | viewportHeight: 800, 9 | experimentalStudio: true, 10 | experimentalInteractiveRunEvents: false, 11 | projectId: '89mmxs', 12 | e2e: { 13 | experimentalRunAllSpecs: true, 14 | // We've imported your old cypress plugins here. 15 | // You may want to clean this up later by importing these. 16 | setupNodeEvents(on, config) { 17 | // `on` is used to hook into various events Cypress emits 18 | // "cy.task" can be used from specs to "jump" into Node environment 19 | // and doing anything you might want. For example, checking "data.json" file! 20 | // https://on.cypress.io/task 21 | on('task', { 22 | // saves given or default empty data object into todomvc/data.json file 23 | // if the server is watching this file, next reload should show the updated values 24 | async resetData(dataToSet = DEFAULT_DATA) { 25 | resetData(dataToSet) 26 | 27 | // add a small delay for the server to "notice" 28 | // the changed JSON file and reload 29 | await delay(100) 30 | 31 | // cy.task handlers should always return something 32 | // otherwise it might be an accidental return 33 | return null 34 | }, 35 | 36 | hasSavedRecord(title, ms = 3000) { 37 | debug('inside task') 38 | console.log( 39 | 'looking for title "%s" in the database (time limit %dms)', 40 | title, 41 | ms 42 | ) 43 | return hasRecordAsync(title, ms) 44 | }, 45 | 46 | /** 47 | * Call this method using cy.task('getSavedTodos') command. 48 | * Make sure the backend had plenty of time to save the data. 49 | */ 50 | getSavedTodos() { 51 | const s = fs.readFileSync(getDbFilename(), 'utf8') 52 | const data = JSON.parse(s) 53 | console.log('returning %d saved todos', data.todos.length) 54 | return data.todos 55 | } 56 | }) 57 | 58 | on('before:spec', (spec) => { 59 | console.log('resetting DB before spec %s', spec.name) 60 | resetData() 61 | }) 62 | }, 63 | specPattern: ['cypress/e2e/*/spec.js', 'cypress/e2e/*/demo.js'], 64 | baseUrl: 'http://localhost:3000' 65 | } 66 | }) 67 | 68 | const getDbFilename = () => path.join(__dirname, 'todomvc', 'data.json') 69 | 70 | const findRecord = (title) => { 71 | const dbFilename = getDbFilename() 72 | const contents = JSON.parse(fs.readFileSync(dbFilename)) 73 | const todos = contents.todos 74 | return todos.find((record) => record.title === title) 75 | } 76 | 77 | const hasRecordAsync = (title, ms) => { 78 | const delay = 50 79 | return new Promise((resolve, reject) => { 80 | if (ms < 0) { 81 | return reject(new Error(`Could not find record with title "${title}"`)) 82 | } 83 | const found = findRecord(title) 84 | if (found) { 85 | return resolve(found) 86 | } 87 | setTimeout(() => { 88 | hasRecordAsync(title, ms - delay).then(resolve, reject) 89 | }, 50) 90 | }) 91 | } 92 | 93 | /** 94 | * Default object representing our "database" file in "todomvc/data.json" 95 | */ 96 | const DEFAULT_DATA = { 97 | todos: [] 98 | } 99 | 100 | const resetData = (dataToSet = DEFAULT_DATA) => { 101 | const dbFilename = getDbFilename() 102 | debug('reset data file %s with %o', dbFilename, dataToSet) 103 | if (!dataToSet) { 104 | console.error('Cannot save empty object in %s', dbFilename) 105 | throw new Error('Cannot save empty object in resetData') 106 | } 107 | const str = JSON.stringify(dataToSet, null, 2) + '\n' 108 | fs.writeFileSync(dbFilename, str, 'utf8') 109 | } 110 | 111 | async function delay(ms) { 112 | return new Promise((resolve) => { 113 | setTimeout(resolve, ms) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /cypress.slides-config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | viewportWidth: 600, 5 | viewportHeight: 800, 6 | e2e: { 7 | baseUrl: 'http://localhost:3100', 8 | specPattern: 'cypress/slides-tests/*.js', 9 | supportFile: false, 10 | fixturesFolder: false 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/ci-tests/api-spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | enterTodo, 3 | getTodoItems, 4 | makeTodo, 5 | resetDatabase, 6 | stubMathRandom, 7 | visit 8 | } from '../support/utils' 9 | 10 | // testing TodoMVC server API 11 | // because json-server can fail sometimes, let the tests retry 12 | // https://on.cypress.io/test-retries 13 | describe('via API', { retries: 2 }, () => { 14 | beforeEach(resetDatabase) 15 | 16 | // used to create predictable ids 17 | let counter = 1 18 | beforeEach(() => { 19 | counter = 1 20 | }) 21 | 22 | const addTodo = (title) => 23 | // the way to print newly created id in the command log 24 | cy 25 | .request('POST', '/todos', { 26 | title, 27 | completed: false, 28 | id: String(counter++) 29 | }) 30 | .its('body.id') 31 | .then(cy.log) 32 | 33 | /** 34 | * Fetches TODO items, returns just the body of the XHR request. 35 | */ 36 | const fetchTodos = () => 37 | cy 38 | .request('/todos') 39 | .its('body') 40 | .then((list) => { 41 | cy.log(JSON.stringify(list, null, 2)) 42 | cy.wrap(list, { log: false }) 43 | }) 44 | 45 | const deleteTodo = (id) => cy.request('DELETE', `/todos/${id}`) 46 | 47 | it('adds 2 todos', () => { 48 | addTodo('first todo') 49 | addTodo('second todo') 50 | fetchTodos().should('have.length', 2) 51 | }) 52 | 53 | it('adds todo deep', () => { 54 | addTodo('first todo') 55 | addTodo('second todo') 56 | fetchTodos().should('deep.equal', [ 57 | { 58 | title: 'first todo', 59 | completed: false, 60 | id: '1' 61 | }, 62 | { 63 | title: 'second todo', 64 | completed: false, 65 | id: '2' 66 | } 67 | ]) 68 | }) 69 | 70 | it('adds and deletes a todo', () => { 71 | addTodo('first todo') // id "1" 72 | addTodo('second todo') // id "2" 73 | deleteTodo('2') 74 | fetchTodos().should('deep.equal', [ 75 | { 76 | title: 'first todo', 77 | completed: false, 78 | id: '1' 79 | } 80 | ]) 81 | }) 82 | }) 83 | 84 | describe('stub network', () => { 85 | it('initial todos', () => { 86 | cy.intercept('/todos', [ 87 | { 88 | title: 'mock first', 89 | completed: false, 90 | id: '1' 91 | }, 92 | { 93 | title: 'mock second', 94 | completed: true, 95 | id: '2' 96 | } 97 | ]) 98 | 99 | visit(true) 100 | getTodoItems() 101 | .should('have.length', 2) 102 | .contains('li', 'mock second') 103 | .find('.toggle') 104 | .should('be.checked') 105 | }) 106 | }) 107 | 108 | describe('API', { retries: 2 }, () => { 109 | beforeEach(resetDatabase) 110 | beforeEach(() => visit(true)) 111 | beforeEach(stubMathRandom) 112 | 113 | it('receives empty list of items', () => { 114 | cy.request('todos').its('body').should('deep.equal', []) 115 | }) 116 | 117 | it('adds two items', () => { 118 | const first = makeTodo() 119 | const second = makeTodo() 120 | 121 | cy.request('POST', 'todos', first) 122 | cy.request('POST', 'todos', second) 123 | cy.request('todos') 124 | .its('body') 125 | .should('have.length', 2) 126 | .and('deep.equal', [first, second]) 127 | }) 128 | 129 | it('adds two items and deletes one', () => { 130 | const first = makeTodo() 131 | const second = makeTodo() 132 | cy.request('POST', 'todos', first) 133 | cy.request('POST', 'todos', second) 134 | cy.request('DELETE', `todos/${first.id}`) 135 | cy.request('todos') 136 | .its('body') 137 | .should('have.length', 1) 138 | .and('deep.equal', [second]) 139 | }) 140 | 141 | it('does not delete non-existent item', () => { 142 | cy.request({ 143 | method: 'DELETE', 144 | url: 'todos/aaa111bbb', 145 | failOnStatusCode: false 146 | }) 147 | .its('status') 148 | .should('equal', 404) 149 | }) 150 | 151 | it('is adding todo item', () => { 152 | cy.intercept({ 153 | method: 'POST', 154 | url: '/todos' 155 | }).as('postTodo') 156 | 157 | // go through the UI 158 | enterTodo('first item') // id "1" 159 | 160 | // thanks to stubbed random id generator 161 | // we can "predict" what the TODO object is going to look like 162 | cy.wait('@postTodo').its('request.body').should('deep.equal', { 163 | title: 'first item', 164 | completed: false, 165 | id: '1' 166 | }) 167 | }) 168 | 169 | it('is deleting a todo item', () => { 170 | cy.intercept({ 171 | method: 'DELETE', 172 | url: '/todos/1' 173 | }).as('deleteTodo') 174 | 175 | // go through the UI 176 | enterTodo('first item') // id "1" 177 | getTodoItems().first().find('.destroy').click({ force: true }) 178 | 179 | cy.wait('@deleteTodo') 180 | }) 181 | }) 182 | -------------------------------------------------------------------------------- /cypress/ci-tests/ui-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | enterTodo, 4 | getNewTodoInput, 5 | getTodoApp, 6 | getTodoItems, 7 | resetDatabase, 8 | visit 9 | } from '../support/utils' 10 | 11 | it('loads the app', () => { 12 | visit() 13 | getTodoApp().should('be.visible') 14 | }) 15 | 16 | describe('UI', () => { 17 | beforeEach(resetDatabase) 18 | beforeEach(() => visit()) 19 | 20 | context('basic features', () => { 21 | it('loads application', () => { 22 | getTodoApp().should('be.visible') 23 | }) 24 | 25 | it('starts with zero items', () => { 26 | cy.get('.todo-list').find('li').should('have.length', 0) 27 | }) 28 | 29 | it('adds two items', () => { 30 | enterTodo('first item') 31 | enterTodo('second item') 32 | getTodoItems().should('have.length', 2) 33 | }) 34 | 35 | it('enters text in the input', () => { 36 | const text = 'do something' 37 | getNewTodoInput().type(text) 38 | getNewTodoInput().should('have.value', text) 39 | }) 40 | 41 | it('can add many items', () => { 42 | const N = 5 43 | for (let k = 0; k < N; k += 1) { 44 | enterTodo(`item ${k + 1}`) 45 | } 46 | getTodoItems().should('have.length', N) 47 | }) 48 | }) 49 | 50 | context('advanced', () => { 51 | it('adds two and deletes first', () => { 52 | enterTodo('first item') 53 | enterTodo('second item') 54 | 55 | getTodoItems() 56 | .contains('first item') 57 | .parent() 58 | .find('.destroy') 59 | .click({ force: true }) // because it only becomes visible on hover 60 | 61 | cy.contains('first item').should('not.exist') 62 | cy.contains('second item').should('exist') 63 | getTodoItems().should('have.length', 1) 64 | }) 65 | }) 66 | 67 | context('cy.tasks', () => { 68 | it('can observe records saved in the database', () => { 69 | const title = 'create a task' 70 | enterTodo(title) 71 | // https://on.cypress.io/task 72 | cy.task('hasSavedRecord', title, { timeout: 10000 }) 73 | }) 74 | 75 | it('returns resolved value', () => { 76 | const title = 'create a task' 77 | enterTodo(title) 78 | // https://on.cypress.io/task 79 | cy.task('hasSavedRecord', title, { timeout: 10000 }) 80 | .should('contain', { 81 | title, 82 | completed: false 83 | }) 84 | // there is also an ID 85 | .and('have.property', 'id') 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /cypress/custom-commands.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // add custom command to Cypress declaration 3 | // see https://github.com/cypress-io/cypress-example-todomvc/blob/master/cypress/support/index.d.ts 4 | -------------------------------------------------------------------------------- /cypress/e2e/01-basic/answer.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | it('loads', () => { 4 | // application should be running at port 3000 5 | // see the documentation for "cy.visit" command 6 | // in the Cypress docs at https://on.cypress.io/visit 7 | // TIP: all commands are linked from https://on.cypress.io/api 8 | cy.visit('localhost:3000') 9 | 10 | // passing assertions 11 | // https://on.cypress.io/get 12 | cy.get('.new-todo').get('footer') 13 | 14 | // https://on.cypress.io/contains 15 | // use ("selector", "text") arguments to "cy.contains" 16 | cy.contains('h1', 'todos') 17 | 18 | // or can use regular expression 19 | cy.contains('h1', /^todos$/) 20 | 21 | // also good practice is to use data attributes specifically for testing 22 | // see https://on.cypress.io/best-practices#Selecting-Elements 23 | // which play well with "Selector Playground" tool 24 | cy.contains('[data-cy=app-title]', 'todos') 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/01-basic/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | // @ts-check 3 | it('loads', () => { 4 | // application should be running at port 3000 5 | // see the documentation for "cy.visit" command 6 | // in the Cypress docs at https://on.cypress.io/visit 7 | // TIP: all commands are linked from https://on.cypress.io/api 8 | cy.visit('localhost:3000') 9 | 10 | // passing assertions 11 | // https://on.cypress.io/get 12 | cy.get('.new-todo').get('footer') 13 | 14 | // this assertion fails on purpose 15 | // can you fix it? 16 | // https://on.cypress.io/contains 17 | cy.contains('h1', 'Todos App') 18 | 19 | // can you write "cy.contains" using regular expression? 20 | // cy.contains('h1', /.../) 21 | 22 | // also good practice is to use data attributes specifically for testing 23 | // see https://on.cypress.io/best-practices#Selecting-Elements 24 | // which play well with "Selector Playground" tool 25 | // how would you do select this element? 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/02-adding-items/answer-delete.js: -------------------------------------------------------------------------------- 1 | /// 2 | // IMPORTANT ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 3 | // remember to manually delete all items before running the test 4 | // IMPORTANT ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 5 | 6 | /** 7 | * Adds a todo item 8 | * @param {string} text 9 | */ 10 | const addItem = (text) => { 11 | cy.get('.new-todo').type(`${text}{enter}`) 12 | } 13 | 14 | beforeEach(() => { 15 | cy.visit('localhost:3000') 16 | }) 17 | 18 | it('can delete an item', () => { 19 | // adds a few items 20 | addItem('simple') 21 | addItem('hard') 22 | // deletes the first item 23 | cy.contains('li.todo', 'simple') 24 | .should('exist') 25 | .find('.destroy') 26 | // use force: true because we don't have the hover 27 | .click({ force: true }) 28 | 29 | // confirm the deleted item is gone from the dom 30 | cy.contains('li.todo', 'simple').should('not.exist') 31 | // confirm the other item still exists 32 | cy.contains('li.todo', 'hard').should('exist') 33 | }) 34 | 35 | it('deletes all items at the start', () => { 36 | // visit the page 37 | // wait for the page to load the todos 38 | // using cy.wait() for now 39 | cy.wait(1000) 40 | // get all todo items (there might not be any!) 41 | cy.get('li.todo') 42 | .should(Cypress._.noop) 43 | // for each todo item click the remove button 44 | .each(($item) => { 45 | cy.wrap($item).find('.destroy').click({ force: true }) 46 | }) 47 | // confirm that the item is gone from the dom 48 | cy.get('li.todo').should('not.exist') 49 | }) 50 | 51 | it('deletes all items at the start (click multiple elements)', () => { 52 | // visit the page 53 | // wait for the page to load the todos 54 | // using cy.wait() for now 55 | cy.wait(1000) 56 | // get all todo elements and their destroy buttons 57 | // (there might not be any!) 58 | // the click on them all at once 59 | // see https://on.cypress.io/click documentation 60 | cy.get('li.todo .destroy') 61 | .should(Cypress._.noop) 62 | .then(($destroy) => { 63 | if ($destroy.length) { 64 | cy.wrap($destroy).click({ force: true, multiple: true }) 65 | } 66 | }) 67 | // confirm that the item is gone from the dom 68 | cy.get('li.todo').should('not.exist') 69 | }) 70 | -------------------------------------------------------------------------------- /cypress/e2e/02-adding-items/answer-many-items.js: -------------------------------------------------------------------------------- 1 | /// 2 | // IMPORTANT ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 3 | // remember to manually delete all items before running the test 4 | // IMPORTANT ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 5 | 6 | /** 7 | * Adds a todo item 8 | * @param {string} text 9 | */ 10 | const addItem = (text) => { 11 | cy.get('.new-todo').type(`${text}{enter}`) 12 | } 13 | 14 | beforeEach(() => { 15 | cy.visit('localhost:3000') 16 | }) 17 | 18 | it('can add many items', () => { 19 | // assumes there are no items at the beginning 20 | 21 | const N = 5 22 | for (let k = 0; k < N; k += 1) { 23 | addItem(`item ${k}`) 24 | } 25 | // check number of items 26 | cy.get('li.todo').should('have.length', 5) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/02-adding-items/demo.js: -------------------------------------------------------------------------------- 1 | /// 2 | const isLocalHost = () => Cypress.config('baseUrl').includes('localhost') 3 | 4 | if (isLocalHost()) { 5 | // we can reset data only when running locally 6 | beforeEach(function resetData() { 7 | cy.request('POST', '/reset', { 8 | todos: [] 9 | }) 10 | }) 11 | } 12 | 13 | beforeEach(function visitSite() { 14 | cy.log('Visiting', Cypress.config('baseUrl')) 15 | cy.visit('/') 16 | }) 17 | 18 | it('adds items', function () { 19 | cy.get('.new-todo') 20 | .type('todo A{enter}') 21 | .type('todo B{enter}') 22 | .type('todo C{enter}') 23 | .type('todo D{enter}') 24 | cy.get('.todo-list li') // command 25 | .should('have.length', 4) // assertion 26 | cy.log('**complete items**') 27 | cy.contains('[data-cy="remaining-count"]', '4') 28 | cy.contains('.todo', 'todo B').find('.toggle').click() 29 | cy.contains('[data-cy="remaining-count"]', '3') 30 | }) 31 | -------------------------------------------------------------------------------- /cypress/e2e/02-adding-items/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | it('loads', () => { 3 | // application should be running at port 3000 4 | cy.visit('localhost:3000') 5 | cy.contains('h1', 'todos') 6 | }) 7 | 8 | // IMPORTANT ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 9 | // remember to manually delete all items before running the test 10 | // IMPORTANT ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ 11 | 12 | it('adds two items', () => { 13 | // visit the site 14 | // https://on.cypress.io/visit 15 | // repeat twice 16 | // get the input field 17 | // https://on.cypress.io/get 18 | // type text and "enter" 19 | // https://on.cypress.io/type 20 | // assert that the new Todo item 21 | // has been added added to the list 22 | // cy.get(...).should('have.length', 2) 23 | }) 24 | 25 | it('can mark an item as completed', () => { 26 | // visit the site 27 | // adds a few items 28 | // marks the first item as completed 29 | // https://on.cypress.io/get 30 | // https://on.cypress.io/find 31 | // https://on.cypress.io/first 32 | // confirms the first item has the expected completed class 33 | // confirms the other items are still incomplete 34 | // check the number of remaining items 35 | }) 36 | 37 | it('can delete an item', () => { 38 | // adds a few items 39 | // deletes the first item 40 | // use force: true because we don't want to hover 41 | // confirm the deleted item is gone from the dom 42 | // confirm the other item still exists 43 | }) 44 | 45 | it('can add many items', () => { 46 | const N = 5 47 | for (let k = 0; k < N; k += 1) { 48 | // add an item 49 | // probably want to have a reusable function to add an item! 50 | } 51 | // check number of items 52 | }) 53 | 54 | it('shows the expected elements', () => { 55 | // TODO: remove duplicate commands that get an element 56 | // and check if it is visible 57 | // https://youtu.be/DnmnzemS_HA 58 | cy.get('header').should('be.visible') 59 | cy.get('footer').should('be.visible') 60 | cy.get('.new-todo').should('be.visible') 61 | }) 62 | 63 | it('adds item with random text', () => { 64 | // use a helper function with Math.random() 65 | // or Cypress._.random() to generate unique text label 66 | // add such item 67 | // and make sure it is visible and does not have class "completed" 68 | }) 69 | 70 | it('starts with zero items', () => { 71 | // check if the list is empty initially 72 | // find the selector for the individual TODO items in the list 73 | // use cy.get(...) and it should have length of 0 74 | // https://on.cypress.io/get 75 | // ".should('have.length', 0)" 76 | // or ".should('not.exist')" 77 | }) 78 | 79 | it('disables the built-in assertion', () => { 80 | // try to get a non-existent element 81 | // without failing the test 82 | // pass it to the `.then($el)` callback 83 | // to check it yourself 84 | }) 85 | 86 | it('deletes all items at the start', () => { 87 | // visit the page 88 | // wait for the page to load the todos 89 | // using cy.wait() for now 90 | // get all todo items (there might not be any!) 91 | // for each todo item click the remove button 92 | // tip: use cy.each and cy.wrap commands 93 | // confirm that the item is gone from the dom 94 | // using "should not exist" or "should have length 0" assertion 95 | }) 96 | 97 | it('deletes all items at the start (click multiple elements)', () => { 98 | // visit the page 99 | // wait for the page to load the todos 100 | // using cy.wait() for now 101 | // get all todo elements and their destroy buttons 102 | // (there might not be any!) 103 | // the click on them all at once 104 | // see https://on.cypress.io/click documentation 105 | // confirm that the item is gone from the dom 106 | }) 107 | 108 | it('adds one more todo item', () => { 109 | // make sure the application has loaded first 110 | // maybe using cy.wait() or by spying on the network call 111 | // or by checking something in the DOM 112 | cy.wait(1000) 113 | 114 | // take the initial number of items (could be zero!) 115 | // add one more todo via UI 116 | // take the new number of items 117 | // confirm it is the initial number + 1 118 | }) 119 | 120 | it('saves the added todos', () => { 121 | // use a random label 122 | // make sure the application has saved the item 123 | cy.wait(1000) 124 | // get the saved todos using cy.task from the plugins file 125 | // confirm the list includes an item with "title: randomLabel" 126 | }) 127 | 128 | it('does not allow adding blank todos', () => { 129 | // https://on.cypress.io/catalog-of-events#App-Events 130 | cy.on('uncaught:exception', () => { 131 | // check e.message to match expected error text 132 | // return false if you want to ignore the error 133 | }) 134 | 135 | // try adding an item with just spaces 136 | }) 137 | 138 | it('shows remaining count only if there are items', () => { 139 | // make sure the application has loaded first 140 | cy.wait(1000) 141 | // there are no todos 142 | // there is no footer 143 | // add one todo item 144 | // the footer should be visible and have the count of 1 145 | // delete the single todo 146 | // the footer is gone 147 | }) 148 | 149 | it('clears completed items', () => { 150 | // make sure the application has loaded first 151 | cy.wait(1000) 152 | // there are no todos 153 | // add two items 154 | // make both items completed 155 | const items = ['first', 'second'] 156 | // click the "Clear completed" button 157 | // the todo items should be gone 158 | // the footer should be gone 159 | // reload the page just to be sure the server has removed the items 160 | // there should be no items 161 | }) 162 | 163 | // watch the video "Write An API Test Using Cypress" 164 | // https://youtu.be/OWTrczUUVpA 165 | it('adds and deletes items using REST API calls', () => { 166 | // reset the backend data using POST /request call 167 | // https://on.cypress.io/request 168 | // add an item using POST /todos call 169 | // passing the title and the completed: false properties 170 | // from the response get the body and confirm 171 | // it has the expected properties, including the "id" 172 | // get the "id" property and confirm it is a number 173 | // TIP: add a short wait for our simple server to 174 | // really save the added item 175 | // then use the "id" property to get the item 176 | // and then use the DELETE /todos/:id call to delete it 177 | // the status of the response should be 200 178 | // 179 | // bonus: use cy.fixture command to reset the todos 180 | // or to create items one by one. Then visit the page 181 | // and verify the shown items 182 | }) 183 | 184 | it('completes an item using REST call', () => { 185 | // reset the todos on the server 186 | // create a new item using cy.request POST /todos call 187 | // and get its ID from the response 188 | // confirm the item is not completed by fetching it using GET /todos/:id 189 | // complete the item using the PATCH /todos/:id call 190 | // with { completed: true } 191 | // 192 | // after completing the item via an API call 193 | // visit the page (or reload) and confirm it is shown as completed 194 | // confirm the count of remaining items is 0 195 | }) 196 | 197 | it('creates todos from a fixture', () => { 198 | // reset the todos on the server 199 | // 200 | // load a list of todos from a fixture file using cy.fixture 201 | // https://on.cypress.io/fixture 202 | // get the list of todos from the fixture using .then callback 203 | // for each item make a cy.request to create it on the server 204 | // after creating all items, 205 | // reload the page and confirm each item is shown 206 | // 207 | // bonus: import the fixture directly into the spec for simplicity 208 | // read https://glebbahmutov.com/blog/import-cypress-fixtures/ 209 | }) 210 | 211 | it('checks the meta tags in the head element', () => { 212 | // visit the page "/" 213 | // https://on.cypress.io/visit 214 | // confirm the page title includes the string "TodoMVC" 215 | // tip: how would you set the title element from the DevTools console? 216 | // confirm the meta tag name is "Gleb Bahmutov" 217 | // confirm the meta tag description includes the expected text "workshop" 218 | }) 219 | 220 | describe('Title', () => { 221 | it('adds an item with test case title', () => { 222 | // confirm the current test title 223 | // confirm the current full test name 224 | // (the parent suite title(s) plus the test title) 225 | // 226 | // visit the page 227 | // 228 | // let all todos load 229 | cy.wait(1000) 230 | // normalize the full test title to remove characters 231 | // and maybe make it into a single string like "foo-bar-baz-..." 232 | // 233 | // add a new todo with the normalized title 234 | // 235 | // confirm the list has the new todo at the first position in the list 236 | }) 237 | }) 238 | 239 | // what a challenge? 240 | // test more UI at http://todomvc.com/examples/vue/ 241 | -------------------------------------------------------------------------------- /cypress/e2e/03-selector-playground/answer.js: -------------------------------------------------------------------------------- 1 | /// 2 | beforeEach(() => { 3 | cy.visit('/') 4 | }) 5 | it('loads', () => { 6 | cy.contains('h1', 'todos') 7 | }) 8 | // optional test data attribute selector helper 9 | const tid = (id) => `[data-cy="${id}"]` 10 | /** 11 | * Adds a todo item 12 | * @param {string} text 13 | */ 14 | const addItem = (text) => { 15 | cy.get('[data-cy="input"]').type(`${text}{enter}`) 16 | } 17 | 18 | // to enable this test need to add appropriate "data-cy" attributes 19 | it.skip('adds two items', () => { 20 | addItem('first item') 21 | addItem('second item') 22 | cy.get(tid('item')).should('have.length', 2) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/03-selector-playground/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-unused-vars */ 3 | 4 | beforeEach(() => { 5 | // application should be running at port 3000 6 | // and the "localhost:3000" is set as "baseUrl" in "cypress.json" 7 | cy.visit('/') 8 | }) 9 | it('loads', () => { 10 | cy.contains('h1', 'todos') 11 | }) 12 | // optional test data attribute selector helper 13 | // const tid = id => `[data-cy="${id}"]` 14 | /** 15 | * Adds a todo item 16 | * @param {string} text 17 | */ 18 | const addItem = (text) => { 19 | // write Cy commands here to add the new item 20 | } 21 | it('adds two items', () => { 22 | addItem('first item') 23 | addItem('second item') 24 | // fill the selector 25 | // maybe use "tid" function 26 | cy.get('selector').should('have.length', 2) 27 | }) 28 | 29 | it('can record a test', () => { 30 | // use Cypress Studio to record a test 31 | }) 32 | -------------------------------------------------------------------------------- /cypress/e2e/04-reset-state/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * Adds a todo item 4 | * @param {string} text 5 | */ 6 | const addItem = (text) => { 7 | cy.get('.new-todo').type(`${text}{enter}`) 8 | } 9 | 10 | describe('ANTI-PATTERN: reset state through the UI', () => { 11 | beforeEach(() => { 12 | cy.visit('/') 13 | // we need to wait for the items to be loaded 14 | cy.wait(1000) 15 | 16 | // how do you remove all items? 17 | // what happens if there are NO items? 18 | // Try removing all items and re-running the test 19 | cy.get('li.todo') 20 | // by default, cy.get retries until it finds at least 1 item 21 | // we can "trick" it to give us the items or not items 22 | // by adding our own assertion to pass even if 23 | // the number of items is zero 24 | .should('have.length.gte', 0) 25 | .then(($todos) => { 26 | // if there are no todos, we have nothing to clean up 27 | // the test thus has to use IF/ELSE statement 28 | // implementing its own logic 29 | // https://on.cypress.io/conditional-testing 30 | // 31 | // if 32 | // there are no todos, return 33 | // else: 34 | // there might be multiple items to click 35 | // and the destroy button is not visible 36 | // until the user hovers over it, thus 37 | // we need to force it to be clickable 38 | }) 39 | }) 40 | 41 | it('adds two items starting with zero', () => { 42 | // this test does not clean up after itself 43 | // leaving two items for the other test 44 | }) 45 | 46 | it('adds and removes an item', () => { 47 | // this test adds an item then cleans up after itself 48 | // leaving no items for other test to clean up 49 | }) 50 | }) 51 | 52 | describe('reset data using XHR call', () => { 53 | beforeEach(() => { 54 | // application should be running at port 3000 55 | // and the "localhost:3000" is set as "baseUrl" in "cypress.json" 56 | // TODO call /reset endpoint with POST method and object {todos: []} 57 | cy.visit('/') 58 | }) 59 | 60 | it('adds two items', () => { 61 | addItem('first item') 62 | addItem('second item') 63 | cy.get('li.todo').should('have.length', 2) 64 | }) 65 | }) 66 | 67 | describe('reset data using cy.writeFile', () => { 68 | beforeEach(() => { 69 | // TODO write file "todomvc/data.json" with stringified todos object 70 | // file path is relative to the project's root folder 71 | // where cypress.json is located 72 | cy.visit('/') 73 | }) 74 | 75 | it('adds two items', () => { 76 | addItem('first item') 77 | addItem('second item') 78 | cy.get('li.todo').should('have.length', 2) 79 | }) 80 | }) 81 | 82 | describe('reset data using a task', () => { 83 | beforeEach(() => { 84 | // TODO call a task to reset data 85 | cy.visit('/') 86 | }) 87 | 88 | it('adds two items', () => { 89 | addItem('first item') 90 | addItem('second item') 91 | cy.get('li.todo').should('have.length', 2) 92 | }) 93 | }) 94 | 95 | describe('set initial data', () => { 96 | it('sets data to complex object right away', () => { 97 | // TODO call task and pass an object with todos 98 | cy.visit('/') 99 | // check what is rendered 100 | }) 101 | 102 | it('sets data using fixture', () => { 103 | // TODO load todos from "cypress/fixtures/two-items.json" 104 | // https://on.cypress.io/fixture 105 | // and then call the task to set todos 106 | // https://on.cypress.io/task 107 | cy.visit('/') 108 | // check what is rendered 109 | }) 110 | }) 111 | 112 | describe('create todos using API', () => { 113 | it('creates a random number of items', () => { 114 | // reset the data on the server 115 | // pick a random number of todos to create between 1 and 10 116 | // form the todos array with random titles 117 | // tip: you can use console.table to print an array of objects 118 | // call cy.request to post each TODO item 119 | // visit the page and check the displayed number of todos 120 | }) 121 | 122 | it('creates a random number of items (Lodash)', () => { 123 | // reset the data on the server 124 | // create a random number of todos using cy.request 125 | // tip: use can use Lodash methods to draw a random number 126 | // look at the POST /todos calls the application sends 127 | // visit the page and check the displayed number of todos 128 | }) 129 | 130 | it('can modify JSON fixture as text and create todo', () => { 131 | // load the "two-items.json" from a fixture without converting it to JSON 132 | // replace the first item's title with some other text 133 | // replace the second item's title with some other text 134 | // convert the string to JSON and reset the data on the server 135 | // visit the page and confirm each item is present 136 | }) 137 | }) 138 | 139 | // problem with cy.session + setup + validate combination 140 | // SKIP https://github.com/cypress-io/cypress/issues/17805 141 | describe.skip('ANTI-PATTERN: reset state through the UI using cy.session', () => { 142 | function clearTodos() { 143 | cy.request('POST', '/reset', { todos: [] }) 144 | // cy.visit('/') 145 | // cy.get('body').should('have.class', 'loaded') 146 | // cy.get('li.todo').then(($todos) => { 147 | // cy.wrap($todos) 148 | // .find('.destroy') 149 | // // there might be multiple items to click 150 | // // and the destroy button is not visible 151 | // // until the user hovers over it, thus 152 | // // we need to force it to be clickable 153 | // .click({ multiple: true, force: true }) 154 | // }) 155 | } 156 | 157 | function validate() { 158 | // cy.request('/todos').its('body', { timeout: 0 }).should('have.length', 0) 159 | return false 160 | } 161 | 162 | beforeEach(() => { 163 | cy.session('reset-todos', clearTodos, { validate }) 164 | }) 165 | 166 | it('adds two items starting with zero', () => { 167 | cy.visit('/') 168 | // this test does not clean up after itself 169 | // leaving two items for the other test 170 | addItem('first item') 171 | addItem('second item') 172 | cy.get('li.todo').should('have.length', 2) 173 | }) 174 | }) 175 | 176 | describe('routing', () => { 177 | beforeEach(() => { 178 | // reset the app to have a few todos 179 | // load the fixture "three-items.json" using cy.fixture command 180 | // call the server and set the "todos" to the list of items 181 | }) 182 | 183 | it('shows todos based on selected filter', () => { 184 | // visit the page 185 | // by default, all todos are shown 186 | // and the "all" filter has the selected class 187 | // click on the "active" filter 188 | // make sure the URL changes correctly using cy.location command 189 | // make sure the "active" filter has the selected class 190 | // check the shown items 191 | // repeat for "completed" filter, and then back to "all" filter 192 | }) 193 | 194 | it('navigates to /active', () => { 195 | // visit the active route and make sure it loads 196 | // validate the application is showing the active todos 197 | // and the "active" filter has the selected class 198 | }) 199 | 200 | it('navigates to /completed', () => { 201 | // visit the completed route and make sure it loads 202 | // validate the application is showing the completed todos 203 | // and the "completed" filter has the selected class 204 | }) 205 | 206 | it('navigates to /all', () => { 207 | // visit the all route and make sure it loads 208 | // validate the application is showing the all todos 209 | // and the "all" filter has the selected class 210 | }) 211 | }) 212 | -------------------------------------------------------------------------------- /cypress/e2e/05-network/answer-clock.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // application periodically loads todos from the server 4 | // we do not want to wait 1 minute for the load call 5 | // instead we want to speed up the application's clock 6 | it('loads todos every minute', () => { 7 | // note that the interceptors are matched in reverse order 8 | // thus we put the last interceptor first 9 | // answer the 3rd and all other calls with one two 10 | cy.intercept( 11 | { 12 | method: 'GET', 13 | url: '/todos', 14 | times: 1 15 | }, 16 | { body: [{ id: 1, title: 'use cy.clock', completed: true }] } 17 | ) 18 | // answer the 2nd call with three items 19 | cy.intercept( 20 | { 21 | method: 'GET', 22 | url: '/todos', 23 | times: 1 24 | }, 25 | { fixture: 'three-items.json' } 26 | ).as('load2') 27 | // answer the first call with two items 28 | cy.intercept( 29 | { 30 | method: 'GET', 31 | url: '/todos', 32 | times: 1 33 | }, 34 | { fixture: 'two-items.json' } 35 | ).as('load1') 36 | // leave the date unchanged, and only "freeze" the setInterval function 37 | cy.clock(null, ['setInterval']) 38 | cy.visit('/') 39 | cy.wait('@load1') 40 | cy.get('li.todo').should('have.length', 2) 41 | // make the application think an entire minute has passed 42 | cy.tick(60000) 43 | cy.wait('@load2') 44 | cy.get('li.todo').should('have.length', 3) 45 | // another minute passes 46 | cy.tick(60000) 47 | cy.get('li.todo').should('have.length', 1).contains('.todo', 'use cy.clock') 48 | }) 49 | -------------------------------------------------------------------------------- /cypress/e2e/05-network/answer-idle.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('waits for network idle', () => { 4 | // we want to wait for the app to finish all network calls 5 | // before proceeding with the test commands 6 | 7 | beforeEach(() => { 8 | // before each test, stub the network call to load zero items 9 | cy.intercept('GET', '/todos', []).as('todos') 10 | }) 11 | 12 | it('waits for the network to be idle for 2 seconds', () => { 13 | // keep track of the timestamp of the network call 14 | // intercept all calls (or maybe a specific pattern) 15 | // and in the callback save the current timestamp 16 | let lastNetworkAt 17 | cy.intercept('*', () => { 18 | lastNetworkAt = +new Date() 19 | }) 20 | // load the page, but delay loading of the data by some random number 21 | // using /?delay= query param 22 | const delayMs = Cypress._.random(100, 1500) 23 | cy.visit(`/?delay=${delayMs}`).then(() => { 24 | // start waiting after the cy.visit command finishes 25 | 26 | // wait for network to be idle for 1 second 27 | // using a .should(cb) assertion that looks at the current timestamp 28 | // vs the timestamp of the last network call 29 | // see assertion examples at 30 | // https://glebbahmutov.com/cypress-examples/commands/assertions.html 31 | // TIP: cy.wrap('message').should(cb) works really well 32 | const started = +new Date() 33 | let finished 34 | cy.wrap('network idle for 2 sec') 35 | .should(() => { 36 | const t = lastNetworkAt || started 37 | const elapsed = +new Date() - t 38 | if (elapsed < 2000) { 39 | throw new Error('Network is busy') 40 | } 41 | finished = +new Date() 42 | }) 43 | .then(() => { 44 | const waited = finished - started 45 | cy.log(`finished after ${waited} ms`) 46 | }) 47 | }) 48 | // by now everything should have been loaded 49 | // we can check the page and use a very short timeout 50 | // because the page is ready to be tested 51 | cy.get('.todo-list li', { timeout: 10 }).should('have.length', 0) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /cypress/e2e/05-network/answer-json.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // read the blog post "Visit Non-HTML Page" 4 | // https://glebbahmutov.com/blog/visit-non-html-page/ 5 | describe( 6 | 'visit non-html page', 7 | { viewportWidth: 400, viewportHeight: 100 }, 8 | () => { 9 | beforeEach(() => { 10 | cy.fixture('two-items').as('todos') 11 | }) 12 | 13 | beforeEach(function () { 14 | // by using "function () {}" callback we can access 15 | // the alias created in the previous hook using "this." 16 | cy.task('resetData', { todos: this.todos }) 17 | }) 18 | 19 | /* 20 | Skipping because this will cause an error: 21 | 22 | cy.visit() failed trying to load: 23 | 24 | http://localhost:3000/todos/1 25 | 26 | The content-type of the response we received from your web server was: 27 | 28 | > application/json 29 | 30 | This was considered a failure because responses must have content-type: 'text/html' 31 | */ 32 | it.skip('tries to visit JSON resource', () => { 33 | cy.visit('/todos/1') 34 | }) 35 | 36 | it('visits the todo JSON response', function () { 37 | cy.intercept('GET', '/todos/*', (req) => { 38 | req.continue((res) => { 39 | if (res.headers['content-type'].includes('application/json')) { 40 | res.headers['content-type'] = 'text/html' 41 | const text = `
${JSON.stringify(
42 |               res.body,
43 |               null,
44 |               2
45 |             )}
` 46 | res.send(text) 47 | } 48 | }) 49 | }).as('todo') 50 | cy.visit('/todos/1') 51 | // make sure you intercept has worked 52 | cy.wait('@todo') 53 | // check the text shown in the browser 54 | cy.contains(this.todos[0].title) 55 | // confirm the item ID is in the URL 56 | // 1. less than ideal, since we use position arguments 57 | cy.location('pathname') 58 | .should('include', '/todos/') 59 | // we have a string, which we can split by '/' 60 | .invoke('split', '/') 61 | // and get the 3rd item in the array ["", "todos", "1"] 62 | .its(2) 63 | // and verify this is the same as the item ID 64 | .should('eq', '1') 65 | // 2. alternative: use regex exec with a capture group 66 | // https://javascript.info/regexp-groups 67 | cy.location('pathname') 68 | .should('match', /\/todos\/\d+/) 69 | // use named capture group to get the ID from the string 70 | .then((s) => /\/todos\/(?\d+)/.exec(s)) 71 | .its('groups.id') 72 | .should('equal', '1') 73 | // 3. use regular expression match with a capture group 74 | cy.location('pathname') 75 | .should('include', 'todos') 76 | // use named capture group to get the ID from the string 77 | .invoke('match', /\/todos\/(?\d+)/) 78 | .its('groups.id') 79 | .should('equal', '1') 80 | }) 81 | } 82 | ) 83 | -------------------------------------------------------------------------------- /cypress/e2e/05-network/answer-loader.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | it('shows loading element', () => { 4 | // delay XHR to "/todos" by a few seconds 5 | // and respond with an empty list 6 | cy.intercept( 7 | { 8 | method: 'GET', 9 | pathname: '/todos' 10 | }, 11 | { 12 | body: [], 13 | delayMs: 2000 14 | } 15 | ).as('loading') 16 | cy.visit('/') 17 | 18 | // shows Loading element 19 | cy.get('.loading').should('be.visible') 20 | 21 | // wait for the network call to complete 22 | cy.wait('@loading') 23 | 24 | // now the Loading element should go away 25 | cy.get('.loading').should('not.be.visible') 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/05-network/answer-refactor.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import spok from 'cy-spok' 4 | 5 | // read the blog post "How To Check Network Requests Using Cypress" 6 | // https://glebbahmutov.com/blog/network-requests-with-cypress/ 7 | describe('Refactor network code example', () => { 8 | beforeEach(() => { 9 | cy.intercept('GET', '/todos', []).as('todos') 10 | cy.visit('/') 11 | }) 12 | 13 | it('validates and processes the intercept object', () => { 14 | cy.intercept('POST', '/todos').as('postTodo') 15 | const title = 'new todo' 16 | const completed = false 17 | cy.get('.new-todo').type(title + '{enter}') 18 | cy.wait('@postTodo') 19 | .then((intercept) => { 20 | // get the field from the intercept object 21 | const { statusCode, body } = intercept.response 22 | // confirm the status code is 201 23 | expect(statusCode).to.eq(201) 24 | // confirm some properties of the response data 25 | expect(body.title).to.equal(title) 26 | expect(body.completed).to.equal(completed) 27 | // return the field from the body object 28 | return body.id 29 | }) 30 | .then(cy.log) 31 | }) 32 | 33 | it('extracts the response property first', () => { 34 | cy.intercept('POST', '/todos').as('postTodo') 35 | const title = 'new todo' 36 | const completed = false 37 | cy.get('.new-todo').type(title + '{enter}') 38 | cy.wait('@postTodo') 39 | .its('response') 40 | .then((response) => { 41 | const { statusCode, body } = response 42 | // confirm the status code is 201 43 | expect(statusCode).to.eq(201) 44 | // confirm some properties of the response data 45 | expect(body.title).to.equal(title) 46 | expect(body.completed).to.equal(completed) 47 | // return the field from the body object 48 | return body.id 49 | }) 50 | .then(cy.log) 51 | }) 52 | 53 | it('checks the status code', () => { 54 | cy.intercept('POST', '/todos').as('postTodo') 55 | const title = 'new todo' 56 | const completed = false 57 | cy.get('.new-todo').type(title + '{enter}') 58 | cy.wait('@postTodo') 59 | .its('response') 60 | .then((response) => { 61 | const { body } = response 62 | // confirm the status code is 201 63 | expect(response).to.have.property('statusCode', 201) 64 | // confirm some properties of the response data 65 | expect(body.title).to.equal(title) 66 | expect(body.completed).to.equal(completed) 67 | // return the field from the body object 68 | return body.id 69 | }) 70 | .then(cy.log) 71 | }) 72 | 73 | it('checks the status code in its own then', () => { 74 | cy.intercept('POST', '/todos').as('postTodo') 75 | const title = 'new todo' 76 | const completed = false 77 | cy.get('.new-todo').type(title + '{enter}') 78 | cy.wait('@postTodo') 79 | .its('response') 80 | .then((response) => { 81 | // confirm the status code is 201 82 | expect(response).to.have.property('statusCode', 201) 83 | }) 84 | .its('body') 85 | .then((body) => { 86 | // confirm some properties of the response data 87 | expect(body.title).to.equal(title) 88 | expect(body.completed).to.equal(completed) 89 | // return the field from the body object 90 | return body.id 91 | }) 92 | .then(cy.log) 93 | }) 94 | 95 | it('checks the body object', () => { 96 | cy.intercept('POST', '/todos').as('postTodo') 97 | const title = 'new todo' 98 | const completed = false 99 | cy.get('.new-todo').type(title + '{enter}') 100 | cy.wait('@postTodo') 101 | .its('response') 102 | .then((response) => { 103 | // confirm the status code is 201 104 | expect(response).to.have.property('statusCode', 201) 105 | }) 106 | .its('body') 107 | .then((body) => { 108 | // confirm some properties of the response data 109 | expect(body).to.deep.include({ 110 | title, 111 | completed 112 | }) 113 | }) 114 | .its('id') 115 | .then(cy.log) 116 | }) 117 | 118 | it('checks the body object using should', () => { 119 | cy.intercept('POST', '/todos').as('postTodo') 120 | const title = 'new todo' 121 | const completed = false 122 | cy.get('.new-todo').type(title + '{enter}') 123 | cy.wait('@postTodo') 124 | .its('response') 125 | .then((response) => { 126 | // confirm the status code is 201 127 | expect(response).to.have.property('statusCode', 201) 128 | }) 129 | .its('body') 130 | .should('deep.include', { title, completed }) 131 | .its('id') 132 | .then(cy.log) 133 | }) 134 | 135 | it('checks the body object using cy-spok', () => { 136 | cy.intercept('POST', '/todos').as('postTodo') 137 | const title = 'new todo' 138 | const completed = false 139 | cy.get('.new-todo').type(title + '{enter}') 140 | cy.wait('@postTodo') 141 | .its('response') 142 | .should( 143 | spok({ 144 | statusCode: 201 145 | }) 146 | ) 147 | .its('body') 148 | .should( 149 | spok({ 150 | title, 151 | completed 152 | }) 153 | ) 154 | .its('id') 155 | .then(cy.log) 156 | }) 157 | 158 | it('checks the response using cy-spok', () => { 159 | cy.intercept('POST', '/todos').as('postTodo') 160 | const title = 'new todo' 161 | const completed = false 162 | cy.get('.new-todo').type(title + '{enter}') 163 | cy.wait('@postTodo') 164 | .its('response') 165 | .should( 166 | spok({ 167 | statusCode: 201, 168 | body: { 169 | title, 170 | completed, 171 | id: spok.string 172 | } 173 | }) 174 | ) 175 | .its('body.id') 176 | .then(cy.log) 177 | }) 178 | }) 179 | -------------------------------------------------------------------------------- /cypress/e2e/06-app-data-store/answer.js: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * Adds a todo item 4 | * @param {string} text 5 | */ 6 | const addItem = (text) => { 7 | cy.get('.new-todo').type(`${text}{enter}`) 8 | } 9 | 10 | // allow re-running each test up to 2 more attempts 11 | // on failure. This avoids flaky tests on CI 12 | // https://on.cypress.io/test-retries 13 | describe('App Data Store', { retries: 2 }, () => { 14 | beforeEach(() => { 15 | cy.request('POST', '/reset', { 16 | todos: [] 17 | }) 18 | }) 19 | beforeEach(() => { 20 | cy.visit('/') 21 | }) 22 | beforeEach(function stubRandomId() { 23 | let count = 1 24 | cy.window() 25 | .its('Math') 26 | .then((Math) => { 27 | cy.stub(Math, 'random', () => { 28 | return `0.${count++}` 29 | }).as('random') // save reference to the spy 30 | }) 31 | }) 32 | 33 | it('logs a todo add message to the console', () => { 34 | // get the window object from the app's iframe 35 | // using https://on.cypress.io/window 36 | // get its console object and spy on the "log" method 37 | // using https://on.cypress.io/spy 38 | cy.window() 39 | .its('console') 40 | .then((console) => { 41 | cy.spy(console, 'log').as('log') 42 | }) 43 | // add a new todo item 44 | // get the spy and check that it was called 45 | // with the expected arguments 46 | addItem('new todo') 47 | cy.get('@log').should( 48 | 'have.been.calledWith', 49 | 'tracking event "%s"', 50 | 'todo.add' 51 | ) 52 | }) 53 | 54 | afterEach(function () { 55 | // makes debugging failing tests much simpler 56 | cy.screenshot(this.currentTest.fullTitle(), { capture: 'runner' }) 57 | }) 58 | 59 | it('has window.app property', () => { 60 | // get its "app" property 61 | // and confirm it is an object 62 | // see https://on.cypress.io/its 63 | cy.window().its('app').should('be.an', 'object') 64 | }) 65 | 66 | it('has vuex store', () => { 67 | // check app's $store property 68 | // and confirm it has typical Vuex store methods 69 | // see https://on.cypress.io/its 70 | cy.window() 71 | .its('app.$store') 72 | .should('include.keys', ['commit', 'dispatch']) 73 | .its('state') 74 | .should('be.an', 'object') 75 | .its('todos') 76 | .should('be.an', 'array') 77 | }) 78 | 79 | it('starts with an empty store', () => { 80 | // the list of todos in the Vuex store should be empty 81 | cy.window().its('app.$store.state.todos').should('have.length', 0) 82 | }) 83 | 84 | it('adds items to store', () => { 85 | addItem('something') 86 | addItem('something else') 87 | cy.window().its('app.$store.state.todos').should('have.length', 2) 88 | }) 89 | 90 | it('creates an item with id 1', () => { 91 | cy.intercept('POST', '/todos').as('new-item') 92 | addItem('something') 93 | cy.wait('@new-item').its('request.body').should('deep.equal', { 94 | id: '1', 95 | title: 'something', 96 | completed: false 97 | }) 98 | }) 99 | 100 | it('calls spy twice', () => { 101 | addItem('something') 102 | addItem('else') 103 | cy.get('@random').should('have.been.calledTwice') 104 | }) 105 | 106 | it('puts todos in the store', () => { 107 | addItem('something') 108 | addItem('else') 109 | cy.window() 110 | .its('app.$store.state.todos') 111 | .should('deep.equal', [ 112 | { title: 'something', completed: false, id: '1' }, 113 | { title: 'else', completed: false, id: '2' } 114 | ]) 115 | }) 116 | 117 | it('adds todos via app', () => { 118 | // bypass the UI and call app's actions directly from the test 119 | // app.$store.dispatch('setNewTodo', ) 120 | // app.$store.dispatch('addTodo') 121 | // using https://on.cypress.io/invoke 122 | cy.window().its('app.$store').invoke('dispatch', 'setNewTodo', 'new todo') 123 | 124 | cy.window().its('app.$store').invoke('dispatch', 'addTodo') 125 | // and then check the UI 126 | cy.contains('li.todo', 'new todo') 127 | }) 128 | 129 | it('handles todos with blank title', () => { 130 | // bypass the UI and call app's actions directly from the test 131 | // app.$store.dispatch('setNewTodo', ) 132 | // app.$store.dispatch('addTodo') 133 | cy.window().its('app.$store').invoke('dispatch', 'setNewTodo', ' ') 134 | 135 | cy.window().its('app.$store').invoke('dispatch', 'addTodo') 136 | 137 | // confirm the application is not breaking 138 | cy.get('li.todo') 139 | .should('have.length', 1) 140 | .first() 141 | .should('not.have.class', 'completed') 142 | .find('label') 143 | .should('have.text', ' ') 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /cypress/e2e/06-app-data-store/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * Adds a todo item 4 | * @param {string} text 5 | */ 6 | const addItem = (text) => { 7 | cy.get('.new-todo').type(`${text}{enter}`) 8 | } 9 | 10 | // application should be running at port 3000 11 | // and the "localhost:3000" is set as "baseUrl" in "cypress.json" 12 | beforeEach(() => { 13 | cy.request('POST', '/reset', { 14 | todos: [] 15 | }) 16 | }) 17 | beforeEach(() => { 18 | cy.visit('/') 19 | }) 20 | 21 | it('logs a todo add message to the console', () => { 22 | // get the window object from the app's iframe 23 | // using https://on.cypress.io/window 24 | // get its console object and spy on the "log" method 25 | // using https://on.cypress.io/spy 26 | // add a new todo item 27 | // get the spy and check that it was called 28 | // with the expected arguments 29 | }) 30 | 31 | it('has window.app property', () => { 32 | // get its "app" property 33 | // and confirm it is an object 34 | // see https://on.cypress.io/its 35 | cy.window() 36 | }) 37 | 38 | it('has vuex store', () => { 39 | // check app's $store property 40 | // and confirm it has typical Vuex store methods 41 | // see https://on.cypress.io/its 42 | cy.window() 43 | }) 44 | 45 | it('starts with an empty store', () => { 46 | // the list of todos in the Vuex store should be empty 47 | cy.window() 48 | }) 49 | 50 | it('adds items to store', () => { 51 | addItem('something') 52 | addItem('something else') 53 | // get application's window 54 | // then get app, $store, state, todos 55 | // it should have 2 items 56 | }) 57 | 58 | it('creates an item with id 1', () => { 59 | cy.intercept('POST', '/todos').as('new-item') 60 | 61 | // TODO change Math.random to be deterministic 62 | 63 | // STEPS 64 | // get the application's "window" object using cy.window 65 | // then change its Math object and replace it 66 | // with your function that always returns "0.1" 67 | 68 | addItem('something') 69 | // confirm the item sent to the server has the right values 70 | cy.wait('@new-item').its('request.body').should('deep.equal', { 71 | id: '1', 72 | title: 'something', 73 | completed: false 74 | }) 75 | }) 76 | 77 | // stub function Math.random using cy.stub 78 | it('creates an item with id using a stub', () => { 79 | // get the application's "window.Math" object using cy.window 80 | // replace Math.random with cy.stub and store the stub under an alias 81 | // create a todo using addItem("foo") 82 | // and then confirm that the stub was called once 83 | }) 84 | 85 | it('puts the todo items into the data store', () => { 86 | // application uses data store to store its items 87 | // you can get the data store using "window.app.$store.state.todos" 88 | // add a couple of items 89 | // get the data store 90 | // check its contents 91 | }) 92 | 93 | it('handles todos with blank title', () => { 94 | // bypass the UI and call app's actions directly from the test 95 | // app.$store.dispatch('setNewTodo', ) 96 | // app.$store.dispatch('addTodo') 97 | // using https://on.cypress.io/invoke 98 | // and then 99 | // confirm the application is not breaking 100 | }) 101 | -------------------------------------------------------------------------------- /cypress/e2e/08-retry-ability/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-unused-vars */ 3 | 4 | import 'cypress-cdp' 5 | 6 | describe('retry-ability', () => { 7 | beforeEach(function resetData() { 8 | cy.request('POST', '/reset', { 9 | todos: [] 10 | }) 11 | }) 12 | 13 | beforeEach(function visitSite() { 14 | // do not delay adding new items after pressing Enter 15 | cy.visit('/') 16 | // enable a delay when adding new items 17 | // cy.visit('/?addTodoDelay=1000') 18 | }) 19 | 20 | it('creates 2 items', function () { 21 | cy.visit('/?addTodoDelay=1000') // command 22 | cy.get('.new-todo') // query 23 | .type('todo A{enter}') // command 24 | .type('todo B{enter}') // command 25 | cy.get('.todo-list li') // query 26 | .should('have.length', 2) // assertion 27 | }) 28 | 29 | it('shows UL', function () { 30 | cy.get('.new-todo') 31 | .type('todo A{enter}') 32 | .type('todo B{enter}') 33 | .type('todo C{enter}') 34 | .type('todo D{enter}') 35 | cy.contains('ul', 'todo A') 36 | // confirm that the above element 37 | // 1. is visible 38 | // 2. has class "todo-list" 39 | // 3. css property "list-style-type" is equal "none" 40 | }) 41 | 42 | it('shows UL - TDD', function () { 43 | cy.get('.new-todo') 44 | .type('todo A{enter}') 45 | .type('todo B{enter}') 46 | .type('todo C{enter}') 47 | .type('todo D{enter}') 48 | cy.contains('ul', 'todo A').then(($ul) => { 49 | // use TDD assertions 50 | // $ul is visible 51 | // $ul has class "todo-list" 52 | // $ul css has "list-style-type" = "none" 53 | }) 54 | }) 55 | 56 | it('every item starts with todo', function () { 57 | cy.get('.new-todo') 58 | .type('todo A{enter}') 59 | .type('todo B{enter}') 60 | .type('todo C{enter}') 61 | .type('todo D{enter}') 62 | cy.get('.todo label').should(($labels) => { 63 | // confirm that there are 4 labels 64 | // and that each one starts with "todo-" 65 | }) 66 | }) 67 | 68 | it('has the right label', () => { 69 | cy.get('.new-todo').type('todo A{enter}') 70 | // get the li elements 71 | // find the label with the text 72 | // which should contain the text "todo A" 73 | }) 74 | 75 | // flaky test - can pass or not depending on the app's speed 76 | // to make the test flaky add the timeout 77 | // in todomvc/app.js "addTodo({ commit, state })" method 78 | it('has two labels', () => { 79 | cy.get('.new-todo').type('todo A{enter}') 80 | cy.get('.todo-list li') // command 81 | .find('label') // command 82 | .should('contain', 'todo A') // assertion 83 | 84 | cy.get('.new-todo').type('todo B{enter}') 85 | // ? copy the same check as above 86 | // then make the test flaky ... 87 | }) 88 | 89 | it('solution 1: remove cy.then', () => { 90 | cy.get('.new-todo').type('todo A{enter}') 91 | // ? 92 | 93 | cy.get('.new-todo').type('todo B{enter}') 94 | // ? 95 | }) 96 | 97 | it('solution 2: alternate commands and assertions', () => { 98 | cy.get('.new-todo').type('todo A{enter}') 99 | // ? 100 | 101 | cy.get('.new-todo').type('todo B{enter}') 102 | // ? 103 | }) 104 | 105 | it('solution 3: replace cy.then with a query', () => { 106 | Cypress.Commands.addQuery('later', (fn) => { 107 | return (subject) => { 108 | fn(subject) 109 | return subject 110 | } 111 | }) 112 | cy.get('.new-todo').type('todo A{enter}') 113 | // ? 114 | 115 | cy.get('.new-todo').type('todo B{enter}') 116 | // ? 117 | }) 118 | 119 | it('confirms the text of each todo', () => { 120 | // use cypress-map queries to get the text from 121 | // each todo and confirm the list of strings 122 | // add printing the strings before the assertion 123 | // Tip: there are queries for this in cypress-map 124 | }) 125 | 126 | it('retries reading the JSON file', () => { 127 | // add N items via UI 128 | // then read the file ./todomvc/data.json 129 | // and assert it has the N items and the first item 130 | // is the one entered first 131 | // note cy.readFile retries reading the file until the should(cb) passes 132 | // https://on.cypress.io/readilfe 133 | }) 134 | }) 135 | 136 | describe('cypress-recurse', () => { 137 | beforeEach(function resetData() { 138 | cy.request('POST', '/reset', { 139 | todos: [] 140 | }) 141 | cy.visit('/') 142 | }) 143 | 144 | it('adds todos until we have 5 of them', () => { 145 | // use cypress-recurse to add an item 146 | // if there are fewer than 5 147 | }) 148 | }) 149 | 150 | // if the tests are flaky, add test retries 151 | // https://on.cypress.io/test-retries 152 | describe('Careful with negative assertions', () => { 153 | it('hides the loading element', () => { 154 | cy.visit('/') 155 | // the loading element should not be visible 156 | }) 157 | 158 | it('uses negative assertion and passes for the wrong reason', () => { 159 | cy.visit('/?delay=3000') 160 | // the loading element should not be visible 161 | }) 162 | 163 | it('slows down the network response', () => { 164 | // use cy.intercept to delay the mock response 165 | cy.visit('/?delay=1000') 166 | 167 | // first, make sure the loading indicator shows up (positive assertion) 168 | // then assert it goes away (negative assertion) 169 | }) 170 | }) 171 | 172 | describe('should vs then', () => { 173 | it('retries should(cb) but does not return value', () => { 174 | cy.wrap(42).should((x) => { 175 | // the return here does nothing 176 | // the original subject 42 is yielded instead 177 | }) 178 | // assert the value is 42 179 | }) 180 | 181 | it('first use should(cb) then then(cb) to change the value', () => { 182 | cy.wrap(42) 183 | .should((x) => { 184 | // the returned value is ignored 185 | }) 186 | .then((x) => { 187 | // check the current value 188 | return 10 189 | }) 190 | // assert the value is 10 191 | .should('equal', 10) 192 | }) 193 | }) 194 | 195 | describe('timing commands', () => { 196 | // reset data before each test 197 | 198 | // see solution in the video 199 | // "Time Part Of A Cypress Test Or A Single Command" 200 | // https://youtu.be/tjK_FCYikzI 201 | it('takes less than 2 seconds for the app to load', () => { 202 | // intercept the GET /todos load and randomly delay the response 203 | cy.visit('/') 204 | 205 | // check the loading indicator is visible 206 | // take a timestamp after the loading indicator is visible 207 | // how to check if the loading element goes away in less than 2 seconds? 208 | // take another timestamp when the indicator goes away. 209 | // compute the elapsed time 210 | // assert the elapsed time is less than 2 seconds 211 | }) 212 | }) 213 | 214 | // TODO: finish this exercise 215 | describe.skip('delayed app start', () => { 216 | it('waits for the event listeners to be attached', () => { 217 | cy.visit('/?appStartDelay=2000') 218 | const selector = 'input.new-todo' 219 | 220 | function checkListeners() { 221 | cy.CDP('Runtime.evaluate', { 222 | expression: 'frames[0].document.querySelector("' + selector + '")' 223 | }) 224 | .should((v) => { 225 | expect(v.result).to.have.property('objectId') 226 | }) 227 | .its('result.objectId') 228 | .then(cy.log) 229 | .then((objectId) => { 230 | cy.CDP('DOMDebugger.getEventListeners', { 231 | objectId, 232 | depth: -1, 233 | pierce: true 234 | }).then((v) => { 235 | if (v.listeners && v.listeners.length > 0) { 236 | // all good 237 | return 238 | } 239 | cy.wait(100).then(checkListeners) 240 | }) 241 | }) 242 | } 243 | 244 | checkListeners() 245 | // cy.hasEventListeners('input.new-todo') 246 | }) 247 | }) 248 | -------------------------------------------------------------------------------- /cypress/e2e/09-custom-commands/answer.js: -------------------------------------------------------------------------------- 1 | // /// 2 | // /// 3 | // require('cypress-pipe') 4 | // import { resetData, visitSite } from '../../support/utils' 5 | 6 | // beforeEach(resetData) 7 | // beforeEach(visitSite) 8 | 9 | // it('enters 10 todos', function () { 10 | // cy.get('.new-todo') 11 | // .type('todo 0{enter}') 12 | // .type('todo 1{enter}') 13 | // .type('todo 2{enter}') 14 | // .type('todo 3{enter}') 15 | // .type('todo 4{enter}') 16 | // .type('todo 5{enter}') 17 | // .type('todo 6{enter}') 18 | // .type('todo 7{enter}') 19 | // .type('todo 8{enter}') 20 | // .type('todo 9{enter}') 21 | // cy.get('.todo').should('have.length', 10) 22 | // }) 23 | 24 | // // simple custom command 25 | // Cypress.Commands.add('createTodo', (todo) => { 26 | // cy.get('.new-todo').type(`${todo}{enter}`) 27 | // }) 28 | 29 | // // with better command log 30 | // Cypress.Commands.add('createTodo', (todo) => { 31 | // cy.get('.new-todo', { log: false }).type(`${todo}{enter}`, { log: false }) 32 | // cy.log('createTodo', todo) 33 | // }) 34 | 35 | // // with full command log 36 | // Cypress.Commands.add('createTodo', (todo) => { 37 | // const cmd = Cypress.log({ 38 | // name: 'create todo', 39 | // message: todo, 40 | // consoleProps() { 41 | // return { 42 | // 'Create Todo': todo 43 | // } 44 | // } 45 | // }) 46 | 47 | // cy.get('.new-todo', { log: false }) 48 | // .type(`${todo}{enter}`, { log: false }) 49 | // .then(($el) => { 50 | // cmd.set({ $el }).snapshot().end() 51 | // }) 52 | // }) 53 | 54 | // it('creates a todo', () => { 55 | // cy.createTodo('my first todo') 56 | // }) 57 | 58 | // it('passes when object gets new property', () => { 59 | // const o = {} 60 | // setTimeout(() => { 61 | // o.foo = 'bar' 62 | // }, 1000) 63 | // const get = (name) => 64 | // function getProp(from) { 65 | // console.log('getting', from) 66 | // return from[name] 67 | // } 68 | 69 | // cy.wrap(o).pipe(get('foo')).should('not.be.undefined').and('equal', 'bar') 70 | // }) 71 | -------------------------------------------------------------------------------- /cypress/e2e/09-custom-commands/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | // beforeEach(function resetData() { 4 | // cy.request('POST', '/reset', { 5 | // todos: [] 6 | // }) 7 | // }) 8 | // beforeEach(function visitSite() { 9 | // cy.visit('/') 10 | // }) 11 | 12 | // it('enters 10 todos', function () { 13 | // cy.get('.new-todo') 14 | // .type('todo 0{enter}') 15 | // .type('todo 1{enter}') 16 | // .type('todo 2{enter}') 17 | // .type('todo 3{enter}') 18 | // .type('todo 4{enter}') 19 | // .type('todo 5{enter}') 20 | // .type('todo 6{enter}') 21 | // .type('todo 7{enter}') 22 | // .type('todo 8{enter}') 23 | // .type('todo 9{enter}') 24 | // cy.get('.todo').should('have.length', 10) 25 | // }) 26 | 27 | // // it('creates a todo') 28 | 29 | // it.skip('passes when object gets new property', () => { 30 | // const o = {} 31 | // setTimeout(() => { 32 | // o.foo = 'bar' 33 | // }, 1000) 34 | // // TODO write "get" that returns the given property 35 | // // from an object. 36 | // // cy.wrap(o).pipe(get('foo')) 37 | // // add assertions 38 | // }) 39 | 40 | // it('creates todos', () => { 41 | // cy.get('.new-todo') 42 | // .type('todo 0{enter}') 43 | // .type('todo 1{enter}') 44 | // .type('todo 2{enter}') 45 | // cy.get('.todo').should('have.length', 3) 46 | // cy.window().its('app.todos').toMatchSnapshot() 47 | // }) 48 | -------------------------------------------------------------------------------- /cypress/fixtures/empty-list.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /cypress/fixtures/three-items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "first item from fixture", 4 | "completed": false, 5 | "id": 1 6 | }, 7 | { 8 | "title": "second item from fixture", 9 | "completed": true, 10 | "id": 2 11 | }, 12 | { 13 | "title": "third item", 14 | "completed": false, 15 | "id": 3 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /cypress/fixtures/two-items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "first item from fixture", 4 | "completed": false, 5 | "id": 1 6 | }, 7 | { 8 | "title": "second item from fixture", 9 | "completed": true, 10 | "id": 2 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /cypress/slides-tests/slides.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import 'cypress-real-events/support' 5 | 6 | // https://j1000.github.io/blog/2022/10/27/enhanced_cypress_logging.html 7 | Cypress.Commands.add('endGroup', () => { 8 | collapseLastGroup() 9 | Cypress.log({ groupEnd: true, emitOnly: true }) 10 | }) 11 | 12 | function collapseLastGroup() { 13 | const openExpanders = window.top.document.getElementsByClassName( 14 | 'command-expander-is-open' 15 | ) 16 | const numExpanders = openExpanders.length 17 | const el = openExpanders[numExpanders - 1] 18 | 19 | if (el) { 20 | el.parentElement.click() 21 | } 22 | } 23 | 24 | Cypress.Commands.add('checkSlide', (column, row) => { 25 | Cypress.log({ 26 | name: 'checkSlide', 27 | message: `${column}, ${row}`, 28 | groupStart: true 29 | }) 30 | if (row > 1) { 31 | cy.location('hash').should('equal', `#/${column}/${row}`) 32 | } 33 | cy.contains('.slide-number-a', String(column)).should('be.visible') 34 | cy.contains('.slide-number-b', String(row)).should('be.visible') 35 | return cy.endGroup() 36 | }) 37 | 38 | describe('Workshop slides', () => { 39 | const checkSlide = (column, row) => { 40 | Cypress.log({ 41 | name: 'checkSlide', 42 | message: `${column}, ${row}`, 43 | groupStart: true 44 | }) 45 | cy.contains('.slide-number-a', String(column)).should('be.visible') 46 | cy.contains('.slide-number-b', String(row)).should('be.visible') 47 | return cy.endGroup() 48 | } 49 | 50 | it('loads', () => { 51 | cy.visit('/') 52 | const title = 'Cypress Workshop: Basics' 53 | cy.title().should('equal', title) 54 | cy.contains('h1', title).should('be.visible') 55 | 56 | cy.log('**speaker slide**') 57 | cy.get('[aria-label="below slide"]').should('be.visible').click() 58 | cy.contains('h2', 'Gleb Bahmutov').should('be.visible') 59 | cy.hash().should('equal', '#/1/2') 60 | 61 | cy.get('.progress').should('be.visible') 62 | }) 63 | 64 | it('navigates using the keyboard', () => { 65 | cy.log('**very first column with 3 slides**') 66 | const noLog = { log: false } 67 | cy.visit('/') 68 | cy.get('h1').should('be.visible') 69 | 70 | cy.checkSlide(1, 1) 71 | cy.get('.navigate-down') 72 | .should('have.class', 'enabled') 73 | .and('be.visible') 74 | .wait(1000, noLog) 75 | 76 | // focus on the app 77 | cy.get('h1').realClick().realPress('ArrowDown').wait(500, noLog) 78 | cy.checkSlide(1, 2) 79 | cy.get('h1').realClick().realPress('ArrowDown').wait(500, noLog) 80 | cy.checkSlide(1, 3) 81 | 82 | cy.log('**no more slides down**') 83 | cy.get('.navigate-down').should('not.be.visible').wait(1000, noLog) 84 | cy.log('**go back up**') 85 | cy.realPress('ArrowUp') 86 | cy.checkSlide(1, 2).wait(1000, noLog) 87 | 88 | cy.log('**go to the next slide column**') 89 | cy.realPress('ArrowRight') 90 | cy.checkSlide(2, 1).wait(1000, noLog) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /cypress/slides-tests/utils.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { updateRelativeUrls } from '../../slides-utils' 4 | 5 | describe('Slides utils', () => { 6 | const baseUrl = '/slides/' 7 | 8 | it('changes relative image links', () => { 9 | const md = '![JSDoc example](./img/jsdoc.png)' 10 | 11 | const changed = updateRelativeUrls(baseUrl, md) 12 | 13 | if (changed !== '![JSDoc example](/slides/img/jsdoc.png)') { 14 | throw new Error(`Expected changed to be ${md}, got ${changed}`) 15 | } 16 | }) 17 | 18 | it('does not change other dots', () => { 19 | const md = "import '../../support/hooks'" 20 | const changed = updateRelativeUrls(baseUrl, md) 21 | expect(changed, 'should not change').to.equal(md) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /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 is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/e2e.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 | -------------------------------------------------------------------------------- /cypress/support/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets the database to the empty list of todos 3 | */ 4 | export const resetDatabase = () => { 5 | console.log('resetDatabase') 6 | cy.request({ 7 | method: 'POST', 8 | url: '/reset', 9 | body: { 10 | todos: [] 11 | } 12 | }) 13 | } 14 | 15 | /** 16 | * 17 | * @param {string} fixtureName The fixture with todos to load and send to the server 18 | */ 19 | export const resetDatabaseTo = (fixtureName) => { 20 | cy.log(`**resetDatabaseTo** ${fixtureName}`) 21 | cy.fixture(fixtureName).then((todos) => { 22 | cy.request({ 23 | method: 'POST', 24 | url: '/reset', 25 | body: { 26 | todos 27 | } 28 | }) 29 | }) 30 | } 31 | 32 | export const visit = (skipWaiting) => { 33 | console.log('visit this =', this) 34 | 35 | if (typeof skipWaiting !== 'boolean') { 36 | skipWaiting = false 37 | } 38 | 39 | const waitForInitialLoad = !skipWaiting 40 | console.log('visit will wait for initial todos', waitForInitialLoad) 41 | if (waitForInitialLoad) { 42 | cy.intercept('/todos').as('initialTodos') 43 | } 44 | cy.visit('/') 45 | console.log('cy.visit /') 46 | if (waitForInitialLoad) { 47 | console.log('waiting for initial todos') 48 | cy.wait('@initialTodos') 49 | } 50 | } 51 | 52 | export const getTodoApp = () => cy.get('.todoapp') 53 | 54 | export const getTodoItems = () => getTodoApp().find('.todo-list').find('li') 55 | 56 | export const newId = () => Math.random().toString().substr(2, 10) 57 | 58 | // if we expose "newId" factory method from the application 59 | // we can easily stub it. But this is a realistic example of 60 | // stubbing "test window" random number generator 61 | // and "application window" random number generator that is 62 | // running inside the test iframe 63 | export const stubMathRandom = () => { 64 | // first two digits are disregarded, so our "random" sequence of ids 65 | // should be '1', '2', '3', ... 66 | let counter = 101 67 | cy.stub(Math, 'random').callsFake(() => counter++) 68 | cy.window().then((win) => { 69 | // inside test iframe 70 | cy.stub(win.Math, 'random').callsFake(() => counter++) 71 | }) 72 | } 73 | 74 | export const makeTodo = (text = 'todo') => { 75 | const id = newId() 76 | const title = `${text} ${id}` 77 | return { 78 | id, 79 | title, 80 | completed: false 81 | } 82 | } 83 | 84 | export const getNewTodoInput = () => getTodoApp().find('.new-todo') 85 | 86 | /** 87 | * Adds new todo to the app. 88 | * 89 | * @param text {string} Text to enter 90 | * @example 91 | * enterTodo('my todo') 92 | */ 93 | export const enterTodo = (text = 'example todo') => { 94 | getNewTodoInput().type(`${text}{enter}`) 95 | 96 | // we need to make sure the store and the vue component 97 | // get updated and the DOM is updated. 98 | // quick check - the new text appears at the last position 99 | // I am going to use combined selector to always grab 100 | // the element and not use stale reference from previous chain call 101 | const lastItem = '.todoapp .todo-list li:last' 102 | cy.get(lastItem).should('contain', text) 103 | } 104 | 105 | /** 106 | * Removes the given todo by text 107 | * @param {string} text The todo to find and remove 108 | */ 109 | export const removeTodo = (text) => { 110 | cy.contains('.todoapp .todo-list li', text) 111 | .find('.destroy') 112 | .click({ force: true }) 113 | } 114 | 115 | // a couple of aliases for 09-custom-commands answers 116 | export const resetData = resetDatabase 117 | export const visitSite = () => visit(true) 118 | -------------------------------------------------------------------------------- /img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/img/app.png -------------------------------------------------------------------------------- /img/cypress-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/img/cypress-desktop.png -------------------------------------------------------------------------------- /img/fails-to-find-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/img/fails-to-find-text.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cypress Workshop: Basics 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import 'reveal.js/dist/reset.css' 2 | import 'reveal.js/dist/reveal.css' 3 | import 'reveal.js/dist/theme/league.css' 4 | 5 | // pick code block syntax highlighting theme 6 | // included with Reveal.js are monokai and zenburn 7 | // import 'reveal.js/plugin/highlight/monokai.css' 8 | // more themes from Highlight.js 9 | // import 'highlight.js/styles/purebasic.css' 10 | import 'highlight.js/styles/docco.css' 11 | 12 | import Reveal from 'reveal.js' 13 | import Markdown from 'reveal.js/plugin/markdown/markdown.esm.js' 14 | import RevealHighlight from 'reveal.js/plugin/highlight/highlight.esm.js' 15 | 16 | import { updateRelativeUrls } from './slides-utils' 17 | 18 | // something like 19 | // {BASE_URL: "/reveal-markdown-example/", MODE: "development", DEV: true, PROD: false, SSR: false} 20 | // https://vitejs.dev/guide/env-and-mode.html 21 | console.log('env variables', import.meta.env) 22 | const { BASE_URL, PROD } = import.meta.env 23 | if (typeof BASE_URL !== 'string') { 24 | throw new Error('Missing BASE_URL in import.meta.env') 25 | } 26 | 27 | const getBaseName = (relativeUrl) => { 28 | if (!relativeUrl.startsWith('./')) { 29 | throw new Error(`Is not relative url "${relativeUrl}"`) 30 | } 31 | const parts = relativeUrl.split('/').filter((s) => s !== '.') // remove "." 32 | parts.pop() // ignore the file part 33 | return parts.join('/') 34 | } 35 | 36 | // fetch the Markdown file ourselves 37 | 38 | // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams 39 | const url = new URL(document.URL) 40 | const slidesFolder = url.searchParams.get('p') || 'intro' 41 | const toLoad = slidesFolder + '/PITCHME.md' 42 | const markdownFilename = './' + (PROD ? toLoad : 'slides/' + toLoad) 43 | 44 | const markdownFileBase = getBaseName(markdownFilename) 45 | console.log('markdown file base', markdownFileBase) 46 | const baseUrl = BASE_URL + markdownFileBase + '/' 47 | console.log('baseUrl', baseUrl) 48 | 49 | fetch(markdownFilename) 50 | .then((r) => r.text()) 51 | .then((md) => { 52 | const updatedUrlsMd = updateRelativeUrls(baseUrl, md) 53 | 54 | document.querySelector('.slides').innerHTML = 55 | '
\n' + 56 | '\n' + 60 | '
\n' 61 | 62 | const deck = new Reveal({ 63 | plugins: [Markdown, RevealHighlight] 64 | }) 65 | deck 66 | .initialize({ 67 | // presentation sizing config 68 | width: 1280, 69 | height: 720, 70 | minScale: 0.2, 71 | maxScale: 1.1, 72 | // show the slide number on the page 73 | // and in the hash fragment and 74 | // make sure they are the same 75 | slideNumber: true, 76 | hash: true, 77 | hashOneBasedIndex: true 78 | }) 79 | .then(() => { 80 | if (window.Cypress) { 81 | // expose the Reveal object to Cypress tests 82 | // to allow waiting for it to be ready 83 | window.Reveal = Reveal 84 | } 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-workshop-basics", 3 | "version": "1.0.0", 4 | "description": "Basics of end-to-end testing with Cypress.io test runner", 5 | "scripts": { 6 | "cy:open": "cypress open", 7 | "cy:run": "cypress run", 8 | "cy:answers": "cypress run --config 'specPattern=cypress/e2e/*/answer*.js'", 9 | "cy:answers:open": "cypress open --config 'specPattern=cypress/e2e/*/answer*.js'", 10 | "start": "npm start --prefix todomvc -- --quiet", 11 | "test": "cypress run --config-file cypress.ci-config.js", 12 | "ci": "start-test http://localhost:3000", 13 | "dev": "start-test http://localhost:3000 cy:open", 14 | "dev:answers": "start-test http://localhost:3000 cy:answers:open", 15 | "postinstall": "npm install --prefix todomvc", 16 | "reset": "npm run reset --prefix todomvc", 17 | "slides": "vite --strictPort --port 3100", 18 | "slides:dev": "start-test slides http://localhost:3100 cy:slides", 19 | "slides:build": "vite build", 20 | "cy:slides": "cypress open --config-file cypress.slides-config.js", 21 | "cy:slides:run": "cypress run --config-file cypress.slides-config.js", 22 | "dev:ci": "start-test 3000", 23 | "names": "find-cypress-specs --names" 24 | }, 25 | "private": true, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/bahmutov/cypress-workshop-basics.git" 29 | }, 30 | "keywords": [ 31 | "cypress", 32 | "cypress-io", 33 | "e2e", 34 | "end-to-end", 35 | "testing", 36 | "workshop" 37 | ], 38 | "author": "Gleb Bahmutov ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/bahmutov/cypress-workshop-basics/issues" 42 | }, 43 | "homepage": "https://github.com/bahmutov/cypress-workshop-basics#readme", 44 | "devDependencies": { 45 | "cy-spok": "1.6.2", 46 | "cypress": "14.4.1", 47 | "cypress-cdp": "1.6.75", 48 | "cypress-map": "1.48.1", 49 | "cypress-real-events": "1.14.0", 50 | "cypress-recurse": "1.35.3", 51 | "find-cypress-specs": "1.54.1", 52 | "highlight.js": "11.11.1", 53 | "prettier": "3.5.3", 54 | "reveal.js": "5.2.1", 55 | "start-server-and-test": "2.0.12", 56 | "vite": "6.3.5" 57 | }, 58 | "engines": { 59 | "node": ">=12" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "updateNotScheduled": false, 7 | "timezone": "America/New_York", 8 | "schedule": [ 9 | "every weekend" 10 | ], 11 | "masterIssue": true 12 | } 13 | -------------------------------------------------------------------------------- /slides-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The image urls pointing at local relative files 3 | * should be updated to be served from the full base URL. 4 | * 5 | * @param {String} baseUrl The URL of the server 6 | * @param {String} md The markdown text 7 | */ 8 | export const updateRelativeUrls = (baseUrl, md) => { 9 | // only update the relative link urls 10 | // in the form [...](./some/path/to/file.jpg) 11 | return md.replace(/]\(\.\//g, '](' + baseUrl) 12 | } 13 | -------------------------------------------------------------------------------- /slides/00-start/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Starting new projects 2 | 3 | ### 📚 You will learn 4 | 5 | - Cypress folder structure 6 | - Writing the first test 7 | - Setting up intelligent code completion 8 | - Cypress documentation 9 | 10 | --- 11 | 12 | ## Quick check: Node.js 13 | 14 | ```bash 15 | $ node -v 16 | v18.14.2 17 | $ npm -v 18 | 9.5.0 19 | # optional: 20 | $ yarn -v 21 | 1.22.19 22 | ``` 23 | 24 | If you need to install Node, see [Basics Requirements](https://github.com/bahmutov/cypress-workshop-basics#requirements) and 📹 [Install Node and Cypress](https://www.youtube.com/watch?v=09KbTRLrgWA) 25 | 26 | --- 27 | 28 | ## Todo: make a new project and add Cypress 29 | 30 | Create a new folder 31 | 32 | - `cd /tmp` 33 | - `mkdir example` 34 | - `cd example` 35 | - `npm init --yes` 36 | - `npm install -D cypress` 37 | 38 | +++ 39 | 40 | ### Cypress bin 41 | 42 | When you run `npm install cypress` it creates a "cypress" alias in the `node_modules/.bin" folder. You can see all tools that install aliases (depending on the platform) 43 | 44 | ```text 45 | $ ls node_modules/.bin 46 | cypress nanoid rollup sshpk-verify vite 47 | esbuild prettier server-test start-server-and-test wait-on 48 | extract-zip ps-tree sshpk-conv start-test 49 | is-ci rimraf sshpk-sign uuid 50 | ``` 51 | 52 | Let's run Cypress alias 53 | 54 | +++ 55 | 56 | ### How to open Cypress 57 | 58 | ```shell 59 | npx cypress open 60 | # or 61 | yarn cypress open 62 | # or 63 | $(npm bin)/cypress open 64 | # or 65 | ./node_modules/.bin/cypress open 66 | ``` 67 | 68 | +++ 69 | 70 | ## 💡 Pro tip 71 | 72 | In `package.json` I usually have 73 | 74 | ```json 75 | { 76 | "scripts": { 77 | "cy:open": "cypress open", 78 | "cy:run": "cypress run" 79 | } 80 | } 81 | ``` 82 | 83 | And I use `npm run cy:open` 84 | 85 | **Tip:** read [https://glebbahmutov.com/blog/organize-npm-scripts/](https://glebbahmutov.com/blog/organize-npm-scripts/) 86 | 87 | --- 88 | 89 | ![First time you open Cypress](./img/start1.png) 90 | 91 | +++ 92 | 93 | ![Scaffold E2E tests](./img/start2.png) 94 | 95 | +++ 96 | 97 | ![Created E2E configuration files](./img/start3.png) 98 | 99 | +++ 100 | 101 | ![Scaffold example specs](./img/start4.png) 102 | 103 | --- 104 | 105 | ## Cypress files and folders 106 | 107 | - "cypress.config.js" - all Cypress settings 108 | - "cypress/e2e" - end-to-end test files (specs) 109 | - "cypress/fixtures" - mock data 110 | - "cypress/support" - shared commands, utilities 111 | 112 | Read blog post [Cypress is just ...](https://glebbahmutov.com/blog/cypress-is/) 113 | 114 | Note: 115 | This section shows how Cypress scaffolds its files and folders. Then the students can ignore this folder. This is only done once to show the scaffolding. 116 | 117 | --- 118 | 119 | Look at the scaffolded example test files (specs). 120 | 121 | Run specs for topics that look interesting 122 | 123 | --- 124 | 125 | ## Configuration 126 | 127 | ```js 128 | // cypress.config.js 129 | // https://on.cypress.io/configuration 130 | module.exports = defineConfig({ 131 | // common settings 132 | viewportWidth: 800, 133 | viewportHeight: 1000, 134 | e2e: { 135 | // end-to-end settings 136 | baseUrl: 'http://localhost:3000' 137 | }, 138 | component: { 139 | // component testing settings 140 | devServer: { 141 | framework: 'create-react-app', 142 | bundler: 'webpack' 143 | } 144 | } 145 | }) 146 | ``` 147 | 148 | --- 149 | 150 | ## 💡 Pro tip 151 | 152 | ```shell 153 | # quickly scaffolds Cypress folders 154 | $ npx @bahmutov/cly init 155 | # bare scaffold 156 | $ npx @bahmutov/cly init -b 157 | # typescript scaffold 158 | $ npx @bahmutov/cly init --typescript 159 | ``` 160 | 161 | Repo [github.com/bahmutov/cly](https://github.com/bahmutov/cly) 162 | 163 | --- 164 | 165 | ## [glebbahmutov.com/cypress-examples](https://glebbahmutov.com/cypress-examples/) 166 | 167 | ![Cypress examples site](./img/cypress-examples.png) 168 | 169 | --- 170 | 171 | ## First spec 172 | 173 | Let's test our TodoMVC application. Create a new spec file 174 | 175 | - `cypress/e2e/spec.cy.js` 176 | 177 | **tip:** the default spec pattern is `cypress/e2e/**/*.cy.{js,jsx,ts,tsx}` 178 | 179 | +++ 180 | 181 | Type into the `spec.cy.js` our first test 182 | 183 | ```javascript 184 | it('loads', () => { 185 | cy.visit('localhost:3000') 186 | }) 187 | ``` 188 | 189 | +++ 190 | 191 | - make sure you have started TodoMVC in another terminal with `npm start` 192 | - click on "spec.cy.js" in Cypress GUI 193 | 194 | +++ 195 | 196 | ## Questions 197 | 198 | - what does Cypress do? 199 | - what happens when the server is down? 200 | - stop the application server running in folder `todomvc` 201 | - reload the tests 202 | 203 | --- 204 | 205 | ![Switch browser](./img/switch-browser.png) 206 | 207 | --- 208 | 209 | Add a special `/// ...` comment 210 | 211 | ```javascript 212 | /// 213 | it('loads', () => { 214 | cy.visit('localhost:3000') 215 | }) 216 | ``` 217 | 218 | - why do we need `reference types ...` line? 219 | 220 | Note: 221 | By having "reference" line we tell editors that support it (VSCode, WebStorm) to use TypeScript definitions included in Cypress to provide intelligent code completion. Hovering over any `cy` command brings helpful tooltips. 222 | 223 | +++ 224 | 225 | ## IntelliSense 226 | 227 | ![IntelliSense in VSCode](./img/cy-get-intellisense.jpeg) 228 | 229 | +++ 230 | 231 | Every Cypress command and every assertion 232 | 233 | ![Should IntelliSense](./img/should-intellisense.jpeg) 234 | 235 | +++ 236 | 237 | Using `ts-check` 238 | 239 | ```javascript 240 | /// 241 | // @ts-check 242 | it('loads', () => { 243 | cy.visit('localhost:3000') 244 | }) 245 | ``` 246 | 247 | - what happens if you add `ts-check` line and misspell `cy.visit`? 248 | 249 | Note: 250 | The check works really well in VSCode editor. I am not sure how well other editors support Cypress type checks right out of the box. 251 | 252 | --- 253 | 254 | ## Docs 255 | 256 | Your best friend is [https://docs.cypress.io/](https://docs.cypress.io/) search 257 | 258 | ![Doc search](./img/docs-search.png) 259 | 260 | +++ 261 | 262 | ## TODO: Find at docs.cypress.io 263 | 264 | - Cypress main features and how it works docs 265 | - core concepts 266 | - command API 267 | - how many commands are there? 268 | - frequently asked questions 269 | 270 | +++ 271 | 272 | ## 💡 Pro tip 273 | 274 | ```text 275 | https://on.cypress.io/ 276 | ``` 277 | 278 | The above URL goes right to the documentation for that command. 279 | 280 | +++ 281 | 282 | ## Todo: find at docs.cypress.io 283 | 284 | - documentation for `click`, `type`, and `contains` commands 285 | - assertions examples 286 | 287 | --- 288 | 289 | ## Todo: Find at docs.cypress.io 290 | 291 | - examples 292 | - recipes 293 | - tutorial videos 294 | - example applications 295 | - blogs 296 | - FAQ 297 | - Cypress changelog and roadmap 298 | 299 | Note: 300 | Students should know where to find information later on. Main resources is the api page [https://on.cypress.io/api](https://on.cypress.io/api) 301 | 302 | --- 303 | 304 | ## 🏆 [cypress.tips/search](https://cypress.tips/search) 305 | 306 | ![Cypress tips search](./img/cypress-tips-search.png) 307 | 308 | +++ 309 | 310 | ## Todo: find using cypress.tips/search 311 | 312 | - Cypress assertion examples 313 | - URL and location examples 314 | - Cypress tips bog posts 315 | - Videos on making Cypress tests run faster 316 | 317 | --- 318 | 319 | ## 🏁 Conclusions 320 | 321 | - set up IntelliSense 322 | - use Docs at [https://docs.cypress.io/](https://docs.cypress.io/) 323 | - use my [cypress.tips/search](https://cypress.tips/search) 324 | 325 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [01-basic](?p=01-basic) chapter 326 | -------------------------------------------------------------------------------- /slides/00-start/img/cy-get-intellisense.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/cy-get-intellisense.jpeg -------------------------------------------------------------------------------- /slides/00-start/img/cypress-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/cypress-examples.png -------------------------------------------------------------------------------- /slides/00-start/img/cypress-scaffold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/cypress-scaffold.png -------------------------------------------------------------------------------- /slides/00-start/img/cypress-tips-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/cypress-tips-search.png -------------------------------------------------------------------------------- /slides/00-start/img/docs-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/docs-search.png -------------------------------------------------------------------------------- /slides/00-start/img/should-intellisense.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/should-intellisense.jpeg -------------------------------------------------------------------------------- /slides/00-start/img/start1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/start1.png -------------------------------------------------------------------------------- /slides/00-start/img/start2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/start2.png -------------------------------------------------------------------------------- /slides/00-start/img/start3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/start3.png -------------------------------------------------------------------------------- /slides/00-start/img/start4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/start4.png -------------------------------------------------------------------------------- /slides/00-start/img/switch-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/switch-browser.png -------------------------------------------------------------------------------- /slides/00-start/img/vscode-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/00-start/img/vscode-icons.png -------------------------------------------------------------------------------- /slides/01-basic/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## The very basic tests 2 | 3 | ### 📚 You will learn 4 | 5 | - `cy.contains` and command retries 6 | - two ways to run Cypress 7 | - screenshots and video recording 8 | 9 | --- 10 | 11 | - keep `todomvc` app running 12 | - open Cypress from the root folder with `npm run cy:open` 13 | - click on `01-basic/spec.js` (we are using custom `specPattern`) 14 | 15 | ```js 16 | /// 17 | it('loads', () => { 18 | cy.visit('localhost:3000') 19 | cy.contains('h1', 'Todos App') 20 | }) 21 | ``` 22 | 23 | +++ 24 | 25 | `cy.contains('h1', 'Todos App')` is not working 😟 26 | 27 | Note: 28 | This is a good moment to show how Cypress stores DOM snapshots and shows them for each step. 29 | 30 | +++ 31 | 32 | ## Questions 1/2 33 | 34 | - where are the docs for `cy.contains` command? 35 | - why is the command failing? 36 | - **hint**: use DevTools 37 | - can you fix this? 38 | 39 | +++ 40 | 41 | ## Questions 2/2 42 | 43 | - do you see the command retrying (blue spinner)? 44 | - try using the timeout option to force the command to try for longer 45 | 46 | --- 47 | 48 | ## Cypress has 2 commands 49 | 50 | - `cypress open` 51 | - `cypress run` 52 | 53 | See [https://on.cypress.io/command-line](https://on.cypress.io/command-line) 54 | 55 | **💡 Hint:** `npx cypress help` 56 | 57 | +++ 58 | 59 | ## Q: How do you: 60 | 61 | - run just the spec `cypress/integration/01-basic/spec.js` in headless mode? 62 | 63 | **💡 Hint:** `npx cypress run --help` 64 | 65 | +++ 66 | 67 | ## Bonus 68 | 69 | **Todo:** use `cypress run` with a failing test. 70 | 71 | - video recording [https://on.cypress.io/configuration#Videos](https://on.cypress.io/configuration#Videos) 72 | - `cy.screenshot` command 73 | 74 | --- 75 | 76 | ## Fix the test 77 | 78 | - can you fix the test? 79 | - how would you select an element: 80 | - by text 81 | - by id 82 | - by class 83 | - by attributes 84 | 85 | **Tip:** https://on.cypress.io/best-practices#Selecting-Elements 86 | 87 | **📝 Read:** https://glebbahmutov.com/blog/debug-cy-get-and-contains/ 88 | 89 | --- 90 | 91 | ## 🏁 Conclusions 92 | 93 | - most commands retry 94 | - run Cypress in headless mode on CI with `cypress run` 95 | - screenshots and videos 96 | 97 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [02-adding-items](?p=02-adding-items) chapter 98 | -------------------------------------------------------------------------------- /slides/02-adding-items/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Part 2: Adding items tests 2 | 3 | ### 📚 You will learn 4 | 5 | - the common commands for working with page elements 6 | - organizing the test code using Mocha hooks 7 | 8 | --- 9 | 10 | ## What kind of tests? 11 | 12 | - discussion 🗣️: what would you test in the TodoMVC app? 13 | 14 | Note: 15 | Longer tests, adding items then deleting one for example. Adding items via GUI and observing communication with the server. Adding items then reloading the page. 16 | 17 | --- 18 | 19 | ## Let's test 20 | 21 | - keep `todomvc` app running 22 | - open `cypress/e2e/02-adding-items/spec.js` in your text editor 23 | - click file `02-adding-items/spec.js` in Cypress 24 | 25 | +++ 26 | 27 | ## ⚠️ Warning ⚠️ 28 | 29 | The tests we are about to write are NOT resetting the previously added Todo items. Delete the Todo items before each test manually. 30 | 31 | We will reset the previously saved Todo items in section "4 Reset State". 32 | 33 | ## ⚠️ Warning ⚠️ 34 | 35 | --- 36 | 37 | ## Todo: Make this test work 38 | 39 | ```js 40 | // cypress/e2e/02-adding-items/spec.js 41 | it('adds two items', () => { 42 | // visit the site 43 | // https://on.cypress.io/visit 44 | // repeat twice 45 | // get the input field 46 | // https://on.cypress.io/get 47 | // type text and "enter" 48 | // https://on.cypress.io/type 49 | // assert that the new Todo item 50 | // has been added added to the list 51 | // cy.get(...).should('have.length', 2) 52 | }) 53 | ``` 54 | 55 | **💡 tip** use `cy.get`, `cy.type`, `cy.contains`, `cy.click`, remember `https://on.cypress.io/` 56 | 57 | Note: 58 | Draw distinction between commands and assertions, show how commands can be chained, 59 | each continues to work with the subject of the previous command. Assertions do 60 | not change the subject. 61 | 62 | +++ 63 | 64 | ## Todo: mark the first item completed 65 | 66 | ```js 67 | it('can mark an item as completed', () => { 68 | // visit the site 69 | // adds a few items 70 | // marks the first item as completed 71 | // https://on.cypress.io/get 72 | // https://on.cypress.io/find 73 | // https://on.cypress.io/first 74 | // confirms the first item has the expected completed class 75 | // confirms the other items are still incomplete 76 | // check the number of remaining items 77 | }) 78 | ``` 79 | 80 | +++ 81 | 82 | ## Refactor code 1/3 83 | 84 | - visit the page before each test 85 | 86 | Note: 87 | Avoid duplicate `cy.visit('localhost:3000')` command at the start of each test. 88 | 89 | +++ 90 | 91 | ## Refactor code 2/3 92 | 93 | - move the url into `cypress.config.js` 94 | 95 | **💡 tip** look at [https://on.cypress.io/configuration](https://on.cypress.io/configuration) 96 | 97 | +++ 98 | 99 | ## Refactor code 3/3 100 | 101 | - make a helper function to add todo item 102 | 103 | **💡 tip** it is just JavaScript 104 | 105 | Note: 106 | Move `addItem` function into a separate file and import from the spec file. It is just JavaScript, and Cypress bundles each spec file, so utilities can have `cy...` commands too! 107 | 108 | +++ 109 | 110 | ## Run multiple specs 111 | 112 | `experimentalRunAllSpecs: true` config option. 113 | 114 | --- 115 | 116 | ## Todo: delete an item 117 | 118 | ```javascript 119 | it('can delete an item', () => { 120 | // adds a few items 121 | // deletes the first item 122 | // use force: true because we don't want to hover 123 | // confirm the deleted item is gone from the dom 124 | // confirm the other item still exists 125 | }) 126 | ``` 127 | 128 | --- 129 | 130 | ## Todo: use random text 131 | 132 | ```javascript 133 | it('adds item with random text', () => { 134 | // use a helper function with Math.random() 135 | // or Cypress._.random() to generate unique text label 136 | // add such item 137 | // and make sure it is visible and does not have class "completed" 138 | }) 139 | ``` 140 | 141 | --- 142 | 143 | ## Todo: no items 144 | 145 | ```js 146 | it('starts with zero items', () => { 147 | // check if the list is empty initially 148 | // find the selector for the individual TODO items in the list 149 | // use cy.get(...) and it should have length of 0 150 | // https://on.cypress.io/get 151 | // ".should('have.length', 0)" 152 | // or ".should('not.exist')" 153 | }) 154 | ``` 155 | 156 | --- 157 | 158 | ## Default assertions 159 | 160 | ```js 161 | cy.get('li.todo') 162 | // is the same as 163 | cy.get('li.todo').should('exist') 164 | ``` 165 | 166 | See [cy.get Assertions](https://on.cypress.io/get#Assertions) 167 | 168 | +++ 169 | 170 | What if you do not know if an element exists? You can disable the built-in assertions using a "dummy" `should(cb)` assertion. 171 | 172 | ```js 173 | cy.get('li.todo').should(() => {}) 174 | // or using the bundled Lodash 175 | cy.get('li.todo').should(Cypress._.noop) 176 | ``` 177 | 178 | Todo: write test "disables the built-in assertion". 179 | 180 | --- 181 | 182 | ## Todo: number of items increments by one 183 | 184 | How do you check if an unknown number of items grows by one? There might be no items at first. 185 | 186 | Implement the test "adds one more todo item" 187 | 188 | --- 189 | 190 | ## 💡 Pro tips 191 | 192 | - resize the viewport in `cypress.config.js` 193 | 194 | --- 195 | 196 | ## Checking the saved items 197 | 198 | The application saves the items in "todomvc/data.json" file. Can we verify that a new item has been saved? 199 | 200 | Todo: write the test "saves the added todos" 201 | 202 | **Tip:** use [cy.task](https://on.cypress.io/task) in the plugins file or [cy.readFile](https://on.cypress.io/readfile) 203 | 204 | --- 205 | 206 | ## Adding blank item 207 | 208 | The application does not allow adding items with blank titles. What happens when the user does it? Hint: open DevTools console. 209 | 210 | +++ 211 | 212 | ## Todo: finish this test 213 | 214 | ```js 215 | it('does not allow adding blank todos', () => { 216 | // https://on.cypress.io/catalog-of-events#App-Events 217 | cy.on('uncaught:exception', () => { 218 | // check e.message to match expected error text 219 | // return false if you want to ignore the error 220 | }) 221 | 222 | // try adding an item with just spaces 223 | }) 224 | ``` 225 | 226 | --- 227 | 228 | ## Bonus 229 | 230 | Unit tests vs end-to-end tests 231 | 232 | ### Unit tests 233 | 234 | ```javascript 235 | import add from './add' 236 | test('add', () => { 237 | expect(add(2, 3)).toBe(5) 238 | }) 239 | ``` 240 | 241 | - arrange - action - assertion 242 | 243 | +++ 244 | 245 | ### End-to-end tests 246 | 247 | ```javascript 248 | const addItem = (text) => { 249 | cy.get('.new-todo').type(`${text}{enter}`) 250 | } 251 | it('can mark items as completed', () => { 252 | const ITEM_SELECTOR = 'li.todo' 253 | addItem('simple') 254 | addItem('difficult') 255 | cy.contains(ITEM_SELECTOR, 'simple') 256 | .should('exist') 257 | .find('input[type="checkbox"]') 258 | .check() 259 | // have to force click because the button does not appear unless we hover 260 | cy.contains(ITEM_SELECTOR, 'simple').find('.destroy').click({ force: true }) 261 | cy.contains(ITEM_SELECTOR, 'simple').should('not.exist') 262 | cy.get(ITEM_SELECTOR).should('have.length', 1) 263 | cy.contains(ITEM_SELECTOR, 'difficult').should('be.visible') 264 | }) 265 | ``` 266 | 267 | command - assertion - command - assertion (CACA pattern) 268 | 269 | - **tip** check out `cy.pause` command 270 | 271 | Note: 272 | Revisit the discussion about what kind of tests one should write. E2E tests can cover a lot of features in a single test, and that is a recommended practice. If a test fails, it is easy to debug it, and see how the application looks during each step. 273 | 274 | +++ 275 | 276 | ### Unit vs component vs E2E 277 | 278 | - if you are describing how code works: **unit test** 279 | - if you are testing a component that runs in the browser: **component test** 280 | - if you are describing how code is used by the user: **end-to-end test** 281 | 282 | +++ 283 | 284 | ## Todo: run unit tests in Cypress 285 | 286 | Does this test run in Cypress? 287 | 288 | ```javascript 289 | import add from './add' 290 | test('add', () => { 291 | expect(add(2, 3)).toBe(5) 292 | }) 293 | ``` 294 | 295 | +++ 296 | 297 | ### Bonus 298 | 299 | - Core concepts [https://on.cypress.io/writing-and-organizing-tests](https://on.cypress.io/writing-and-organizing-tests) 300 | 301 | --- 302 | 303 | Organize tests using folder structure and spec files 304 | 305 | ```text 306 | cypress/integration/ 307 | featureA/ 308 | first-spec.js 309 | second-spec.js 310 | featureB/ 311 | another-spec.js 312 | errors-spec.js 313 | ``` 314 | 315 | **Tip:** splitting longer specs into smaller ones allows to run them faster in parallel mode https://glebbahmutov.com/blog/split-spec/ 316 | 317 | +++ 318 | 319 | Organize tests inside a spec using Mocha functions 320 | 321 | ```js 322 | describe('Feature A', () => { 323 | beforeEach(() => {}) 324 | 325 | it('works', () => {}) 326 | 327 | it('handles error', () => {}) 328 | 329 | // context is alias of describe 330 | context('in special case', () => { 331 | it('starts correctly', () => {}) 332 | 333 | it('works', () => {}) 334 | }) 335 | }) 336 | ``` 337 | 338 | +++ 339 | 340 | ## Support file 341 | 342 | Support file is included before each spec file. 343 | 344 | ```html 345 | 346 | 347 | ``` 348 | 349 | **💡 Tip:** Want to reset the data and visit the site before each test? Put the commands into `beforeEach` hook inside the support file. 350 | 351 | --- 352 | 353 | ## 🏁 Write your tests like a user 354 | 355 | - go through UI 356 | - validate the application after actions 357 | 358 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [03-selector-playground](?p=03-selector-playground) chapter 359 | -------------------------------------------------------------------------------- /slides/03-selector-playground/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Part 3: Selector playground 2 | 3 | ### 📚 You will learn 4 | 5 | - Cypress Selector Playground tool 6 | - best practices for selecting elements 7 | - Cypress Studio for recording tests 8 | 9 | +++ 10 | 11 | - keep `todomvc` app running 12 | - open `03-selector-playground/spec.js` 13 | 14 | --- 15 | 16 | > How do we select element in `cy.get(...)`? 17 | 18 | - Browser's DevTools can suggest selector 19 | 20 | +++ 21 | 22 | ![Chrome suggests selector](./img/chrome-copy-js-path.png) 23 | 24 | +++ 25 | 26 | Open "Selector Playground" 27 | 28 | ![Selector playground button](./img/selector-button.png) 29 | 30 | +++ 31 | 32 | Selector playground can suggest much better selectors. 33 | 34 | ![Selector playground](./img/selector-playground.png) 35 | 36 | +++ 37 | 38 | ⚠️ It can suggest a weird selector 39 | 40 | ![Default suggestion](./img/default-suggestion.png) 41 | 42 | +++ 43 | 44 | Read [best-practices.html#Selecting-Elements](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements) 45 | 46 | ![Best practice](./img/best-practice.png) 47 | 48 | +++ 49 | 50 | ## Todo 51 | 52 | - add test data ids to `todomvc/index.html` DOM markup 53 | - use new selectors to write `cypress/integration/03-selector-playground/spec.js` 54 | 55 | ```js 56 | // fill the selector, maybe use "tid" function 57 | cy.get('...').should('have.length', 2) 58 | ``` 59 | 60 | Note: 61 | The updated test should look something like the next image 62 | 63 | +++ 64 | 65 | ![Selectors](./img/selectors.png) 66 | 67 | +++ 68 | 69 | ## Cypress is just JavaScript 70 | 71 | ```js 72 | import { selectors, tid } from './common-selectors' 73 | it('finds element', () => { 74 | cy.get(selectors.todoInput).type('something{enter}') 75 | 76 | // "tid" forms "data-test-id" attribute selector 77 | // like "[data-test-id='item']" 78 | cy.get(tid('item')).should('have.length', 1) 79 | }) 80 | ``` 81 | 82 | --- 83 | 84 | ## Cypress Studio 85 | 86 | Record tests by clicking on the page 87 | 88 | ```json 89 | { 90 | "experimentalStudio": true 91 | } 92 | ``` 93 | 94 | Watch 📹 [Record A Test Using Cypress Studio](https://www.youtube.com/watch?v=kBYtqsK-8Aw) and read [https://on.cypress.io/studio](https://on.cypress.io/studio). 95 | 96 | +++ 97 | 98 | ## Start recording 99 | 100 | ![open Cypress Studio](./img/start-studio.png) 101 | 102 | --- 103 | 104 | ## 🏁 Selecting Elements 105 | 106 | - Use Selector Playground 107 | - follow [https://on.cypress.io/best-practices#Selecting-Elements](https://on.cypress.io/best-practices#Selecting-Elements) 108 | - **bonus:** try [@testing-library/cypress](https://testing-library.com/docs/cypress-testing-library/intro) 109 | 110 | +++ 111 | 112 | ## 🏁 Quickly write tests 113 | 114 | - pick elements using Selector Playground 115 | - record tests using Cypress Studio 116 | 117 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [04-reset-state](?p=04-reset-state) chapter 118 | -------------------------------------------------------------------------------- /slides/03-selector-playground/img/best-practice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/best-practice.png -------------------------------------------------------------------------------- /slides/03-selector-playground/img/chrome-copy-js-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/chrome-copy-js-path.png -------------------------------------------------------------------------------- /slides/03-selector-playground/img/default-suggestion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/default-suggestion.png -------------------------------------------------------------------------------- /slides/03-selector-playground/img/selector-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/selector-button.png -------------------------------------------------------------------------------- /slides/03-selector-playground/img/selector-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/selector-playground.png -------------------------------------------------------------------------------- /slides/03-selector-playground/img/selectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/selectors.png -------------------------------------------------------------------------------- /slides/03-selector-playground/img/start-studio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/03-selector-playground/img/start-studio.png -------------------------------------------------------------------------------- /slides/04-reset-state/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Part 4: Reset state data 2 | 3 | ### 📚 You will learn 4 | 5 | - how one test can affect another test by leaving its data behind 6 | - when and how to reset state during testing 7 | 8 | --- 9 | 10 | ## The problem 11 | 12 | - keep `todomvc` app running 13 | - open `cypress/e2e/04-reset-state/spec.js` 14 | - if you reload the test it starts failing 😕 15 | 16 | +++ 17 | 18 | ![The first test run](./img/passing-test.png) 19 | 20 | +++ 21 | 22 | ![The second test run](./img/failing-test.png) 23 | 24 | +++ 25 | 26 | ![Inspect the first network call](./img/inspect-first-get-todos.png) 27 | 28 | --- 29 | 30 | ```javascript 31 | // cypress/e2e/04-reset-state/spec.js 32 | beforeEach(() => { 33 | cy.visit('/') 34 | }) 35 | const addItem = (text) => { 36 | cy.get('.new-todo').type(`${text}{enter}`) 37 | } 38 | it('adds two items', () => { 39 | addItem('first item') 40 | addItem('second item') 41 | cy.get('li.todo').should('have.length', 2) 42 | }) 43 | ``` 44 | 45 | +++ 46 | 47 | ## Anti-pattern: using UI to clean up the state 48 | 49 | - there could be 0, 1, or more items to remove 50 | - the items could be paginated 51 | - the spec becomes full of logic 52 | 53 | See the example test in the spec file, it is complicated. 54 | 55 | +++ 56 | 57 | ## Questions 58 | 59 | - how to reset the database? 60 | - **tip** we are using [json-server-reset](https://github.com/bahmutov/json-server-reset#readme) middleware 61 | - try to reset it from command line 62 | 63 | ```shell 64 | # using https://httpie.io/ instead of curl 65 | $ http POST :3000/reset todos:=[] 66 | ``` 67 | 68 | --- 69 | 70 | - how to make an arbitrary cross-domain XHR request from Cypress? 71 | - reset the database before each test 72 | - modify `04-reset-state/spec.js` to make XHR call to reset the database 73 | - before or after `cy.visit`? 74 | 75 | Note: 76 | Students should modify `cypress/e2e/04-reset-state/spec.js` and make the request to reset the database before each test using `cy.request`. 77 | 78 | The answer to this and other TODO assignments are in [cypress/e2e/04-reset-state/answer.js](/cypress/e2e/04-reset-state/answer.js) file. 79 | 80 | --- 81 | 82 | ## Alternative: Using cy.writeFile 83 | 84 | ``` 85 | "start": "json-server --static . --watch data.json" 86 | ``` 87 | 88 | If we overwrite `todomvc/data.json` and reload the web app we should see new data 89 | 90 | +++ 91 | 92 | ## TODO: use cy.writeFile to reset todos 93 | 94 | ```js 95 | describe('reset data using cy.writeFile', () => { 96 | beforeEach(() => { 97 | // TODO write file "todomvc/data.json" with stringified todos object 98 | cy.visit('/') 99 | }) 100 | ... 101 | }) 102 | ``` 103 | 104 | See [`cy.writeFile`](https://on.cypress.io/writefile) 105 | 106 | +++ 107 | Make sure you are writing the right file. 108 | 109 | ![See the file path written](./img/write-file-path.png) 110 | 111 | Note: 112 | Most common mistake is using file path relative to the spec file, should be relative to the project's root folder. 113 | 114 | --- 115 | 116 | ## Alternative: use cy.task 117 | 118 | You can execute Node code during browser tests by calling [`cy.task`](https://on.cypress.io/task) 119 | 120 | ```js 121 | // cypress.config.file 122 | // runs in Node 123 | setupNodeEvents(on, config) { 124 | on('task', { 125 | hello(name) { 126 | console.log('Hello', name) 127 | return null // or Promise 128 | } 129 | }) 130 | } 131 | // cypress/e2e/spec.js 132 | // runs in the browser 133 | cy.task('hello', 'World') 134 | ``` 135 | 136 | +++ 137 | 138 | ## TODO reset data using cy.task 139 | 140 | Find "resetData" task in `cypress.config.js` 141 | 142 | ```js 143 | describe('reset data using a task', () => { 144 | beforeEach(() => { 145 | // call the task "resetData" 146 | // https://on.cypress.io/task 147 | // cy.task("resetData", ...) 148 | cy.visit('/') 149 | }) 150 | }) 151 | ``` 152 | 153 | +++ 154 | 155 | ## TODO set data using cy.task 156 | 157 | Pass an object when calling `cy.task('resetData')` 158 | 159 | ```js 160 | it('sets data to complex object right away', () => { 161 | cy.task('resetData' /* object*/) 162 | cy.visit('/') 163 | // check what is rendered 164 | }) 165 | ``` 166 | 167 | +++ 168 | 169 | ## TODO set data from fixture 170 | 171 | Pass an object when calling `cy.task('resetData')` 172 | 173 | ```js 174 | it('sets data using fixture', () => { 175 | // load todos from "cypress/fixtures/two-items.json" 176 | // https://on.cypress.io/fixture 177 | // and then call the task to set todos 178 | // https://on.cypress.io/task 179 | cy.visit('/') 180 | // check what is rendered 181 | }) 182 | ``` 183 | 184 | --- 185 | 186 | ## Reset data before each spec 187 | 188 | Using the `experimentalInteractiveRunEvents` flag 189 | 190 | ```js 191 | // cypress.config.js 192 | setupNodeEvents(on, config) { 193 | on('before:spec', (spec) => { 194 | console.log('resetting DB before spec %s', spec.name) 195 | resetData() 196 | }) 197 | } 198 | ``` 199 | 200 | **Warning:** as of Cypress v7 only available in `cypress run` mode. See [https://on.cypress.io/before-spec-api](https://on.cypress.io/before-spec-api) for details. 201 | 202 | --- 203 | 204 | ## Best practices 205 | 206 | - reset state before each test 207 | - in our [Best practices guide](https://on.cypress.io/best-practices) 208 | - use [`cy.request`](https://on.cypress.io/request), [`cy.exec`](https://on.cypress.io/exec), [`cy.task`](https://on.cypress.io/task) 209 | - watch presentation "Cypress: beyond the Hello World test" [https://slides.com/bahmutov/cypress-beyond-the-hello-world](https://slides.com/bahmutov/cypress-beyond-the-hello-world) 210 | 211 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [05-network](?p=05-network) chapter 212 | -------------------------------------------------------------------------------- /slides/04-reset-state/img/failing-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/04-reset-state/img/failing-test.png -------------------------------------------------------------------------------- /slides/04-reset-state/img/inspect-first-get-todos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/04-reset-state/img/inspect-first-get-todos.png -------------------------------------------------------------------------------- /slides/04-reset-state/img/passing-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/04-reset-state/img/passing-test.png -------------------------------------------------------------------------------- /slides/04-reset-state/img/write-file-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/04-reset-state/img/write-file-path.png -------------------------------------------------------------------------------- /slides/05-network/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Part 5: Control network calls 2 | 3 | ### 📚 You will learn 4 | 5 | - how to spy on / stub network calls 6 | - how to wait for the network calls from tests 7 | - how to use network calls in assertions 8 | 9 | +++ 10 | 11 | - keep `todomvc` app running 12 | - open `cypress/e2e/05-network/spec.js` 13 | - read [cy.intercept](https://on.cypress.io/intercept) API documentation 14 | 15 | 📖 Fun read: [Cypress Network Requests Guide](https://on.cypress.io/network-requests) and [https://glebbahmutov.com/blog/cypress-intercept-problems/](https://glebbahmutov.com/blog/cypress-intercept-problems/) 16 | 17 | --- 18 | 19 | ## Situation 20 | 21 | - there is **no resetting** the state before each test 22 | - the test passes but _something is wrong_ 23 | 24 | ```javascript 25 | it('starts with zero items', () => { 26 | cy.visit('/') 27 | cy.get('li.todo').should('have.length', 0) 28 | }) 29 | ``` 30 | 31 | ![Should have failed](./img/test-passes-but-this-is-wrong.png) 32 | 33 | +++ 34 | 35 | ## Problem 36 | 37 | - page loads 38 | - web application makes XHR call "GET /todos" 39 | - meanwhile it shows an empty list of todos 40 | - Cypress assertion passes! 41 | - "GET /todos" returns with 2 items 42 | - they are added to the DOM 43 | - but the test has already finished 44 | 45 | --- 46 | 47 | ## Waiting 48 | 49 | ```javascript 50 | // 05-network/spec.js 51 | it('starts with zero items (waits)', () => { 52 | cy.visit('/') 53 | cy.wait(1000) 54 | cy.get('li.todo').should('have.length', 0) 55 | }) 56 | ``` 57 | 58 | ![Waiting works](./img/waiting.png) 59 | 60 | +++ 61 | 62 | There might be multiple delays: loading the page, fetching todos, rendering data on the page. 63 | 64 | ![Page load diagram](./img/get-todos.png) 65 | 66 | +++ 67 | 68 | ## Wait for application signal 69 | 70 | ```js 71 | // todomvc/app.js 72 | axios.get('/todos') 73 | ... 74 | .finally(() => { 75 | // an easy way for the application to signal 76 | // that it is done loading 77 | document.body.classList.add('loaded') 78 | }) 79 | ``` 80 | 81 | **TODO:** write a test that waits for the body to have class "loaded" after the visit 82 | 83 | ⌨️ test "starts with zero items (check body.loaded)" 84 | 85 | +++ 86 | 87 | **better** to wait on a specific XHR request. Network is just observable public effect, just like DOM. 88 | 89 | +++ 90 | 91 | ### Todo 92 | 93 | Use the test "starts with zero items" in the file `05-network/spec.js` 94 | 95 | - spy on specific route with "cy.intercept" 96 | - should we set the spy _before_ or _after_ `cy.visit`? 97 | - save as an alias 98 | - wait for this XHR alias 99 | - then check the DOM 100 | 101 | **tips:** [`cy.intercept`]('https://on.cypress.io/intercept), [Network requests guide](https://on.cypress.io/network-requests) 102 | 103 | +++ 104 | 105 | 💡 No need to `cy.wait(...).then(...)`. All Cypress commands will be chained automatically. 106 | 107 | ```js 108 | cy.intercept('GET', '/todos').as('todos') 109 | cy.visit('/') 110 | cy.wait('@todos') 111 | // cy.get() will run AFTER cy.wait() finishes 112 | cy.get('li.todo').should('have.length', 0) 113 | ``` 114 | 115 | Read [Introduction to Cypress](https://on.cypress.io/introduction-to-cypress) "Commands Run Serially" 116 | 117 | --- 118 | 119 | ## Todo 120 | 121 | add to the test "starts with zero items": 122 | 123 | - wait for the XHR alias like before 124 | - its response body should be an empty array 125 | 126 | ![Checking response body](./img/response-body.png) 127 | 128 | --- 129 | 130 | ## Todo 131 | 132 | There are multiple tests in the "05-network/spec.js" that you can go through 133 | 134 | - 'starts with zero items (delay)' 135 | - 'starts with zero items (delay plus render delay)' 136 | - 'starts with zero items (check body.loaded)' 137 | - 'starts with zero items (check the window)' 138 | - 'starts with N items' 139 | - 'starts with N items and checks the page' 140 | 141 | **Tip:** read [https://glebbahmutov.com/blog/app-loaded/](https://glebbahmutov.com/blog/app-loaded/) 142 | 143 | --- 144 | 145 | ## Stub the network call 146 | 147 | Update test "starts with zero items (stubbed response)" 148 | 149 | - instead of just spying on XHR call, let's return some mock data 150 | 151 | ```javascript 152 | // returns an empty list 153 | // when `GET /todos` is requested 154 | cy.intercept('GET', '/todos', []) 155 | ``` 156 | 157 | +++ 158 | 159 | ```javascript 160 | it('starts with zero items (fixture)', () => { 161 | // stub `GET /todos` with fixture "empty-list" 162 | 163 | // visit the page 164 | cy.visit('/') 165 | 166 | // then check the DOM 167 | cy.get('li.todo').should('have.length', 0) 168 | }) 169 | ``` 170 | 171 | **tip:** use [`cy.fixture`](https://on.cypress.io/fixture) command 172 | 173 | +++ 174 | 175 | ```javascript 176 | it('loads several items from a fixture', () => { 177 | // stub route `GET /todos` with data from a fixture file "two-items.json" 178 | // THEN visit the page 179 | cy.visit('/') 180 | // then check the DOM: some items should be marked completed 181 | // we can do this in a variety of ways 182 | }) 183 | ``` 184 | 185 | --- 186 | 187 | ### Spying on adding an item network call 188 | 189 | When you add an item through the DOM, the app makes `POST` XHR call. 190 | 191 | ![Post new item](./img/post-item.png) 192 | 193 | Note: 194 | It is important to be able to use DevTools network tab to inspect the XHR and its request and response. 195 | 196 | +++ 197 | 198 | **Todo 1/3** 199 | 200 | - write a test "posts new item to the server" that confirms that new item is posted to the server 201 | 202 | ![Post new item](./img/post-item.png) 203 | 204 | Note: 205 | see instructions in the `05-network/spec.js` for the test 206 | 207 | +++ 208 | 209 | **Todo 2/3** 210 | 211 | - write a test "posts new item to the server response" that confirms that RESPONSE when a new item is posted to the server 212 | 213 | ![Post new item response](./img/post-item-response.png) 214 | 215 | Note: 216 | see instructions in the `05-network/spec.js` for the test 217 | 218 | +++ 219 | 220 | **Todo 3/3** 221 | 222 | - ⌨️ implement the test "'confirms the request and the response" 223 | - verify the request body and the response body of the intercept 224 | - **Tip:** after you waited for the intercept once, you can use `cy.get('@alias')` 225 | 226 | ![Post new item response](./img/post-item-response.png) 227 | 228 | Note: 229 | see instructions in the `05-network/spec.js` for the test 230 | 231 | --- 232 | 233 | ## Bonus 234 | 235 | Network requests guide at [https://on.cypress.io/network-requests](https://on.cypress.io/network-requests), blog post [https://glebbahmutov.com/blog/network-requests-with-cypress/](https://glebbahmutov.com/blog/network-requests-with-cypress/) 236 | 237 | **Question:** which requests do you spy on, which do you stub? 238 | 239 | --- 240 | 241 | ## Caching 242 | 243 | ⚠️ Be careful with the browser caching the data. If the browser caches the data, the server might return "304" HTTP code. 244 | 245 | ⌨️ test "shows the items loaded from the server" 246 | 247 | --- 248 | 249 | ## Testing the loading element 250 | 251 | In the application we are showing (very quickly) "Loading" state 252 | 253 | ```html 254 |
Loading data ...
255 | ``` 256 | 257 | +++ 258 | 259 | ## Todo 260 | 261 | - delay the loading XHR request 262 | - assert the UI is showing "Loading" element 263 | - assert the "Loading" element goes away after XHR completes 264 | 265 | ⌨️ test "shows loading element" 266 | 267 | **Note:** most querying commands have the built-in `should('exist')` assertion, thus in this case we need to use `should('be.visible')` and `should('not.be.visible')` assertions. 268 | 269 | --- 270 | 271 | ## Refactor a failing test 272 | 273 | ```js 274 | // cypress/e2e/05-network/spec.js 275 | // can you fix this test? 276 | it.skip('confirms the right Todo item is sent to the server', () => { 277 | const id = cy.wait('@postTodo').then((intercept) => { 278 | // assert the response fields 279 | return intercept.response.body.id 280 | }) 281 | console.log(id) 282 | }) 283 | ``` 284 | 285 | ⌨️ test "refactor example" 286 | 287 | --- 288 | 289 | ## Let's test an edge data case 290 | 291 | User cannot enter blank titles. What if our database has old data records with blank titles which it returns on load? Does the application show them? Does it crash? 292 | 293 | **Todo:** write the test `handles todos with blank title` 294 | 295 | --- 296 | 297 | ## Test periodic network requests 298 | 299 | The application loads Todos every minute 300 | 301 | ```js 302 | // how would you test the periodic loading of todos? 303 | setInterval(() => { 304 | this.$store.dispatch('loadTodos') 305 | }, 60000) 306 | ``` 307 | 308 | +++ 309 | 310 | ## Application clock 311 | 312 | - learn about controlling the web page clock [https://on.cypress.io/stubs-spies-and-clocks](https://on.cypress.io/stubs-spies-and-clocks) 313 | - use [cy.clock](https://on.cypress.io/clock) and [cy.tick](https://on.cypress.io/tick) commands 314 | 315 | +++ 316 | 317 | ## Todo 318 | 319 | - set up a test "loads todos every minute" that intercepts the `GET /todos` with different responses using `times: 1` option 320 | - advance the clock by 1 minute and confirm different responses are displayed 321 | 322 | ⌨️ test "test periodic loading" 323 | 324 | --- 325 | 326 | ## Wait for Network Idle 327 | 328 | You can spy on every network request and keep track of its timestamp. Waiting for network idle means waiting for the network request to be older than N milliseconds before continuing the test. 329 | 330 | **Todo:** implement the test "waits for the network to be idle for 2 seconds". Bonus for logging the timings. 331 | 332 | **Todo 2:** or use [cypress-network-idle](https://github.com/bahmutov/cypress-network-idle) plugin 333 | 334 | --- 335 | 336 | ## 🏁 Spy and stub the network from your tests 337 | 338 | - confirm the REST calls 339 | - stub random data 340 | - 🎓 [Cypress Network Testing Exercises](https://cypress.tips/courses/network-testing) course 341 | 342 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [06-app-data-store](?p=06-app-data-store) chapter 343 | -------------------------------------------------------------------------------- /slides/05-network/img/get-todos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/05-network/img/get-todos.png -------------------------------------------------------------------------------- /slides/05-network/img/post-item-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/05-network/img/post-item-response.png -------------------------------------------------------------------------------- /slides/05-network/img/post-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/05-network/img/post-item.png -------------------------------------------------------------------------------- /slides/05-network/img/response-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/05-network/img/response-body.png -------------------------------------------------------------------------------- /slides/05-network/img/test-passes-but-this-is-wrong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/05-network/img/test-passes-but-this-is-wrong.png -------------------------------------------------------------------------------- /slides/05-network/img/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/05-network/img/waiting.png -------------------------------------------------------------------------------- /slides/06-app-data-store/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Part 6: Application data store 2 | 3 | ### 📚 You will learn 4 | 5 | - how to access the running application from test code 6 | - how to spy on or stub an application method 7 | - how to drive application by dispatching actions 8 | 9 | +++ 10 | 11 | - keep `todomvc` app running 12 | - open `cypress/e2e/06-app-data-store/spec.js` 13 | - test that Vuex data store is working correctly 14 | 15 | --- 16 | 17 | ## Spy on console.log 18 | 19 | ```js 20 | it('logs a todo add message to the console', () => { 21 | // get the window object from the app's iframe 22 | // using https://on.cypress.io/window 23 | // get its console object and spy on the "log" method 24 | // using https://on.cypress.io/spy 25 | // add a new todo item 26 | // get the spy and check that it was called 27 | // with the expected arguments 28 | }) 29 | ``` 30 | 31 | Read [https://on.cypress.io/stubs-spies-and-clocks](https://on.cypress.io/stubs-spies-and-clocks) and [https://glebbahmutov.com/cypress-examples/commands/spies-stubs-clocks.html](https://glebbahmutov.com/cypress-examples/commands/spies-stubs-clocks.html) 32 | 33 | --- 34 | 35 | ## The application object 36 | 37 | ```javascript 38 | // todomvc/app.js 39 | // if you want to expose "app" globally only 40 | // during end-to-end tests you can guard it using "window.Cypress" flag 41 | // if (window.Cypress) { 42 | window.app = app 43 | // } 44 | ``` 45 | 46 | +++ 47 | 48 | ![window.app object](./img/window-app.png) 49 | 50 | +++ 51 | 52 | ## Todo: confirm window.app 53 | 54 | ```js 55 | // cypress/e2e/06-app-data-store/spec.js 56 | it('has window.app property', () => { 57 | // get its "app" property 58 | // and confirm it is an object 59 | // see https://on.cypress.io/its 60 | cy.window() 61 | }) 62 | ``` 63 | 64 | --- 65 | 66 | ## Todo: confirm window.app.$store 67 | 68 | ```js 69 | // cypress/e2e/06-app-data-store/spec.js 70 | it('has window.app property', () => { 71 | // get the app.$store property 72 | // and confirm it has expected Vuex properties 73 | // see https://on.cypress.io/its 74 | cy.window() 75 | }) 76 | ``` 77 | 78 | --- 79 | 80 | ## Todo: the initial Vuex state 81 | 82 | ```js 83 | it('starts with an empty store', () => { 84 | // the list of todos in the Vuex store should be empty 85 | cy.window() 86 | }) 87 | ``` 88 | 89 | --- 90 | 91 | ## Todo: check Vuex state 92 | 93 | Let's add two items via the page, then confirm the Vuex store has them 94 | 95 | ```javascript 96 | // cypress/e2e/06-app-data-store/spec.js 97 | const addItem = (text) => { 98 | cy.get('.new-todo').type(`${text}{enter}`) 99 | } 100 | it('adds items to store', () => { 101 | addItem('something') 102 | addItem('something else') 103 | // get application's window 104 | // then get app, $store, state, todos 105 | // it should have 2 items 106 | }) 107 | ``` 108 | 109 | --- 110 | 111 | ## Question 112 | 113 | Why can't we confirm both items using `should('deep.equal', [...])`? 114 | 115 | ```js 116 | cy.window() 117 | .its('app.$store.state.todos') 118 | .should('deep.equal', [ 119 | { title: 'something', completed: false, id: '1' }, 120 | { title: 'else', completed: false, id: '2' } 121 | ]) 122 | ``` 123 | 124 | +++ 125 | 126 | ![Random id](./img/new-todo.png) 127 | 128 | --- 129 | 130 | ## Non-determinism 131 | 132 | - random data in tests makes it very hard 133 | - UUIDs, dates, etc 134 | - Cypress includes network and method stubbing using [https://sinonjs.org/](https://sinonjs.org/) 135 | - [https://on.cypress.io/network-requests](https://on.cypress.io/network-requests) 136 | - [https://on.cypress.io/stubs-spies-and-clocks](https://on.cypress.io/stubs-spies-and-clocks) 137 | 138 | +++ 139 | 140 | ## Questions 141 | 142 | - how does a new item get its id? 143 | - can you override random id generator from DevTools? 144 | 145 | --- 146 | 147 | ## Stub application's random generator 148 | 149 | - ⌨️ test "creates an item with id 1" in `06-app-data-store/spec.js` 150 | - get the application's context using `cy.window` 151 | - get application's `window.Math` object 152 | - can you stub application's random generator? 153 | - **hint** use `cy.stub` 154 | 155 | +++ 156 | 157 | ## Confirm spy's behavior 158 | 159 | - ⌨️ test "creates an item with id using a stub" 160 | - write a test that adds 1 item 161 | - name spy with an alias `cy.spy(...).as('name')` 162 | - get the spy using the alias and confirm it was called once 163 | 164 | --- 165 | 166 | ## Abstract common actions 167 | 168 | The tests can repeat common actions (like creating items) by always going through the DOM, called **page objects** 169 | 170 | The tests can access the app and call method bypassing the DOM, called "app actions" 171 | 172 | The tests can be a combination of DOM and App actions. 173 | 174 | Read [https://glebbahmutov.com/blog/realworld-app-action/](https://glebbahmutov.com/blog/realworld-app-action/) 175 | 176 | +++ 177 | 178 | ## Practice 179 | 180 | Write a test that: 181 | 182 | - dispatches actions to the store to add items 183 | - confirms new items are added to the DOM 184 | 185 | (see next slide) 186 | +++ 187 | 188 | ```js 189 | it('adds todos via app', () => { 190 | // bypass the UI and call app's actions directly from the test 191 | // using https://on.cypress.io/invoke 192 | // app.$store.dispatch('setNewTodo', ) 193 | // app.$store.dispatch('addTodo') 194 | // and then check the UI 195 | }) 196 | ``` 197 | 198 | +++ 199 | 200 | ## Todo: test edge data case 201 | 202 | ```js 203 | it('handles todos with blank title', () => { 204 | // add todo that the user cannot add via UI 205 | cy.window().its('app.$store').invoke('dispatch', 'setNewTodo', ' ') 206 | // app.$store.dispatch('addTodo') 207 | // confirm the UI 208 | }) 209 | ``` 210 | 211 | +++ 212 | 213 | ### ⚠️ Watch out for stale data 214 | 215 | Note that the web application might NOT have updated the data right away. For example: 216 | 217 | ```js 218 | getStore().then((store) => { 219 | store.dispatch('setNewTodo', 'a new todo') 220 | store.dispatch('addTodo') 221 | store.dispatch('clearNewTodo') 222 | }) 223 | // not necessarily has the new item right away 224 | getStore().its('state') 225 | ``` 226 | 227 | Note: 228 | In a flaky test https://github.com/cypress-io/cypress-example-recipes/issues/246 the above code was calling `getStore().its('state').snapshot()` sometimes before and sometimes after updating the list of todos. 229 | 230 | +++ 231 | 232 | ### ⚠️ Watch out for stale data 233 | 234 | **Solution:** confirm the data is ready before using it. 235 | 236 | ```js 237 | // add new todo using dispatch 238 | // retry until new item is in the list 239 | getStore().its('state.todos').should('have.length', 1) 240 | // do other checks 241 | ``` 242 | 243 | --- 244 | 245 | ## 🏁 App Access 246 | 247 | - when needed, you can access the application directly from the test 248 | 249 | Read also: 250 | 251 | - https://www.cypress.io/blog/2018/11/14/testing-redux-store/, 252 | - https://glebbahmutov.com/blog/stub-navigator-api/ 253 | - https://glebbahmutov.com/blog/realworld-app-action/ 254 | 255 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [07-ci](?p=07-ci) chapter 256 | -------------------------------------------------------------------------------- /slides/06-app-data-store/img/app-in-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/06-app-data-store/img/app-in-window.png -------------------------------------------------------------------------------- /slides/06-app-data-store/img/contexts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/06-app-data-store/img/contexts.png -------------------------------------------------------------------------------- /slides/06-app-data-store/img/new-todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/06-app-data-store/img/new-todo.png -------------------------------------------------------------------------------- /slides/06-app-data-store/img/window-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/06-app-data-store/img/window-app.png -------------------------------------------------------------------------------- /slides/07-ci/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Part 7: Continuous integration 2 | 3 | ### 📚 You will learn 4 | 5 | - Cypress Docker images for dependencies 6 | - Installing and caching Cypress itself 7 | - How to start server and run Cypress tests 8 | - CircleCI Orb example 9 | - GitHub Actions example 10 | - GitHub reusable workflows 11 | - How to run tests faster 12 | - Cypress paid Test Replay 13 | 14 | --- 15 | 16 | ## Poll: what CI do you use? 17 | 18 | - ❤️ GitHub Actions 19 | - 👏 CircleCI 20 | - 👍 Jenkins 21 | - 🎉 Something else 22 | 23 | --- 24 | 25 | ## Todo if possible 26 | 27 | - sign up for free account on CircleCI 28 | - use your fork of https://github.com/bahmutov/testing-app-example 29 | 30 | Or make your copy of it using 31 | 32 | ``` 33 | $ npx degit https://github.com/bahmutov/testing-app-example test-app-my-example 34 | $ cd test-app-my-example 35 | $ npm i 36 | $ npm start 37 | # create GitHub repo and push "test-app-my-example" 38 | ``` 39 | 40 | --- 41 | 42 | ## Open vs Run 43 | 44 | - run the specs in the interactive mode with `cypress open` 45 | - run the specs in the headless mode with `cypress run` 46 | 47 | See [https://on.cypress.io/command-line](https://on.cypress.io/command-line) 48 | 49 | +++ 50 | 51 | ## Set up Cypress 52 | 53 | ``` 54 | $ npm i -D cypress 55 | $ npx @bahmutov/cly init -b 56 | # add a test or two 57 | ``` 58 | 59 | --- 60 | 61 | ## Set up CircleCI 62 | 63 | - sign up for CircleCI 64 | - add your project to CircleCI 65 | 66 | ![Add project](./img/add-project.png) 67 | 68 | +++ 69 | 70 | ## Continuous integration documentation 71 | 72 | - [https://on.cypress.io/continuous-integration](https://on.cypress.io/continuous-integration) 73 | - [https://on.cypress.io/ci](https://on.cypress.io/ci) (alias) 74 | 75 | --- 76 | 77 | ## On every CI: 78 | 79 | - install and cache dependencies 80 | - start `todomvc` server in the background 81 | - run Cypress using `npx cypress run` 82 | - (maybe) stop the `todomvc` server 83 | 84 | +++ 85 | 86 | ```yaml 87 | version: 2 88 | jobs: 89 | build: 90 | docker: 91 | - image: cypress/base:16.14.2-slim 92 | working_directory: ~/repo 93 | steps: 94 | - checkout 95 | - restore_cache: 96 | keys: 97 | - dependencies-{{ checksum "package.json" }} 98 | # fallback to using the latest cache if no exact match is found 99 | - dependencies- 100 | - run: 101 | name: Install dependencies 102 | # https://docs.npmjs.com/cli/ci 103 | command: npm ci 104 | - save_cache: 105 | paths: 106 | - ~/.npm 107 | - ~/.cache 108 | key: dependencies-{{ checksum "package.json" }} 109 | # continued: start the app and run the tests 110 | ``` 111 | 112 | +++ 113 | 114 | ```yaml 115 | # two commands: start server, run tests 116 | - run: 117 | name: Start TodoMVC server 118 | command: npm start 119 | working_directory: todomvc 120 | background: true 121 | - run: 122 | name: Run Cypress tests 123 | command: npx cypress run 124 | ``` 125 | 126 | +++ 127 | 128 | Alternative: use [start-server-and-test](https://github.com/bahmutov/start-server-and-test) 129 | 130 | ```yaml 131 | - run: 132 | name: Start and test 133 | command: npm run ci 134 | ``` 135 | 136 | ```json 137 | { 138 | "scripts": { 139 | "start": "npm start --prefix todomvc -- --quiet", 140 | "test": "cypress run", 141 | "ci": "start-test http://localhost:3000" 142 | } 143 | } 144 | ``` 145 | 146 | --- 147 | 148 | ## CircleCI Cypress Orb 149 | 150 | A _much simpler_ CI configuration. **⚠️ Warning:** Cypress Orb v3 has significant changes. 151 | 152 | ```yaml 153 | version: 2.1 154 | orbs: 155 | # import Cypress orb by specifying an exact version x.y.z 156 | # or the latest version 2.x.x using "@1" syntax 157 | # https://github.com/cypress-io/circleci-orb 158 | cypress: cypress-io/cypress@2 159 | workflows: 160 | build: 161 | jobs: 162 | # "cypress" is the name of the imported orb 163 | # "run" is the name of the job defined in Cypress orb 164 | - cypress/run: 165 | start: npm start 166 | ``` 167 | 168 | See [https://github.com/cypress-io/circleci-orb](https://github.com/cypress-io/circleci-orb) 169 | 170 | +++ 171 | 172 | ## Todo 173 | 174 | Look how tests are run in [.circleci/config.yml](https://github.com/bahmutov/cypress-workshop-basics/blob/main/.circleci/config.yml) using [cypress-io/circleci-orb](https://github.com/cypress-io/circleci-orb). 175 | 176 | --- 177 | 178 | ## Store test artifacts 179 | 180 | ```yaml 181 | version: 2.1 182 | orbs: 183 | # https://github.com/cypress-io/circleci-orb 184 | cypress: cypress-io/cypress@2 185 | workflows: 186 | build: 187 | jobs: 188 | - cypress/run: 189 | # store videos and any screenshots after tests 190 | store_artifacts: true 191 | ``` 192 | 193 | +++ 194 | 195 | ## Record results on Dashboard 196 | 197 | ```yaml 198 | version: 2.1 199 | orbs: 200 | # https://github.com/cypress-io/circleci-orb 201 | cypress: cypress-io/cypress@2 202 | workflows: 203 | build: 204 | jobs: 205 | # set CYPRESS_RECORD_KEY as CircleCI 206 | # environment variable 207 | - cypress/run: 208 | record: true 209 | ``` 210 | 211 | [https://on.cypress.io/dashboard-introduction](https://on.cypress.io/dashboard-introduction) 212 | 213 | +++ 214 | 215 | ## Parallel builds 216 | 217 | ```yaml 218 | version: 2.1 219 | orbs: 220 | # https://github.com/cypress-io/circleci-orb 221 | cypress: cypress-io/cypress@2 222 | workflows: 223 | build: 224 | jobs: 225 | - cypress/install # single install job 226 | - cypress/run: # 4 test jobs 227 | requires: 228 | - cypress/install 229 | record: true # record results on Cypress Dashboard 230 | parallel: true # split all specs across machines 231 | parallelism: 4 # use 4 CircleCI machines 232 | ``` 233 | 234 | +++ 235 | 236 | ## CircleCI Cypress Orb 237 | 238 | Never struggle with CI config 👍 239 | 240 | - [github.com/cypress-io/circleci-orb](https://github.com/cypress-io/circleci-orb) 241 | - [circleci.com/orbs/registry/orb/cypress-io/cypress](https://circleci.com/orbs/registry/orb/cypress-io/cypress) 242 | - 📺 [CircleCI + Cypress webinar](https://youtu.be/J-xbNtKgXfY) 243 | 244 | --- 245 | 246 | ## GitHub Actions 247 | 248 | - cross-platform CI built on top of Azure CI + MacStadium 249 | - Linux, Windows, and Mac 250 | - Official [cypress-io/github-action](https://github.com/cypress-io/github-action) 251 | 252 | +++ 253 | 254 | ```yaml 255 | jobs: 256 | cypress-run: 257 | runs-on: ubuntu-20.04 258 | steps: 259 | - uses: actions/checkout@v4 260 | # https://github.com/cypress-io/github-action 261 | - uses: cypress-io/github-action@v6 262 | with: 263 | start: npm start 264 | wait-on: 'http://localhost:3000' 265 | ``` 266 | 267 | Check [.github/workflows/ci.yml](https://github.com/bahmutov/cypress-workshop-basics/blob/main/.github/workflows/ci.yml) 268 | 269 | --- 270 | 271 | ## GitHub Reusable Workflows 272 | 273 | ```yml 274 | name: ci 275 | on: [push] 276 | jobs: 277 | test: 278 | # use the reusable workflow to check out the code, install dependencies 279 | # and run the Cypress tests 280 | # https://github.com/bahmutov/cypress-workflows 281 | uses: bahmutov/cypress-workflows/.github/workflows/standard.yml@v1 282 | with: 283 | start: npm start 284 | ``` 285 | 286 | [https://github.com/bahmutov/cypress-workflows](https://github.com/bahmutov/cypress-workflows) 287 | 288 | --- 289 | 290 | ## Cypress on CI: the take away 291 | 292 | - use `npm ci` command instead of `npm install` 293 | - cache `~/.npm` and `~/.cache` folders 294 | - use [start-server-and-test](https://github.com/bahmutov/start-server-and-test) for simplicity 295 | - store videos and screenshots yourself or use Cypress Dashboard 296 | 297 | --- 298 | 299 | ## Run E2E faster 300 | 301 | 1. Run changed specs first 302 | 2. Run tests by tag 303 | 3. Run tests in parallel 304 | 4. Run specs based on test IDs in the modified source files 305 | 306 | +++ 307 | 308 | 1. Run changed specs first 309 | 310 | 📝 Read [Get Faster Feedback From Your Cypress Tests Running On CircleCI](https://glebbahmutov.com/blog/faster-ci-feedback-on-circleci/) 311 | 312 | ``` 313 | $ specs=$(npx find-cypress-specs --branch main) 314 | $ npx cypress run --spec $specs 315 | ``` 316 | 317 | See [find-cypress-specs](https://github.com/bahmutov/find-cypress-specs) 318 | 319 | +++ 320 | 321 | 2. Run tests by tag 322 | 323 | 📝 Read [How To Tag And Run End-to-End Tests](https://glebbahmutov.com/blog/tag-tests/) 324 | 325 | ```js 326 | it('logs in', { tags: 'user' }, () => ...) 327 | ``` 328 | 329 | ``` 330 | $ npx cypress run --env grepTags=user 331 | ``` 332 | 333 | See [@bahmutov/cy-grep](https://github.com/bahmutov/cy-grep) 334 | 335 | +++ 336 | 337 | 3. Run tests in parallel 338 | 339 | ![Workshop tests](./img/workshop-tests.png) 340 | 341 | +++ 342 | 343 | 4. Run specs based on test IDs in the modified source files 344 | 345 | 📝 Read [Using Test Ids To Pick Cypress Specs To Run](https://glebbahmutov.com/blog/using-test-ids-to-pick-specs-to-run/) 346 | 347 | +++ 348 | 349 | ## Examples of running specs in parallel 350 | 351 | - 📝 [Make Cypress Run Faster by Splitting Specs](https://glebbahmutov.com/blog/split-spec/) 352 | - 📝 [Split Long GitHub Action Workflow Into Parallel Cypress Jobs](https://glebbahmutov.com/blog/parallel-cypress-tests-gh-action/) 353 | - 📝 [Testing Time Zones in Parallel](https://glebbahmutov.com/blog/testing-timezones/) 354 | 355 | --- 356 | 357 | ## Run tests in parallel for free 358 | 359 | - 🔌 plugin [cypress-split](https://github.com/bahmutov/cypress-split) 360 | - 📝 read https://glebbahmutov.com/blog/cypress-parallel-free/ 361 | 362 | --- 363 | 364 | ## Test Replay 365 | 366 | - optional paid Cypress service for recording tests 367 | - requires Cypress v13+ 368 | - recreates the local time travel experience 369 | 370 | +++ 371 | 372 | ![Cypress cloud badge](./img/cypress-cloud-badge.png) 373 | 374 | Click on the Cypress Cloud badge to view recorded tests 375 | 376 | +++ 377 | 378 | Test replays for each test run at https://cloud.cypress.io/projects/89mmxs/runs 379 | 380 | Good example test is "can mark an item as completed" from `02-adding-items/answer.js` 381 | 382 | +++ 383 | 384 | ![Test Replay button](./img/replay-button.png) 385 | 386 | +++ 387 | 388 | ![Test Replay screenshot](./img/replay.png) 389 | 390 | ## Todo 391 | 392 | Find the CI you use on [https://on.cypress.io/continuous-integration](https://on.cypress.io/continuous-integration) and [https://github.com/cypress-io/cypress-example-kitchensink#ci-status](https://github.com/cypress-io/cypress-example-kitchensink#ci-status) 393 | 394 | --- 395 | 396 | ## 🏁 Cypress on CI 397 | 398 | - presentation [CircleCI Orbs vs GitHub Actions vs Netlify Build Plugins CI Setup](https://slides.com/bahmutov/ci-triple) 399 | - my [GitHub Actions blog posts](https://glebbahmutov.com/blog/tags/github/) 400 | - my [CircleCI blog posts](https://glebbahmutov.com/blog/tags/circle/) 401 | 402 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [08-retry-ability](?p=08-retry-ability) chapter 403 | -------------------------------------------------------------------------------- /slides/07-ci/img/add-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/07-ci/img/add-project.png -------------------------------------------------------------------------------- /slides/07-ci/img/cypress-cloud-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/07-ci/img/cypress-cloud-badge.png -------------------------------------------------------------------------------- /slides/07-ci/img/replay-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/07-ci/img/replay-button.png -------------------------------------------------------------------------------- /slides/07-ci/img/replay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/07-ci/img/replay.png -------------------------------------------------------------------------------- /slides/07-ci/img/workshop-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/07-ci/img/workshop-tests.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/alias-does-not-exist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/alias-does-not-exist.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/assertion-intellisense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/assertion-intellisense.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/bdd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/bdd.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/chai-intellisense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/chai-intellisense.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/one-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/one-label.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/retry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/retry.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/tdd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/tdd.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/test-retries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/test-retries.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/two-labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/two-labels.png -------------------------------------------------------------------------------- /slides/08-retry-ability/img/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/08-retry-ability/img/waiting.png -------------------------------------------------------------------------------- /slides/09-custom-commands/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Custom commands 2 | 3 | ### 📚 You will learn 4 | 5 | - adding new commands to `cy` 6 | - supporting retry-ability 7 | - TypeScript definition for new command 8 | - useful 3rd party commands 9 | 10 | +++ 11 | 12 | - keep `todomvc` app running 13 | - open `cypress/e2e/09-custom-commands/spec.js` 14 | 15 | --- 16 | 17 | ### 💯 Code reuse and clarity 18 | 19 | ```js 20 | // name the beforeEach functions 21 | beforeEach(function resetData() { 22 | cy.request('POST', '/reset', { 23 | todos: [] 24 | }) 25 | }) 26 | beforeEach(function visitSite() { 27 | cy.visit('/') 28 | }) 29 | ``` 30 | 31 | Note: 32 | Before each test we need to reset the server data and visit the page. The data clean up and opening the site could be a lot more complex that our simple example. We probably want to factor out `resetData` and `visitSite` into reusable functions every spec and test can use. 33 | 34 | --- 35 | 36 | ### Todo: move them into `cypress/support/e2e.js` 37 | 38 | Now these `beforeEach` hooks will be loaded _before every_ test in every spec. The test runner loads the spec files like this: 39 | 40 | ```html 41 | 42 | 43 | ``` 44 | 45 | Note: 46 | Is this a good solution? 47 | 48 | --- 49 | 50 | ## My opinion 51 | 52 | > Little reusable functions are the best 53 | 54 | ```js 55 | import { 56 | enterTodo, 57 | getTodoApp, 58 | getTodoItems, 59 | resetDatabase, 60 | visit 61 | } from '../../support/utils' 62 | beforeEach(() => { 63 | resetDatabase() 64 | visit() 65 | }) 66 | it('loads the app', () => { 67 | getTodoApp().should('be.visible') 68 | enterTodo('first item') 69 | enterTodo('second item') 70 | getTodoItems().should('have.length', 2) 71 | }) 72 | ``` 73 | 74 | Todo: look at the "cypress/support/utils.js" 75 | 76 | Note: 77 | Some functions can return `cy` instance, some don't, whatever is convenient. I also find small functions that return complex selectors very useful to keep selectors from duplication. 78 | 79 | +++ 80 | 81 | Pro: functions are easy to document with JSDoc 82 | 83 | ![JSDoc example](./img/jsdoc.png) 84 | 85 | +++ 86 | 87 | And then IntelliSense works immediately 88 | 89 | ![IntelliSense](./img/intellisense.jpeg) 90 | 91 | +++ 92 | 93 | And MS IntelliSense can understand types from JSDoc and check those! 94 | 95 | [https://github.com/Microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript](https://github.com/Microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript) 96 | 97 | More details in: [https://slides.com/bahmutov/ts-without-ts](https://slides.com/bahmutov/ts-without-ts) 98 | 99 | --- 100 | 101 | ## Use cases for custom commands 102 | 103 | - share code in entire project without individual imports 104 | - complex logic with custom logging into Command Log 105 | - login sequence 106 | - many application actions 107 | 108 | 📝 [on.cypress.io/custom-commands](https://on.cypress.io/custom-commands) and Read [https://glebbahmutov.com/blog/writing-custom-cypress-command/](https://glebbahmutov.com/blog/writing-custom-cypress-command/) 109 | 110 | +++ 111 | 112 | ## Custom commands and queries 113 | 114 | - add a custom command 115 | - add a custom query 116 | - overwrite a command 117 | - overwrite a query (v12.6.0+) 118 | 119 | --- 120 | 121 | Let's write a custom command to create a todo 122 | 123 | ```js 124 | // instead of this 125 | cy.get('.new-todo').type('todo 0{enter}') 126 | // use a custom command "createTodo" 127 | cy.createTodo('todo 0') 128 | ``` 129 | 130 | +++ 131 | 132 | ## Todo: write and use "createTodo" 133 | 134 | ```js 135 | Cypress.Commands.add('createTodo', (todo) => { 136 | cy.get('.new-todo').type(`${todo}{enter}`) 137 | }) 138 | it('creates a todo', () => { 139 | cy.createTodo('my first todo') 140 | }) 141 | ``` 142 | 143 | +++ 144 | 145 | ## ⬆️ Make it better 146 | 147 | - have IntelliSense working for `createTodo` 148 | - have nicer Command Log 149 | 150 | +++ 151 | 152 | ## Todo: add `createTodo` to `cy` object 153 | 154 | How: [https://github.com/cypress-io/cypress-example-todomvc#cypress-intellisense](https://github.com/cypress-io/cypress-example-todomvc#cypress-intellisense) 155 | 156 | +++ 157 | 158 | ⌨️ in file `cypress/e2e/09-custom-commands/custom-commands.d.ts` 159 | 160 | ```ts 161 | /// 162 | declare namespace Cypress { 163 | interface Chainable { 164 | /** 165 | * Creates one Todo using UI 166 | * @example 167 | * cy.createTodo('new item') 168 | */ 169 | createTodo(todo: string): Chainable 170 | } 171 | } 172 | ``` 173 | 174 | +++ 175 | 176 | Load the new definition file in `cypress/e2e/09-custom-commands/spec.js` 177 | 178 | ```js 179 | /// 180 | ``` 181 | 182 | +++ 183 | 184 | ![Custom command IntelliSense](./img/create-todo-intellisense.jpeg) 185 | 186 | More JSDoc examples: [https://slides.com/bahmutov/ts-without-ts](https://slides.com/bahmutov/ts-without-ts) 187 | 188 | Note: 189 | Editors other than VSCode might require work. 190 | 191 | +++ 192 | 193 | ## Better Command Log 194 | 195 | ```js 196 | Cypress.Commands.add('createTodo', (todo) => { 197 | cy.get('.new-todo', { log: false }).type(`${todo}{enter}`, { log: false }) 198 | cy.log('createTodo', todo) 199 | }) 200 | ``` 201 | 202 | +++ 203 | 204 | ## Even better Command Log 205 | 206 | ```js 207 | Cypress.Commands.add('createTodo', (todo) => { 208 | const cmd = Cypress.log({ 209 | name: 'create todo', 210 | message: todo, 211 | consoleProps() { 212 | return { 213 | 'Create Todo': todo 214 | } 215 | } 216 | }) 217 | cy.get('.new-todo', { log: false }).type(`${todo}{enter}`, { log: false }) 218 | }) 219 | ``` 220 | 221 | +++ 222 | 223 | ![createTodo log](./img/create-todo-log.png) 224 | 225 | --- 226 | 227 | ### Mark command completed 228 | 229 | ```js 230 | cy.get('.new-todo', { log: false }) 231 | .type(`${todo}{enter}`, { log: false }) 232 | .then(($el) => { 233 | cmd.set({ $el }).snapshot().end() 234 | }) 235 | ``` 236 | 237 | **Pro-tip:** you can have multiple command snapshots. 238 | 239 | --- 240 | 241 | ### Show result in the console 242 | 243 | ```js 244 | // result will get value when command ends 245 | let result 246 | const cmd = Cypress.log({ 247 | consoleProps() { 248 | return { result } 249 | } 250 | }) 251 | // custom logic then: 252 | .then((value) => { 253 | result = value 254 | cmd.end() 255 | }) 256 | ``` 257 | 258 | +++ 259 | 260 | ## 3rd party custom commands 261 | 262 | - [cypress-map](https://github.com/bahmutov/cypress-map) 👍👍👍 263 | - [cypress-real-events](https://github.com/dmtrKovalenko/cypress-real-events) 👍👍 264 | - [@bahmutov/cy-grep](https://github.com/bahmutov/cy-grep) 265 | - [cypress-recurse](https://github.com/bahmutov/cypress-recurse) 👍 266 | - [cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots) 267 | - [cypress-xpath](https://github.com/cypress-io/cypress-xpath) 🔻 268 | 269 | [on.cypress.io/plugins#custom-commands](https://on.cypress.io/plugins#custom-commands) 270 | 271 | --- 272 | 273 | ## Try `cypress-xpath` 274 | 275 | ```sh 276 | # already done in this repo 277 | npm install -D cypress-xpath 278 | ``` 279 | 280 | in `cypress/support/e2e.js` 281 | 282 | ```js 283 | require('cypress-xpath') 284 | ``` 285 | 286 | +++ 287 | 288 | With `cypress-xpath` 289 | 290 | ```js 291 | it('finds list items', () => { 292 | cy.xpath('//ul[@class="todo-list"]//li').should('have.length', 3) 293 | }) 294 | ``` 295 | 296 | --- 297 | 298 | ## Custom command with retries 299 | 300 | How does `xpath` command retry the assertions that follow it? 301 | 302 | ```js 303 | cy.xpath('...') // command 304 | .should('have.length', 3) // assertions 305 | ``` 306 | 307 | +++ 308 | 309 | ```js 310 | // use cy.verifyUpcomingAssertions 311 | const resolveValue = () => { 312 | return Cypress.Promise.try(getValue).then((value) => { 313 | return cy.verifyUpcomingAssertions(value, options, { 314 | onRetry: resolveValue 315 | }) 316 | }) 317 | } 318 | ``` 319 | 320 | --- 321 | 322 | ## Advanced concepts 323 | 324 | - parent vs child command 325 | - overwriting `cy` command 326 | 327 | [on.cypress.io/custom-commands](https://on.cypress.io/custom-commands), [https://www.cypress.io/blog/2018/12/20/element-coverage/](https://www.cypress.io/blog/2018/12/20/element-coverage/) 328 | 329 | +++ 330 | 331 | ## Example: overwrite `cy.type` 332 | 333 | ```js 334 | Cypress.Commands.overwrite('type', (type, $el, text, options) => { 335 | console.log($el) 336 | return type($el, text, options) 337 | }) 338 | ``` 339 | 340 | [https://www.cypress.io/blog/2018/12/20/element-coverage/](https://www.cypress.io/blog/2018/12/20/element-coverage/) 341 | 342 | --- 343 | 344 | ## Best practices 345 | 346 | - Making reusable function is often faster than writing a custom command 347 | - Know Cypress API to avoid writing what's already available 348 | 349 | Read [https://glebbahmutov.com/blog/writing-custom-cypress-command/](https://glebbahmutov.com/blog/writing-custom-cypress-command/) and [https://glebbahmutov.com/blog/publishing-cypress-command/](https://glebbahmutov.com/blog/publishing-cypress-command/) 350 | 351 | --- 352 | 353 | ## My Fav Plugins 354 | 355 | - https://cypresstips.substack.com/p/my-favorite-cypress-plugins 356 | - https://cypresstips.substack.com/p/my-favorite-cypress-plugins-part 357 | 358 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [10-component-testing](?p=10-component-testing) chapter 359 | -------------------------------------------------------------------------------- /slides/09-custom-commands/img/create-todo-intellisense.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/09-custom-commands/img/create-todo-intellisense.jpeg -------------------------------------------------------------------------------- /slides/09-custom-commands/img/create-todo-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/09-custom-commands/img/create-todo-log.png -------------------------------------------------------------------------------- /slides/09-custom-commands/img/intellisense.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/09-custom-commands/img/intellisense.jpeg -------------------------------------------------------------------------------- /slides/09-custom-commands/img/jsdoc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/09-custom-commands/img/jsdoc.png -------------------------------------------------------------------------------- /slides/09-custom-commands/img/to-match-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/09-custom-commands/img/to-match-snapshot.png -------------------------------------------------------------------------------- /slides/10-component-testing/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## ☀️ Component Testing 2 | 3 | ### 📚 You will learn 4 | 5 | - set up React component testing 6 | - write simple tests 7 | - run tests on CI 8 | 9 | 📝 [How Cypress Component Testing Was Born](https://glebbahmutov.com/blog/how-cypress-component-testing-was-born/) 10 | 11 | +++ 12 | 13 | - clone repo https://github.com/bahmutov/the-fuzzy-line 14 | - check out the branch "workshop" 15 | 16 | ``` 17 | $ npx degit bahmutov/the-fuzzy-line#workshop 18 | $ cd the-fuzzy-line 19 | $ npm install 20 | ``` 21 | 22 | +++ 23 | 24 | ## Setup 25 | 26 | ![Setup component testing](./img/setup-type.png) 27 | 28 | +++ 29 | 30 | There are several component testing placeholders in `src` folder 31 | 32 | - ⌨️ `src/components/Numbers.cy.js` 33 | - ⌨️ `src/components/Difficulty.cy.js` 34 | - ⌨️ `src/components/Overlay.cy.js` 35 | 36 | **Tip:** read https://on.cypress.io/component-testing 37 | 38 | --- 39 | 40 | ## 📝 Take away 41 | 42 | - Mount the component and use it as E2E web app 43 | 44 | ## More information 45 | 46 | - https://slides.com/bahmutov/the-fuzzy-line 47 | - https://slides.com/bahmutov/test-components-without-fear 48 | - https://cypress.tips/courses 49 | 50 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or jump to the [end](?p=end) chapter 51 | -------------------------------------------------------------------------------- /slides/10-component-testing/img/setup-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/10-component-testing/img/setup-type.png -------------------------------------------------------------------------------- /slides/end/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## 🔖 Workshop lessons 2 | 3 | - Write E2E tests to mimic user's actions 4 | - Set the initial state before each test 5 | 6 | +++ 7 | 8 | ## 🔖 Workshop lessons 9 | 10 | - Spy / stub API calls and application code 11 | 12 | +++ 13 | 14 | ## 🔖 Workshop lessons 15 | 16 | - Anything you can do from DevTools console, you can do from your Cypress tests 17 | 18 | +++ 19 | 20 | ## The End 🎉 21 | 22 | Thank you for learning E2E testing with [Cypress.io](https://www.cypress.io) 23 | 24 | - [https://docs.cypress.io/](https://docs.cypress.io/) and [https://on.cypress.io/discord](https://on.cypress.io/discord) 25 | - **My resources** [cypress.tips](https://cypress.tips) and [https://cypresstips.substack.com/](https://cypresstips.substack.com/), https://glebbahmutov.com/cypress-examples, https://www.youtube.com/glebbahmutov, https://glebbahmutov.com/discord 26 | -------------------------------------------------------------------------------- /slides/intro/PITCHME.md: -------------------------------------------------------------------------------- 1 | # Cypress Workshop: Basics 2 | 3 | - [github.com/bahmutov/cypress-workshop-basics](https://github.com/bahmutov/cypress-workshop-basics) 4 | 5 | Jump to: [00-start](?p=00-start), [01-basic](?p=01-basic), [02-adding-items](?p=02-adding-items), [03-selector-playground](?p=03-selector-playground), [04-reset-state](?p=04-reset-state), [05-network](?p=05-network), [06-app-data-store](?p=06-app-data-store), [07-ci](?p=07-ci), [08-retry-ability](?p=08-retry-ability), [09-custom-commands](?p=09-custom-commands), [10-component-testing](?p=10-component-testing), [end](?p=end) 6 | 7 | +++ 8 | 9 | ## Author: Gleb Bahmutov, PhD 10 | 11 | - Ex-VP of Engineering at Cypress 12 | - Ex-Distinguished Engineer at Cypress 13 | - actively using Cypress since 2016 14 | - [gleb.dev](https://gleb.dev) 15 | - [@bahmutov](https://twitter.com/bahmutov) 16 | - [https://glebbahmutov.com/blog/tags/cypress/](https://glebbahmutov.com/blog/tags/cypress/) 300+ Cypress blog posts 17 | - [https://www.youtube.com/glebbahmutov](https://www.youtube.com/glebbahmutov) 500+ Cypress videos 18 | - [cypress.tips](https://cypress.tips) with links, search, my courses 19 | - [Cypress Tips](https://cypresstips.substack.com/) monthly newsletter 20 | 21 | +++ 22 | 23 | [cypress.tips/courses](https://cypress.tips/courses) 24 | 25 | ![My Cypress courses](./img/courses.png) 26 | 27 | --- 28 | 29 | ## What we are going to cover 1/2 30 | 31 | - example TodoMVC 32 | - web app, data store, REST calls 33 | - basic page load test 34 | - selector playground 35 | - resetting state before the test 36 | - any questions 37 | 38 | +++ 39 | 40 | ## What we are going to cover 2/2 41 | 42 | - network spying and stubbing, fixtures 43 | - running E2E tests on CI / Test Replay 44 | - retry-ability and flake-free tests 45 | - custom commands 46 | - component testing 47 | - any questions 48 | 49 | --- 50 | 51 | ## Schedule 🕰 52 | 53 | - Unit 1: 08.30am - 10.00am ☕️ 54 | - Unit 2: 10.30am - 12.00pm 55 | - Lunch Break: 12.00pm to 1.00pm 56 | - Unit 3: 1.00pm - 2.30pm ☕️ 57 | - Unit 4: 3.00pm - 4.30pm 58 | - time for questions during the workshop and after each section 59 | 60 | +++ 61 | 62 | 63 | 64 | ## Poll 1 🗳️: have you used Cypress before? 65 | 66 | - This is my first time 67 | - Using for less than 1 month 👍 68 | - Using it for less than 1 year 👍👍 69 | - Using for longer than 1 year ❤️ 70 | - Using for longer than 2 years ❤️❤️ 71 | 72 | --- 73 | 74 | ## Poll 2 🗳️: have you used other E2E test runners? 75 | 76 | - Selenium / Webdriver 77 | - Protractor 78 | - TestCafe 79 | - Puppeteer / Playwright 80 | - Something else? 81 | 82 | --- 83 | 84 | ## Poll 3 🗳️: what unit testing tool do you use? 85 | 86 | - Jest 87 | - Mocha 88 | - Ava 89 | - Tap/Tape 90 | - `node:test` 91 | - Something else? 92 | 93 | --- 94 | 95 | ## Last poll 🗳️: Do you use TypeScript 96 | 97 | - no 98 | - a little 99 | - half of the time 100 | - all the time 101 | 102 | --- 103 | 104 | ## How efficient learning works 105 | 106 | 1. I explain and show 107 | 2. We do together 108 | 3. You do and I help 109 | 110 | **Tip:** this repository has everything to work through the test exercises. 111 | 112 | [bahmutov/cypress-workshop-basics](https://github.com/bahmutov/cypress-workshop-basics) 113 | 114 | +++ 115 | 116 | 🎉 If you can make all "cypress/e2e/.../spec.js" tests work, you know Cypress. 117 | 118 | Tip 💡: there are about 90+ tests to fill with code, see them with "npm run names" command by using "find-cypress-specs" 119 | 120 | --- 121 | 122 | ## Requirements 123 | 124 | You will need: 125 | 126 | - `git` to clone this repo 127 | - Node v16+ to install dependencies 128 | 129 | ```text 130 | git clone 131 | cd cypress-workshop-basics 132 | npm install 133 | ``` 134 | 135 | --- 136 | 137 | ## Repo organization 138 | 139 | - `/todomvc` is a web application we are going to test 140 | - all tests are in `cypress/e2e` folder 141 | - there are subfolders for exercises 142 | - `01-basic` 143 | - `02-adding-items` 144 | - `03-selector-playground` 145 | - `04-reset-state` 146 | - etc 147 | - keep application `todomvc` running! 148 | 149 | Note: 150 | We are going to keep the app running, while switching from spec to spec for each part. 151 | 152 | +++ 153 | 154 | ## `todomvc` 155 | 156 | Let us look at the application. 157 | 158 | - `cd todomvc` 159 | - `npm start` 160 | - `open localhost:3000` 161 | 162 | **important:** keep application running through the entire workshop! 163 | 164 | +++ 165 | 166 | It is a regular TodoMVC application. 167 | 168 | ![TodoMVC](./img/todomvc.png) 169 | 170 | +++ 171 | 172 | If you have Vue DevTools plugin 173 | 174 | ![With Vue DevTools](./img/vue-devtools.png) 175 | 176 | +++ 177 | 178 | Look at XHR when using the app 179 | 180 | ![Network](./img/network.png) 181 | 182 | +++ 183 | 184 | Look at `todomvc/index.html` - main app DOM structure 185 | 186 | ![DOM](./img/DOM.png) 187 | 188 | +++ 189 | 190 | Look at `todomvc/app.js` 191 | 192 | ![Application](./img/app.png) 193 | 194 | +++ 195 | 196 | ## Questions 197 | 198 | - what happens when you add a new Todo item? 199 | - how does it get to the server? 200 | - where does the server save it? 201 | - what happens on start up? 202 | 203 | Note: 204 | The students should open DevTools and look at XHR requests that go between the web application and the server. Also the students should find `todomvc/data.json` file with saved items. 205 | 206 | --- 207 | 208 | ![Application architecture](./img/vue-vuex-rest.png) 209 | 210 | Note: 211 | This app has been coded and described in this blog post [https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/](https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/) 212 | 213 | +++ 214 | 215 | This app has been coded and described in this blog post [https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/](https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/) 216 | 217 | --- 218 | 219 | ## End of introduction 220 | 221 | ➡️ Pick the [next section](https://github.com/bahmutov/cypress-workshop-basics#contents) or go to the [00-start](?p=00-start) chapter 222 | -------------------------------------------------------------------------------- /slides/intro/img/DOM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/DOM.png -------------------------------------------------------------------------------- /slides/intro/img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/app.png -------------------------------------------------------------------------------- /slides/intro/img/courses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/courses.png -------------------------------------------------------------------------------- /slides/intro/img/docs-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/docs-search.png -------------------------------------------------------------------------------- /slides/intro/img/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/network.png -------------------------------------------------------------------------------- /slides/intro/img/todomvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/todomvc.png -------------------------------------------------------------------------------- /slides/intro/img/vue-devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/vue-devtools.png -------------------------------------------------------------------------------- /slides/intro/img/vue-vuex-rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/slides/intro/img/vue-vuex-rest.png -------------------------------------------------------------------------------- /slides/quizes/PITCHME.md: -------------------------------------------------------------------------------- 1 | ## Q1: How old is Cypress test runner? 2 | 3 | - 👏 It just came out 4 | - ❤️ Less than 3 years old 5 | - 🎉 About 6 years old 6 | 7 | --- 8 | 9 | ## Q2: Cypress is using Selenium 10 | 11 | - 👍 Of course, what else could it be 12 | - 🎉 Nope, no Selenium was harmed when making Cypress 13 | 14 | --- 15 | 16 | ## Q3: Cypress needs Dashboard to work 17 | 18 | - 👍 Yes, you must subscribe to run tests 19 | - 🎉 You can subscribe if you want to record test results 20 | - ❤️ What is Dashboard? 21 | 22 | --- 23 | 24 | ## Q4: When studying Cypress your first move is: 25 | 26 | - 👍 Attend Gleb's workshop 27 | - 🎉 Start reading docs.cypress.io 28 | - ❤️ Buy an e-book about Cypress 29 | 30 | --- 31 | 32 | ## Q5: Cypress cy.visit command... 33 | 34 | - 👍 Renders the HTML page by loading it from disk 35 | - 👏 Visits the HTML page by requesting it from the server 36 | - 🎉 Validates the HTML page 37 | - ❤️ Can fail if the application is throwing an error 38 | 39 | --- 40 | 41 | ## Q6: My favorite Cypress command so far is: 42 | 43 | - 👍 `cy.visit` 44 | - 👏 `cy.contains` 45 | - 🎉 `cy.get` 46 | - ❤️ Every command is awesome 47 | 48 | **Tip:** [cypress.tips/which-cypress-command-are-you](https://cypress.tips/which-cypress-command-are-you) 49 | -------------------------------------------------------------------------------- /slides/style.css: -------------------------------------------------------------------------------- 1 | .half-image img { 2 | width: 40%; 3 | } 4 | -------------------------------------------------------------------------------- /todomvc/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Vue + Vuex + REST application 2 | 3 | ![Application organization](img/vue-vuex-rest.png) 4 | 5 | ## Script commands 6 | 7 | - `npm install` to install dependencies (or `npm ci` for modern installs) 8 | - `npm run reset:db` resets [data.json](data.json) to have empty list of todos 9 | 10 | Once NPM dependencies are installed, the application should work locally without WiFi. 11 | 12 | ## Delay 13 | 14 | You can delay the initial loading by adding to the URL `/?delay=`. This is useful to simulate application bootstrapping. 15 | -------------------------------------------------------------------------------- /todomvc/analytics.js: -------------------------------------------------------------------------------- 1 | // example analytics lib 2 | window.track = (eventName) => { 3 | console.log('tracking event "%s"', eventName) 4 | } 5 | window.addEventListener('load', () => { 6 | track('window.load') 7 | }) 8 | -------------------------------------------------------------------------------- /todomvc/app.js: -------------------------------------------------------------------------------- 1 | /* global Vue, Vuex, axios, track */ 2 | /* eslint-disable no-console */ 3 | /* eslint-disable-next-line */ 4 | const uri = window.location.search.substring(1) 5 | const params = new URLSearchParams(uri) 6 | const appStartDelay = parseFloat(params.get('appStartDelay') || '0') 7 | 8 | function appStart() { 9 | Vue.use(Vuex) 10 | 11 | function randomId() { 12 | return Math.random().toString().substr(2, 10) 13 | } 14 | 15 | /** 16 | * When adding new todo items, we can force the delay by using 17 | * the URL query parameter `addTodoDelay=`. 18 | */ 19 | let addTodoDelay = 0 20 | 21 | const store = new Vuex.Store({ 22 | state: { 23 | loading: false, 24 | todos: [], 25 | newTodo: '', 26 | delay: 0 27 | }, 28 | getters: { 29 | newTodo: (state) => state.newTodo, 30 | todos: (state) => state.todos, 31 | loading: (state) => state.loading 32 | }, 33 | mutations: { 34 | SET_DELAY(state, delay) { 35 | state.delay = delay 36 | }, 37 | SET_RENDER_DELAY(state, ms) { 38 | state.renderDelay = ms 39 | }, 40 | SET_LOADING(state, flag) { 41 | state.loading = flag 42 | if (flag === false) { 43 | // an easy way for the application to signal 44 | // that it is done loading 45 | document.body.classList.add('loaded') 46 | } 47 | }, 48 | SET_TODOS(state, todos) { 49 | state.todos = todos 50 | // expose the todos via the global "window" object 51 | // but only if we are running Cypress tests 52 | if (window.Cypress) { 53 | window.todos = todos 54 | } 55 | }, 56 | SET_NEW_TODO(state, todo) { 57 | state.newTodo = todo 58 | }, 59 | ADD_TODO(state, todoObject) { 60 | state.todos.push(todoObject) 61 | }, 62 | REMOVE_TODO(state, todo) { 63 | let todos = state.todos 64 | todos.splice(todos.indexOf(todo), 1) 65 | }, 66 | CLEAR_NEW_TODO(state) { 67 | state.newTodo = '' 68 | } 69 | }, 70 | actions: { 71 | setDelay({ commit }, delay) { 72 | commit('SET_DELAY', delay) 73 | }, 74 | setRenderDelay({ commit }, ms) { 75 | commit('SET_RENDER_DELAY', ms) 76 | }, 77 | 78 | loadTodos({ commit, state }) { 79 | console.log('loadTodos start, delay is %d', state.delay) 80 | setTimeout(() => { 81 | commit('SET_LOADING', true) 82 | 83 | axios 84 | .get('/todos') 85 | .then((r) => r.data) 86 | .then((todos) => { 87 | setTimeout(() => { 88 | commit('SET_TODOS', todos) 89 | }, state.renderDelay) 90 | }) 91 | .catch((e) => { 92 | console.error('could not load todos') 93 | console.error(e.message) 94 | console.error(e.response.data) 95 | }) 96 | .finally(() => { 97 | setTimeout(() => { 98 | commit('SET_LOADING', false) 99 | }, state.renderDelay) 100 | }) 101 | }, state.delay) 102 | }, 103 | 104 | /** 105 | * Sets text for the future todo 106 | * 107 | * @param {any} { commit } 108 | * @param {string} todo Message 109 | */ 110 | setNewTodo({ commit }, todo) { 111 | commit('SET_NEW_TODO', todo) 112 | }, 113 | addTodo({ commit, state }) { 114 | if (!state.newTodo) { 115 | // do not add empty todos 116 | return 117 | } 118 | const todo = { 119 | title: state.newTodo, 120 | completed: false, 121 | id: randomId() 122 | } 123 | // artificial delay in the application 124 | // for test "flaky test - can pass or not depending on the app's speed" 125 | // in cypress/integration/08-retry-ability/answer.js 126 | // increase the timeout delay to make the test fail 127 | // 50ms should be good 128 | setTimeout(() => { 129 | track('todo.add', todo.title) 130 | axios.post('/todos', todo).then(() => { 131 | commit('ADD_TODO', todo) 132 | }) 133 | }, addTodoDelay) 134 | }, 135 | addEntireTodo({ commit }, todoFields) { 136 | const todo = { 137 | ...todoFields, 138 | id: randomId() 139 | } 140 | axios.post('/todos', todo).then(() => { 141 | commit('ADD_TODO', todo) 142 | }) 143 | }, 144 | removeTodo({ commit }, todo) { 145 | track('todo.remove', todo.title) 146 | 147 | axios.delete(`/todos/${todo.id}`).then(() => { 148 | console.log('removed todo', todo.id, 'from the server') 149 | commit('REMOVE_TODO', todo) 150 | }) 151 | }, 152 | async removeCompleted({ commit, state }) { 153 | const remainingTodos = state.todos.filter((todo) => !todo.completed) 154 | const completedTodos = state.todos.filter((todo) => todo.completed) 155 | 156 | for (const todo of completedTodos) { 157 | await axios.delete(`/todos/${todo.id}`) 158 | } 159 | commit('SET_TODOS', remainingTodos) 160 | }, 161 | clearNewTodo({ commit }) { 162 | commit('CLEAR_NEW_TODO') 163 | }, 164 | // example promise-returning action 165 | addTodoAfterDelay({ commit }, { milliseconds, title }) { 166 | return new Promise((resolve) => { 167 | setTimeout(() => { 168 | const todo = { 169 | title, 170 | completed: false, 171 | id: randomId() 172 | } 173 | commit('ADD_TODO', todo) 174 | resolve() 175 | }, milliseconds) 176 | }) 177 | } 178 | } 179 | }) 180 | 181 | // a few helper utilities 182 | const filters = { 183 | all: function (todos) { 184 | return todos 185 | }, 186 | active: function (todos) { 187 | return todos.filter(function (todo) { 188 | return !todo.completed 189 | }) 190 | }, 191 | completed: function (todos) { 192 | return todos.filter(function (todo) { 193 | return todo.completed 194 | }) 195 | } 196 | } 197 | 198 | // app Vue instance 199 | const app = new Vue({ 200 | store, 201 | data: { 202 | file: null, 203 | visibility: 'all' 204 | }, 205 | el: '.todoapp', 206 | 207 | created() { 208 | const delay = parseFloat(params.get('delay') || '0') 209 | const renderDelay = parseFloat(params.get('renderDelay') || '0') 210 | addTodoDelay = parseFloat(params.get('addTodoDelay') || '0') 211 | 212 | this.$store.dispatch('setRenderDelay', renderDelay).then(() => { 213 | this.$store.dispatch('setDelay', delay).then(() => { 214 | this.$store.dispatch('loadTodos') 215 | }) 216 | }) 217 | 218 | // how would you test the periodic loading of todos? 219 | setInterval(() => { 220 | this.$store.dispatch('loadTodos') 221 | }, 60000) 222 | }, 223 | 224 | // computed properties 225 | // https://vuejs.org/guide/computed.html 226 | computed: { 227 | loading() { 228 | return this.$store.getters.loading 229 | }, 230 | newTodo() { 231 | return this.$store.getters.newTodo 232 | }, 233 | todos() { 234 | return this.$store.getters.todos 235 | }, 236 | filteredTodos() { 237 | return filters[this.visibility](this.$store.getters.todos) 238 | }, 239 | remaining() { 240 | return this.$store.getters.todos.filter((todo) => !todo.completed) 241 | .length 242 | } 243 | }, 244 | 245 | // methods that implement data logic. 246 | // note there's no DOM manipulation here at all. 247 | methods: { 248 | pluralize: function (word, count) { 249 | return word + (count === 1 ? '' : 's') 250 | }, 251 | 252 | setNewTodo(e) { 253 | this.$store.dispatch('setNewTodo', e.target.value) 254 | }, 255 | 256 | addTodo(e) { 257 | // do not allow adding empty todos 258 | if (!e.target.value.trim()) { 259 | throw new Error('Cannot add a blank todo') 260 | } 261 | e.target.value = '' 262 | this.$store.dispatch('addTodo') 263 | this.$store.dispatch('clearNewTodo') 264 | }, 265 | 266 | removeTodo(todo) { 267 | this.$store.dispatch('removeTodo', todo) 268 | }, 269 | 270 | // utility method for create a todo with title and completed state 271 | addEntireTodo(title, completed = false) { 272 | this.$store.dispatch('addEntireTodo', { title, completed }) 273 | }, 274 | 275 | removeCompleted() { 276 | this.$store.dispatch('removeCompleted') 277 | } 278 | } 279 | }) 280 | 281 | // use the Router from the vendor/director.js library 282 | ;(function (app, Router) { 283 | 'use strict' 284 | 285 | var router = new Router() 286 | 287 | ;['all', 'active', 'completed'].forEach(function (visibility) { 288 | router.on(visibility, function () { 289 | app.visibility = visibility 290 | }) 291 | }) 292 | 293 | router.configure({ 294 | notfound: function () { 295 | window.location.hash = '' 296 | app.visibility = 'all' 297 | } 298 | }) 299 | 300 | router.init() 301 | })(app, Router) 302 | 303 | // if you want to expose "app" globally only 304 | // during end-to-end tests you can guard it using "window.Cypress" flag 305 | // if (window.Cypress) { 306 | window.app = app 307 | // } 308 | } 309 | 310 | if (appStartDelay > 0) { 311 | setTimeout(appStart, appStartDelay) 312 | } else { 313 | appStart() 314 | } 315 | -------------------------------------------------------------------------------- /todomvc/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [] 3 | } -------------------------------------------------------------------------------- /todomvc/img/DOM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/DOM.png -------------------------------------------------------------------------------- /todomvc/img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/app.png -------------------------------------------------------------------------------- /todomvc/img/docs-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/docs-search.png -------------------------------------------------------------------------------- /todomvc/img/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/network.png -------------------------------------------------------------------------------- /todomvc/img/todomvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/todomvc.png -------------------------------------------------------------------------------- /todomvc/img/vue-devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/vue-devtools.png -------------------------------------------------------------------------------- /todomvc/img/vue-vuex-rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-workshop-basics/7371bf0a135b3b431c9f795e9bc16dc8331d0c96/todomvc/img/vue-vuex-rest.png -------------------------------------------------------------------------------- /todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue.js • TodoMVC 8 | 9 | 13 | 15 | 24 | 39 | 40 | 41 | 42 |
43 |
44 |

todos

45 | 54 |
55 |
56 |
    57 |
  • 63 |
    64 | 65 | 66 | 67 |
    68 |
  • 69 |
70 |
71 |
72 |
Loading data ...
73 |
74 | 75 |
76 | 77 | 78 | {{pluralize('item', remaining)}} left 79 | 80 | 106 | 113 |
114 |
115 |
116 |

117 | Written by 118 | Evan You 119 |

120 |

121 | Part of 122 | TodoMVC 123 |

124 |
125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-example-vue-vuex-rest", 3 | "version": "1.0.0", 4 | "description": "Testing Vue + Vuex + REST TodoMVC using Cypress", 5 | "scripts": { 6 | "start": "json-server --static . --watch data.json --middlewares ./node_modules/json-server-reset", 7 | "reset": "node reset-db.js", 8 | "reset:db": "npm run reset", 9 | "reset:database": "npm run reset" 10 | }, 11 | "dependencies": { 12 | "json-server": "0.17.4", 13 | "json-server-reset": "1.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /todomvc/reset-db.js: -------------------------------------------------------------------------------- 1 | const write = require('fs').writeFileSync 2 | 3 | const resetDatabase = () => { 4 | // for complex resets can use NPM script command 5 | // cy.exec('npm run reset:database') 6 | 7 | // for simple cases, can just overwrite the data file 8 | const data = { 9 | todos: [] 10 | } 11 | const str = JSON.stringify(data, null, 2) + '\n' 12 | write('./data.json', str) 13 | } 14 | 15 | resetDatabase() 16 | -------------------------------------------------------------------------------- /todomvc/vendor/dark.css: -------------------------------------------------------------------------------- 1 | /* a simple todo app dark theme that overwrites standard colors */ 2 | body, 3 | .todoapp { 4 | color: #ddd; 5 | background-color: #222; 6 | } 7 | .todoapp h1 { 8 | color: #b83f45; 9 | } 10 | .info { 11 | text-shadow: none; 12 | } 13 | 14 | .new-todo, 15 | .edit { 16 | /* border: 1px solid #999; */ 17 | box-shadow: inset 0 -1px 5px 0 rgba(255, 255, 255, 0.2); 18 | } 19 | 20 | .main { 21 | border-top: none; 22 | } 23 | 24 | .todoapp input::-webkit-input-placeholder { 25 | color: #bdbdbd; 26 | } 27 | 28 | .todoapp input::-moz-placeholder { 29 | color: #bdbdbd; 30 | } 31 | 32 | .todoapp input::input-placeholder { 33 | color: #bdbdbd; 34 | } 35 | 36 | .todo-list li { 37 | border-bottom: 1px solid #8e8e8e; 38 | } 39 | 40 | .todo-list li.completed label { 41 | color: #888888; 42 | } 43 | 44 | .info { 45 | color: #ddd; 46 | } 47 | -------------------------------------------------------------------------------- /todomvc/vendor/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | TodoMVC css from https://github.com/cypress-io/todomvc-app-css fork 3 | with improved contrast. You can install it using 4 | 5 | npm i -S cypress-io/todomvc-app-css#a9d4ea1 6 | 7 | but to keep this app simple, vendor files are checked in 8 | */ 9 | html, 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | button { 16 | margin: 0; 17 | padding: 0; 18 | border: 0; 19 | background: none; 20 | font-size: 100%; 21 | vertical-align: baseline; 22 | font-family: inherit; 23 | font-weight: inherit; 24 | color: inherit; 25 | -webkit-appearance: none; 26 | appearance: none; 27 | -webkit-font-smoothing: antialiased; 28 | -moz-osx-font-smoothing: grayscale; 29 | } 30 | 31 | body { 32 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 33 | line-height: 1.4em; 34 | background: #f5f5f5; 35 | color: #4d4d4d; 36 | min-width: 230px; 37 | max-width: 550px; 38 | margin: 0 auto; 39 | -webkit-font-smoothing: antialiased; 40 | -moz-osx-font-smoothing: grayscale; 41 | font-weight: 300; 42 | } 43 | 44 | :focus { 45 | outline: 0; 46 | } 47 | 48 | .hidden { 49 | display: none; 50 | } 51 | 52 | .todoapp { 53 | background: #fff; 54 | margin: 130px 0 40px 0; 55 | position: relative; 56 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 57 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 58 | } 59 | 60 | .todoapp input::-webkit-input-placeholder { 61 | font-style: italic; 62 | font-weight: 300; 63 | color: #111111; 64 | } 65 | 66 | .todoapp input::-moz-placeholder { 67 | font-style: italic; 68 | font-weight: 300; 69 | color: #111111; 70 | } 71 | 72 | .todoapp input::input-placeholder { 73 | font-style: italic; 74 | font-weight: 300; 75 | color: #111111; 76 | } 77 | 78 | .todoapp h1 { 79 | position: absolute; 80 | top: -155px; 81 | width: 100%; 82 | font-size: 100px; 83 | font-weight: 100; 84 | text-align: center; 85 | color: #b83f45; 86 | -webkit-text-rendering: optimizeLegibility; 87 | -moz-text-rendering: optimizeLegibility; 88 | text-rendering: optimizeLegibility; 89 | } 90 | 91 | .new-todo, 92 | .edit { 93 | position: relative; 94 | margin: 0; 95 | width: 100%; 96 | font-size: 24px; 97 | font-family: inherit; 98 | font-weight: inherit; 99 | line-height: 1.4em; 100 | color: inherit; 101 | padding: 6px; 102 | border: 1px solid #999; 103 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 104 | box-sizing: border-box; 105 | -webkit-font-smoothing: antialiased; 106 | -moz-osx-font-smoothing: grayscale; 107 | } 108 | 109 | .new-todo { 110 | padding: 16px 16px 16px 60px; 111 | border: none; 112 | background: rgba(0, 0, 0, 0.003); 113 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 114 | } 115 | 116 | .main { 117 | position: relative; 118 | z-index: 2; 119 | border-top: 1px solid #111111; 120 | } 121 | 122 | .toggle-all { 123 | width: 1px; 124 | height: 1px; 125 | border: none; /* Mobile Safari */ 126 | opacity: 0; 127 | position: absolute; 128 | right: 100%; 129 | bottom: 100%; 130 | } 131 | 132 | .toggle-all + label { 133 | width: 60px; 134 | height: 34px; 135 | font-size: 0; 136 | position: absolute; 137 | top: -52px; 138 | left: -13px; 139 | -webkit-transform: rotate(90deg); 140 | transform: rotate(90deg); 141 | } 142 | 143 | .toggle-all + label:before { 144 | content: '❯'; 145 | font-size: 22px; 146 | color: #111111; 147 | padding: 10px 27px 10px 27px; 148 | } 149 | 150 | .toggle-all:checked + label:before { 151 | color: #737373; 152 | } 153 | 154 | .todo-list { 155 | margin: 0; 156 | padding: 0; 157 | list-style: none; 158 | } 159 | 160 | .todo-list li { 161 | position: relative; 162 | font-size: 24px; 163 | border-bottom: 1px solid #ededed; 164 | } 165 | 166 | .todo-list li:last-child { 167 | border-bottom: none; 168 | } 169 | 170 | .todo-list li.editing { 171 | border-bottom: none; 172 | padding: 0; 173 | } 174 | 175 | .todo-list li.editing .edit { 176 | display: block; 177 | width: calc(100% - 43px); 178 | padding: 12px 16px; 179 | margin: 0 0 0 43px; 180 | } 181 | 182 | .todo-list li.editing .view { 183 | display: none; 184 | } 185 | 186 | .todo-list li .toggle { 187 | text-align: center; 188 | width: 40px; 189 | /* auto, since non-WebKit browsers doesn't support input styling */ 190 | height: auto; 191 | position: absolute; 192 | top: 0; 193 | bottom: 0; 194 | margin: auto 0; 195 | border: none; /* Mobile Safari */ 196 | -webkit-appearance: none; 197 | appearance: none; 198 | } 199 | 200 | .todo-list li .toggle { 201 | opacity: 0; 202 | } 203 | 204 | .todo-list li .toggle + label { 205 | /* 206 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 207 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 208 | */ 209 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 210 | background-repeat: no-repeat; 211 | background-position: center left; 212 | } 213 | 214 | .todo-list li .toggle:checked + label { 215 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 216 | } 217 | 218 | .todo-list li label { 219 | word-break: break-all; 220 | padding: 15px 15px 15px 60px; 221 | display: block; 222 | line-height: 1.2; 223 | transition: color 0.4s; 224 | } 225 | 226 | .todo-list li.completed label { 227 | color: #d9d9d9; 228 | text-decoration: line-through; 229 | } 230 | 231 | .todo-list li .destroy { 232 | display: none; 233 | position: absolute; 234 | top: 0; 235 | right: 10px; 236 | bottom: 0; 237 | width: 40px; 238 | height: 40px; 239 | margin: auto 0; 240 | font-size: 30px; 241 | color: #cc9a9a; 242 | margin-bottom: 11px; 243 | transition: color 0.2s ease-out; 244 | font-weight: 600; 245 | } 246 | 247 | .todo-list li .destroy:hover { 248 | color: #ff0101; 249 | } 250 | 251 | .todo-list li .destroy:after { 252 | content: '×'; 253 | } 254 | 255 | .todo-list li:hover .destroy { 256 | display: block; 257 | } 258 | 259 | .todo-list li .edit { 260 | display: none; 261 | } 262 | 263 | .todo-list li.editing:last-child { 264 | margin-bottom: -1px; 265 | } 266 | 267 | .footer { 268 | padding: 10px 15px; 269 | height: 20px; 270 | text-align: center; 271 | border-top: 1px solid #111111; 272 | font-weight: 400; 273 | } 274 | 275 | .footer:before { 276 | content: ''; 277 | position: absolute; 278 | right: 0; 279 | bottom: 0; 280 | left: 0; 281 | height: 50px; 282 | overflow: hidden; 283 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 284 | 0 8px 0 -3px #f6f6f6, 285 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 286 | 0 16px 0 -6px #f6f6f6, 287 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 288 | } 289 | 290 | .todo-count { 291 | float: left; 292 | text-align: left; 293 | } 294 | 295 | .todo-count strong { 296 | font-weight: 800; 297 | } 298 | 299 | .filters { 300 | margin: 0; 301 | padding: 0; 302 | list-style: none; 303 | position: absolute; 304 | right: 0; 305 | left: 0; 306 | } 307 | 308 | .filters li { 309 | display: inline; 310 | } 311 | 312 | .filters li a { 313 | color: inherit; 314 | margin: 3px; 315 | padding: 3px 7px; 316 | text-decoration: none; 317 | border: 1px solid transparent; 318 | border-radius: 3px; 319 | } 320 | 321 | .filters li a:hover { 322 | border-color: rgba(175, 47, 47, 0.1); 323 | } 324 | 325 | .filters li a.selected { 326 | border-color: rgba(175, 47, 47, 0.2); 327 | } 328 | 329 | .clear-completed, 330 | html .clear-completed:active { 331 | float: right; 332 | position: relative; 333 | line-height: 20px; 334 | text-decoration: none; 335 | cursor: pointer; 336 | } 337 | 338 | .clear-completed:hover { 339 | text-decoration: underline; 340 | } 341 | 342 | .info { 343 | margin: 65px auto 0; 344 | color: #4d4d4d; 345 | font-size: 10px; 346 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 347 | text-align: center; 348 | } 349 | 350 | .info p { 351 | line-height: 1; 352 | } 353 | 354 | .info a { 355 | color: inherit; 356 | text-decoration: none; 357 | font-weight: 400; 358 | } 359 | 360 | .info a:hover { 361 | text-decoration: underline; 362 | } 363 | 364 | /* 365 | Hack to remove background from Mobile Safari. 366 | Can't use it globally since it destroys checkboxes in Firefox 367 | */ 368 | @media screen and (-webkit-min-device-pixel-ratio:0) { 369 | .toggle-all, 370 | .todo-list li .toggle { 371 | background: none; 372 | } 373 | 374 | .todo-list li .toggle { 375 | height: 40px; 376 | } 377 | } 378 | 379 | @media (max-width: 430px) { 380 | .footer { 381 | height: 50px; 382 | } 383 | 384 | .filters { 385 | bottom: 10px; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | publicDir: 'slides', 3 | } 4 | --------------------------------------------------------------------------------