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
--------------------------------------------------------------------------------