├── .github └── workflows │ └── run-cypress.yml ├── .gitignore ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── authentication.feature │ ├── general.feature │ └── login.feature ├── plugins │ └── index.js └── support │ └── step_definitions │ ├── authentication.js │ ├── forms.js │ ├── headers.js │ └── navigation.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── reset.css └── serviceWorker.js ├── talk ├── using-gwt-with-cypress-raw.md └── using-gwt-with-cypress.pdf └── yarn.lock /.github/workflows/run-cypress.yml: -------------------------------------------------------------------------------- 1 | name: Install, Build & Run Cypress 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: macos-latest 8 | strategy: 9 | matrix: 10 | node-version: [12.x] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - name: Install Packages 17 | run: yarn 18 | - name: Build 19 | run: yarn build 20 | - name: Run Cypress 21 | run: yarn test:ci 22 | env: 23 | CYPRESS_DASHBOARD_KEY: ${{ secrets.CYPRESS_DASHBOARD_KEY }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # csilk Gitignore default # 2 | ########################### 3 | 4 | .env 5 | build 6 | cypress/videos 7 | 8 | # Editors # 9 | ########### 10 | .vscode 11 | .idea 12 | *.iml 13 | .project 14 | .gradle/ 15 | /nbproject/private/ 16 | 17 | # Compiled source # 18 | ################### 19 | 20 | dist 21 | *.com 22 | *.class 23 | *.dll 24 | *.exe 25 | *.o 26 | *.so 27 | 28 | # Packages # 29 | ############ 30 | 31 | *.7z 32 | *.dmg 33 | *.gz 34 | *.iso 35 | *.jar 36 | *.rar 37 | *.tar 38 | *.zip 39 | .vs 40 | *.msi 41 | *.nupkg 42 | 43 | # Logs and databases # 44 | ###################### 45 | 46 | *.log 47 | *.sql 48 | *.sqlite 49 | npm-debug.log* 50 | coverage 51 | junit.xml 52 | 53 | # Caches # 54 | ########## 55 | 56 | .eslintcache 57 | eslint-data 58 | /tmp/ 59 | /.cache-loader 60 | 61 | # Node # 62 | ######## 63 | lib-cov 64 | *.seed 65 | *.log 66 | *.dat 67 | *.out 68 | *.pid 69 | *.gz 70 | pids 71 | logs 72 | npm-debug.log 73 | node_modules/ 74 | .pnp.js 75 | .pnp 76 | .yarn-meta 77 | /packages/lockfile/index.js 78 | /.nyc_output 79 | 80 | # OS generated files # 81 | ###################### 82 | 83 | .DS_Store 84 | .DS_Store? 85 | ._* 86 | .Spotlight-V100 87 | .Trashes 88 | ehthumbs.db 89 | Thumbs.db 90 | debug.log 91 | .settings 92 | .settings/* 93 | .buildpath 94 | *.iml 95 | sftp-config* 96 | *.sublime-* 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Callum Silcock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 cypress-gwt-example 2 | 3 | > Example repo showing how to use Cypress with Cucumber Feature files 4 | 5 | A companion to [Callum Silcock's](https://csi.lk) talk on "How to get your product owners to write your functional tests" this repo gives you some simple examples on how to connect Cypress with Cucumber and test an app 6 | 7 | ## 🙊 The Talk 8 | 9 | If you missed the talk check out: 10 | 11 | - [Speakerdeck - External (slides)](https://speakerdeck.com/csilk/then-with-cypress) 12 | - [PDF - Here (slides)](./talk/using-gwt-with-cypress.pdf) 13 | - [Raw markdown - Here (notes + slides)](./talk/using-gwt-with-cypress-raw.md) 14 | 15 | ## 😎 How It Works 16 | 17 | Using [cypress](https://www.cypress.io) and the [cypress-cucumber-preprocessor plugin](https://github.com/TheBrainFamily/cypress-cucumber-preprocessor) we setup `.feature` files in the [integration folder](https://github.com/csi-lk/cypress-gwt-example/tree/master/cypress/integration) that reads the step definitions in the [step_definitions folder](https://github.com/csi-lk/cypress-gwt-example/tree/master/cypress/support/step_definitions) to generate our Cypress tests. 18 | 19 | These then test our served [App](https://github.com/csi-lk/cypress-gwt-example/blob/master/src/App.js) 20 | 21 | ## 🔥Running Locally 22 | 23 | ```sh 24 | # Clone me 25 | git clone git@github.com:csi-lk/cypress-gwt-example.git 26 | ``` 27 | 28 | ```sh 29 | # Install dependencies 30 | yarn 31 | ``` 32 | 33 | ```sh 34 | # Build the app 35 | yarn build 36 | ``` 37 | 38 | ```sh 39 | # Open Cypress 40 | yarn test 41 | ``` 42 | 43 | ```sh 44 | # Run Cypress in headless mode 🤯 45 | yarn test:ci 46 | ``` 47 | 48 | ## 🛂 CI 49 | 50 | Have setup a [workflow](https://github.com/csi-lk/cypress-gwt-example/blob/master/.github/workflows/run-cypress.yml) in the [Actions](https://github.com/csi-lk/cypress-gwt-example/actions) tab above that should give you a good idea of how it operates in a CI environment 51 | 52 | If you're interested, I've also connected the [cypress dashboard](https://dashboard.cypress.io/#/projects/syj7cg/runs) so you can check also check the videos of it in action. 53 | 54 | --- 55 | 56 | If you have any questions please [raise an issue](https://github.com/csi-lk/cypress-gwt-example/issues/new) above 57 | 58 | <3 [Callum Silcock](https://csi.lk) 59 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "testFiles": "**/*.feature", 3 | "projectId": "syj7cg" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/authentication.feature: -------------------------------------------------------------------------------- 1 | Feature: Authentication 2 | 3 | Scenario: Can't access protected routes 4 | Given I "am not" logged in 5 | When I navigate to the "dashboard" page 6 | Then I should be on the "home" page 7 | 8 | Scenario: Should be able to log out 9 | Given I "am" logged in 10 | When I click the "navigation logout" button 11 | Then I should be on the "home" page 12 | And the "navigation logout" button should "not" "exist" -------------------------------------------------------------------------------- /cypress/integration/general.feature: -------------------------------------------------------------------------------- 1 | Feature: General 2 | 3 | General scenarios 4 | 5 | Scenario: Opening the homepage 6 | Given I am on the "home" page 7 | Then I should see the "Cypress GWT Example" title -------------------------------------------------------------------------------- /cypress/integration/login.feature: -------------------------------------------------------------------------------- 1 | Feature: Logging In 2 | 3 | Tests the user can successfully login and log out 4 | 5 | Scenario: Logging In Sucessfully 6 | Given I am on the "login" page 7 | When I input my "email" as "test@test.com" 8 | And I input my "password" as "password" 9 | And I click the "login" button 10 | Then I should be on the "dashboard" page -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const cucumber = require("cypress-cucumber-preprocessor").default 2 | 3 | module.exports = on => { 4 | on("file:preprocessor", cucumber()) 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/step_definitions/authentication.js: -------------------------------------------------------------------------------- 1 | import { Given } from "cypress-cucumber-preprocessor/steps" 2 | import { navigateUrl } from "./navigation" 3 | import { inputString, clickButton } from "./forms" 4 | 5 | Given(`I {string} logged in`, status => { 6 | if (status === "am not") return navigateUrl("home") 7 | // Can set a cookie here for eg, don't need to fill out manually each time 8 | navigateUrl("login") 9 | inputString("email", "test@test.com") 10 | inputString("password", "password") 11 | clickButton("login") 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/support/step_definitions/forms.js: -------------------------------------------------------------------------------- 1 | import { When } from "cypress-cucumber-preprocessor/steps" 2 | 3 | // Inputs 4 | 5 | When(`I input my {string} as {string}`, (id, input) => inputString(id, input)) 6 | 7 | export function inputString(id, input) { 8 | dataQa("input", id).type(input) 9 | } 10 | 11 | // Buttons 12 | 13 | When(`I click the {string} button`, id => clickButton(id)) 14 | When(`the {string} button should {string} {string}`, (id, assert, assertion) => 15 | assertButton(id, assert, assertion) 16 | ) 17 | 18 | export function clickButton(id) { 19 | dataQa("button", id).click() 20 | } 21 | 22 | // Complex assertion example 23 | export function assertButton(id, assert, assertion) { 24 | dataQa("button", id).should(assert, assertion) 25 | //And the "logout" button "should say" "login" 26 | } 27 | 28 | // Utility 29 | // As your tests grow and get more complex these should be moved to helpers / library functions 30 | 31 | export function dataQa(type, id) { 32 | return cy.get(`[data-qa="${type}-${id.replace(" ", "-").toLowerCase()}"]`) 33 | } 34 | -------------------------------------------------------------------------------- /cypress/support/step_definitions/headers.js: -------------------------------------------------------------------------------- 1 | import { Then } from "cypress-cucumber-preprocessor/steps" 2 | 3 | Then(`I should see the {string} title`, title => { 4 | cy.get("h1").should("be", title) 5 | }) 6 | -------------------------------------------------------------------------------- /cypress/support/step_definitions/navigation.js: -------------------------------------------------------------------------------- 1 | import { Given, When, Then } from "cypress-cucumber-preprocessor/steps" 2 | 3 | const localUrl = "http://localhost:3000/" 4 | 5 | // Navigation 6 | Given(`I am on the {string} page`, target => navigateUrl(target)) 7 | When(`I navigate to the {string} page`, target => navigateUrl(target)) 8 | 9 | export function navigateUrl(target) { 10 | cy.visit(`${localUrl}${target.toLowerCase()}`) 11 | } 12 | 13 | // Assertions 14 | Then(`I should be on the {string} page`, target => assertUrl(target)) 15 | 16 | export function assertUrl(target) { 17 | cy.url().should("be", `${localUrl}${target}`) //TODO: this assertion wrong, need to read the docs 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-gwt-example", 3 | "description": "Example repo showing how to use Cypress with Cucumber Feature files", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "yarn cy:open", 9 | "test:ci": "yarn start-server-and-test start 3000 cy:run", 10 | "cy:open": "yarn cypress open", 11 | "cy:run": "yarn cypress run --record --key $CYPRESS_DASHBOARD_KEY" 12 | }, 13 | "dependencies": { 14 | "fsevents": "^2.0.7", 15 | "react": "^16.9.0", 16 | "react-dom": "^16.9.0", 17 | "react-router-dom": "^5.0.1", 18 | "react-scripts": "^3.1.2" 19 | }, 20 | "devDependencies": { 21 | "cypress": "^3.4.1", 22 | "cypress-cucumber-preprocessor": "^1.16.0", 23 | "start-server-and-test": "^1.10.2" 24 | }, 25 | "resolutions": { 26 | "eslint-loader": "3.0.2" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app", 30 | "globals": { 31 | "cy": true 32 | } 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csi-lk/cypress-gwt-example/7b028d1e998f00b0bcc531664df234470cb6d86e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Cypress GWT Example 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csi-lk/cypress-gwt-example/7b028d1e998f00b0bcc531664df234470cb6d86e/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csi-lk/cypress-gwt-example/7b028d1e998f00b0bcc531664df234470cb6d86e/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | text-align: center; 3 | } 4 | 5 | .app nav { 6 | display: flex; 7 | align-content: center; 8 | border-bottom: 2px solid #d6d6d6; 9 | margin-bottom: 2em; 10 | } 11 | 12 | .app nav > * { 13 | flex: 1 auto; 14 | padding: 10px 15px; 15 | text-decoration: none; 16 | color: black; 17 | } 18 | 19 | .app nav button { 20 | flex: 0; 21 | min-width: 10em; 22 | } 23 | 24 | .app h1 { 25 | font: 2.5em/1.6 sans-serif; 26 | letter-spacing: -0.05em; 27 | } 28 | 29 | .app h2 { 30 | font: 1.2em/1.6 sans-serif; 31 | letter-spacing: -0.05em; 32 | margin-bottom: 1em; 33 | } 34 | 35 | .form { 36 | display: flex; 37 | flex-direction: column; 38 | max-width: 60vw; 39 | margin: 0 auto; 40 | } 41 | 42 | .form > * { 43 | flex: 1 auto; 44 | margin-bottom: 2em; 45 | } 46 | 47 | input { 48 | padding: 10px 15px; 49 | font: 1em/1.6 sans-serif; 50 | border: 1px solid #d6d6d6; 51 | } 52 | 53 | button { 54 | padding: 10px 15px; 55 | font: 1em/1.6 sans-serif; 56 | background-color: #d6d6d6; 57 | } 58 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Link, 6 | Redirect, 7 | withRouter 8 | } from "react-router-dom" 9 | import "./App.css" 10 | 11 | const AuthButton = withRouter( 12 | ({ history, isAuthenticated, setAuthentication }) => 13 | isAuthenticated ? ( 14 | 23 | ) : ( 24 | 32 | ) 33 | ) 34 | 35 | function PrivateRoute({ isAuthenticated, component: Component, ...rest }) { 36 | return ( 37 | 40 | isAuthenticated ? ( 41 | 42 | ) : ( 43 | 49 | ) 50 | } 51 | /> 52 | ) 53 | } 54 | 55 | const Login = withRouter(({ history, setAuthentication }) => ( 56 |
57 |

Please login to continue

58 |

Requires no credentials, just click log in

59 | 65 | 71 | 80 |
81 | )) 82 | 83 | const Navigation = ({ isAuthenticated, setAuthentication }) => ( 84 | 104 | ) 105 | 106 | const Home = () => ( 107 |
108 |

Cypress GWT Example

109 |

This is a dummy portal for cypress to test

110 |

Created by Callum Silcock for a talk on how to use GWT with Cypress

111 |
112 | ) 113 | 114 | const Dashboard = () => ( 115 |
116 |

Cypress GWT Dashboard

117 |

This is a dashboard

118 |
119 | ) 120 | 121 | const App = () => { 122 | const [isAuthenticated, setAuthentication] = useState(false) 123 | return ( 124 | 125 |
126 | 130 | 131 | } 134 | /> 135 | 140 |
141 |
142 | ) 143 | } 144 | 145 | export default App 146 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-size: calc(1vw + 1vh + 0.5vmin); 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 16 | monospace; 17 | } 18 | 19 | h1 { 20 | font-size: unset; 21 | } 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import "./reset.css" 4 | import "./index.css" 5 | import App from "./App" 6 | import * as serviceWorker from "./serviceWorker" 7 | 8 | ReactDOM.render(, document.getElementById("root")) 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: https://bit.ly/CRA-PWA 13 | serviceWorker.unregister() 14 | -------------------------------------------------------------------------------- /src/reset.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | object, 6 | iframe, 7 | h1, 8 | h2, 9 | h3, 10 | h4, 11 | h5, 12 | h6, 13 | p, 14 | blockquote, 15 | pre, 16 | abbr, 17 | code, 18 | em, 19 | img, 20 | small, 21 | strong, 22 | sub, 23 | sup, 24 | ol, 25 | ul, 26 | li, 27 | fieldset, 28 | form, 29 | label, 30 | legend, 31 | table, 32 | tbody, 33 | tfoot, 34 | thead, 35 | tr, 36 | th, 37 | td, 38 | article, 39 | aside, 40 | footer, 41 | header, 42 | nav, 43 | section, 44 | time, 45 | audio, 46 | video { 47 | padding: 0; 48 | border: 0; 49 | margin: 0; 50 | background: transparent; 51 | font-size: 100%; 52 | font-weight: inherit; 53 | vertical-align: baseline; 54 | } 55 | 56 | article, 57 | aside, 58 | figure, 59 | footer, 60 | header, 61 | nav, 62 | section { 63 | display: block; 64 | } 65 | 66 | html { 67 | box-sizing: border-box; 68 | overflow-y: scroll; 69 | } 70 | 71 | html, 72 | body { 73 | background-color: #fff; 74 | color: #000; 75 | } 76 | 77 | *, 78 | *::before, 79 | *::after { 80 | box-sizing: inherit; 81 | } 82 | 83 | img, 84 | object { 85 | max-width: 100%; 86 | } 87 | 88 | ul { 89 | list-style: none; 90 | } 91 | 92 | table { 93 | border-collapse: collapse; 94 | border-spacing: 0; 95 | } 96 | 97 | th { 98 | font-weight: bold; 99 | vertical-align: bottom; 100 | } 101 | 102 | td { 103 | font-weight: normal; 104 | vertical-align: top; 105 | } 106 | 107 | input, 108 | select { 109 | vertical-align: middle; 110 | } 111 | 112 | input[type="radio"] { 113 | vertical-align: text-bottom; 114 | } 115 | 116 | input[type="checkbox"] { 117 | vertical-align: bottom; 118 | } 119 | 120 | strong { 121 | font-weight: bold; 122 | } 123 | 124 | label, 125 | input[type="file"] { 126 | cursor: pointer; 127 | } 128 | 129 | input, 130 | select, 131 | textarea { 132 | border: 0; 133 | border-radius: 0; 134 | margin: 0; 135 | } 136 | 137 | button, 138 | input[type="button"], 139 | input[type="submit"] { 140 | padding: 0; 141 | border: 0; 142 | border-radius: 0; 143 | margin: 0; 144 | background: transparent; 145 | appearance: none; 146 | cursor: pointer; 147 | } 148 | 149 | button::-moz-focus-inner { 150 | padding: 0; 151 | border: 0; 152 | } 153 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /talk/using-gwt-with-cypress-raw.md: -------------------------------------------------------------------------------- 1 | ### How to get your product owners to write your functional tests 2 | 3 | ### AKA 4 | 5 | ## [fit] Using Given / When / Then with Cypress 6 | 7 | ### 🥒🤖 8 | 9 | ^ This presentation was written in Markdown for DeckSet (mac) which is why it looks a little weird, see attached PDF output and notes of every slide 10 | 11 | --- 12 | 13 | ## Who are you? 14 | 15 | # Callum Silcock 16 | 17 | ### [fit] Frontend Platform Engineer @ Attest (contract) 18 | 19 | ### https://csi.lk || github.com/csi-lk 20 | 21 | ^ Have been doing frontend development and trying to take the pain out of testing for >10 years now 22 | ^ Not a twitter guy 23 | 24 | --- 25 | 26 | 🇦🇺 27 | "dartah" = data 28 | "rehpo" = repo 29 | 30 | ^ I have an Australian accent and mispronounce things 31 | 32 | --- 33 | 34 | ### What are we trying to solve? 35 | 36 | ^ I started a contract 4 or so years ago that had major issues with their development and deployment pipeline 37 | ^ Main goals were to decrease risk in production deployments 38 | ^ This is an evolution of that thinking to bring us to this end product 39 | ^ So lets dive in 40 | 41 | --- 42 | 43 | ``` 44 | .─. .─. .─. 45 | ( ) ┌────────┬─┐ ( ) ┌──────────┐ ┌──────────┐ ┌──────────┐ ( ) ┌──────────┐ 46 | ┌`─'┐ │ Ticket└─┤ ┌`─'┐ │ │ │ │ Merge │ QA │ ┌`─'┐ Promote │ │ 47 | │Prd│───>│ Story │───>│Dev│──>│ Branch │──>│ PR │──────>│ STG │──>│QA │────────>│Production│ 48 | │ │ │ Bug │ │ │ │ │ │ │ │ UAT │ │ │ │ │ 49 | └───┘ └──────────┘ └───┘ └──────────┘ └┬─────────┘ └──────────┘ └───┘ └──────────┘ 50 | ├>Tests Pass 51 | └>Approval 52 | 53 | ``` 54 | 55 | ^ Your current workflow probably consists of ticket > dev > pr > merge > qa / staging / uat regression > production 56 | ^ Shifting your current workflow of putting QA last to putting QA first by chaining a few tools 57 | 58 | --- 59 | 60 | [.code-highlight: all][.code-highlight: 2] 61 | [.code-highlight: 3][.code-highlight: 4-5] 62 | [.code-highlight: 6][.code-highlight: 7] 63 | 64 | ```javascript 65 | describe('logging in', () => { 66 | it('logs in, () => { 67 | cy.visit('http://localhost:3000/login'); 68 | cy.get('email').type('contact@csi.lk'); 69 | cy.get('password').type('hunter2'); 70 | cy.get('input[type="button"]').click(); 71 | cy.url().should('be', 'http://localhost:3000/dashboard'); 72 | }); 73 | }); 74 | ``` 75 | 76 | ^ Let's start with Cypress 77 | ^ Classic login scenario, most peoples current tests look something like this 78 | ^ Visit, type in email / password, click the button, end up on the dashboard 79 | ^ instead of writing within cypress write these in feature 80 | 81 | --- 82 | 83 | ```feature 84 | Feature: Testing 85 | 86 | Ewww regression 87 | 88 | Scenario: Lack of automation 89 | Given I am a Frontend Developer 90 | When I am forced to manually test 91 | Then kill me 92 | ``` 93 | 94 | ^ We're going to move them to a BDD approach with an old frield, cucumber 95 | ^ I know a lot of people shudder when hearing words like 'gherkins', 'cucumber' and the worst one... 'cukes' but this is quite nice, I promise 96 | ^ So if we translate our cypress scenario from before it looks like this 97 | 98 | --- 99 | 100 | [.code-highlight: all][.code-highlight: 1] 101 | [.code-highlight: 3][.code-highlight: 6] 102 | [.code-highlight: 7-8][.code-highlight: 9] 103 | [.code-highlight: 10] 104 | 105 | ```feature 106 | Feature: Logging In 107 | 108 | Tests the user can successfully login and log out 109 | 110 | Scenario: Logging In Sucessfully 111 | Given I am on the "login" page 112 | When I input my "email" as "contact@csi.lk" 113 | And I input my "password" as "hunter2" 114 | And I click the "login" button 115 | Then I should be on the "dashboard" page 116 | 117 | ``` 118 | 119 | ^ Feature flow of login page > email > password > login button > dashboard page 120 | ^ What's up with the quotes? This is where it gets interesting, we are defining params that will be picked up in our function later 121 | ^ We are able to start defining these steps globally in a reusable way 122 | 123 | --- 124 | 125 | ```javascript 126 | 127 | # Given I am on the "login" page 128 | 129 | import { Given } from "cypress-cucumber-preprocessor/steps" 130 | 131 | Given(`I am on the {string} page`, target => { 132 | cy.visit(`https://localhost:3000/${target.toLowerCase()}`) 133 | }) 134 | 135 | ``` 136 | 137 | ^ Allows you to define a param as part of the step string which we can pass into our re-usable function 138 | ^ Let's switch tracks to a different concept now, using data attributes make it easy for chpres to taget them 139 | 140 | --- 141 | 142 | [.code-highlight: all][.code-highlight: 5, 11, 14] 143 | 144 | ```html 145 | 151 | 157 | 166 | ``` 167 | 168 | ^ React, vue, jquery, mootools doesn't matter, what we're doing here is adding a specific data attribute to each input above, i'm using 'data-qa' as it's quite declarative but you can set this to whatever you like 169 | ^ You can strip your data-qa's out of your build in prod, some do (twitter) some don't (dropbox) 170 | ^ The most important thing is to make sure you have a specific format, see above I have {type} dash {name}, depending on your project you will need to define this but make sure it's consistent 171 | 172 | --- 173 | 174 | [.code-highlight: all][.code-highlight: 1] 175 | [.code-highlight: 3][.code-highlight: 4] 176 | [.code-highlight: 5][.code-highlight: 10] 177 | 178 | ```javascript 179 | // Then the "navigation logout" button should "not" "exist" 180 | 181 | Then( 182 | `the {string} button should {string} {string}`, 183 | (id, assert, assertion) => 184 | cy 185 | .get(`[data-qa="${type}-${id.replace(" ", "-").toLowerCase()}"]`) 186 | .should(assert, assertion) 187 | //cy.get([data-qa="button-navigation-logout"]).should("not", "exist") 188 | ) 189 | 190 | // Reuse! 191 | 192 | // Then the "login" button should "be" "disabled" 193 | ``` 194 | 195 | ^ This leads up the coolest part, combining the global step definitions and the data qa attributes we can do some cool shit like this 196 | ^ Allows us to reuse this over and over 197 | ^ going back to our workflow 198 | 199 | --- 200 | 201 | ``` 202 | 203 | 204 | .─. .─. .─. 205 | ( ) ┌────────┬─┐ ( ) ┌──────────┐ ( ) ┌──────────┐ ┌──────────┐ 206 | ┌`─'┐ │ Ticket└─┤ ┌`─'┐ │ │ ┌`─'┐ │ │ Merge │ │ 207 | │Prd│───>│ Story │───>│QA │──>│ Branch │──>│Dev│──>│ PR │──────┬─────>│Production│ 208 | │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 209 | └───┘ └──────────┘ └───┘ └──────────┘ └───┘ └┬─────────┘ │ └──────────┘ 210 | ├>Tests Pass │ ┌──────────┐ 211 | ├>Approval │ │ .feature │ 212 | └>Canary for UAT └─────>│ static │ 213 | │ site │ 214 | └──────────┘ 215 | 216 | ``` 217 | 218 | ^ move qa to start of pipeline 219 | ^ Using this approach allows your product owners to write the GWT scenarios in plain english, QA picks up and validates over to dev 220 | ^ Also is powerful for Bug flows 221 | 222 | --- 223 | 224 | ``` 225 | .─. .─. 226 | ( ) ┌────────┬─┐ ( ) 227 | ┌`─'┐ │ └─┤ Validate ┌`─'┐ 228 | │QA │─┬─>│ Bug │──────────>│Prd│<┐ 229 | │ │ │ │ │ │ │ │ 230 | └───┘ │ └──────────┘ └───┘ │ 231 | │ .─. │ 232 | │ ┌──────────┐ ( ) ┌──────────┐ ┌──────────┐ 233 | │ │ │ ┌`─'┐ │ │ Merge │ │ 234 | └─>│ Branch │──>│Dev│──>│ PR │──────┬─────>│Production│ 235 | │ │ │ │ │ │ │ │ │ 236 | └──────────┘ └───┘ └┬─────────┘ │ └──────────┘ 237 | ├>Tests Pass │ ┌──────────┐ 238 | ├>Approval │ │ .feature │ 239 | └>Canary for UAT └─────>│ static │ 240 | │ site │ 241 | └──────────┘ 242 | 243 | ``` 244 | 245 | ^ Now enabled QA to create branches of these bugs through the branch / ticket 246 | ^ dev proves bug is fixed through the functional tests 247 | ^ you can even have a validation step in there for product 248 | ^ then straight to prod! 249 | 250 | --- 251 | 252 | ``` 253 | 254 | ┌──────────┐ ┌──────────┐ .─. 255 | │ new │ parse .feature(s) │ Static │ │( )│wow! 256 | │ .feature │────────────────────>│ Site │ │┌`─'┐│ 257 | │ test │ │Generator │ └┤Prd├┘ 258 | └──────────┘┌──────────┐ └──────────┘ │ │ 259 | │ │ Cypress │ │ └───┘ 260 | ├────>│Dashboard │<────┐ │ │ 261 | │ │ │ │ ▼ │ 262 | │ └──────────┘ │ ┌─────┬┐ ┌───────┐ 263 | │ ┌──────────┐ │read │ └┤ rsync │ s3 │ 264 | │ │ Visual │ ├─────│output│──────>│bucket │ 265 | └────>│Regression│<────┘ │ │ │ serve │ 266 | │ │ └──────┘ └───────┘ 267 | └──────────┘ 268 | ``` 269 | 270 | ^ To complete the loop publish the `.feature` files to an internal s3 bucket (or something similar) for product to know what is covered 271 | ^ Visual regression can be linked too (screenshots) 272 | ^ Cypress dashboard showing videos of the flows and screenshots, what is passing and failing 273 | ^ Gives product a overall dashboard showing what is covered, even could show build status (if you're feeling confident) 274 | ^ Ok you now understand the approach so what are the pros and cons? 275 | 276 | --- 277 | 278 | ### Cons 279 | 280 | - Abstracting tests from code 281 | - Lack of IDE tooling 282 | - Lots of tests make for slow builds 283 | - BDD Is hard 284 | 285 | ^ Your runner is now in the feature files rather than the code itself 286 | ^ There's no great intelligent IDE tools yet to bridge that gap 287 | ^ Having a ton of tests takes a while to run (although you can counter this by running against only what's changed and running tests across multiple runners) 288 | ^ BDD is a difficult state to get to, requires a lot of buy in from your product owners to dev to qa 289 | ^ But let's focus on the positive with Pros 290 | 291 | --- 292 | 293 | ### Pros 294 | 295 | - Product owners write the tests and see the whole pipeline 296 | - QA moves to the front of the pipeline 297 | - Devs have less tests to write 298 | - Less risk with deployments 299 | 300 | ^ Everyone is happy 301 | ^ Product owners can now see everything happening and the gaps they need to fill, can go to whole business 302 | ^ QA is happy as they're moving to proactive rather than reactive 303 | ^ Dev is happy because they can focus on code rather than tests 304 | ^ Lastly, less risk with production deployments as all major scnearios are covered 305 | 306 | --- 307 | 308 | ## Live Tech Demo 309 | 310 | ### 💻😱🤖 311 | 312 | ^ Show http://github.com/csi-lk/cypress-gwt-example repo 313 | ^ Show github action workflow: https://github.com/csi-lk/cypress-gwt-example/blob/master/.github/workflows/run-cypress.yml 314 | ^ Show CI working: https://github.com/csi-lk/cypress-gwt-example/actions 315 | ^ Show cypress dashboard: https://dashboard.cypress.io/#/projects/syj7cg/runs 316 | 317 | --- 318 | 319 | # Thanks 320 | 321 | # [FIT] github.com/csi-lk/cypress-gwt-example 322 | 323 | #### ❤️ Callum Silcock 324 | 325 | ^ Companion repo showing how to set this up 326 | ^ if you have questions let me know 327 | ^ Thanks 328 | -------------------------------------------------------------------------------- /talk/using-gwt-with-cypress.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csi-lk/cypress-gwt-example/7b028d1e998f00b0bcc531664df234470cb6d86e/talk/using-gwt-with-cypress.pdf --------------------------------------------------------------------------------