"
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 = ''
10 |
11 | const changed = updateRelativeUrls(baseUrl, md)
12 |
13 | if (changed !== '') {
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 |
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'
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 | 
90 |
91 | +++
92 |
93 | 
94 |
95 | +++
96 |
97 | 
98 |
99 | +++
100 |
101 | 
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 | 
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 | 
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 | 
228 |
229 | +++
230 |
231 | Every Cypress command and every assertion
232 |
233 | 
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 | 
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 | 
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 | 
23 |
24 | +++
25 |
26 | Open "Selector Playground"
27 |
28 | 
29 |
30 | +++
31 |
32 | Selector playground can suggest much better selectors.
33 |
34 | 
35 |
36 | +++
37 |
38 | ⚠️ It can suggest a weird selector
39 |
40 | 
41 |
42 | +++
43 |
44 | Read [best-practices.html#Selecting-Elements](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)
45 |
46 | 
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 | 
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 | 
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 | 
19 |
20 | +++
21 |
22 | 
23 |
24 | +++
25 |
26 | 
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 | 
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 | 
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 | 
59 |
60 | +++
61 |
62 | There might be multiple delays: loading the page, fetching todos, rendering data on the page.
63 |
64 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
385 |
386 | +++
387 |
388 | 
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 | 
84 |
85 | +++
86 |
87 | And then IntelliSense works immediately
88 |
89 | 
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 | 
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 | 
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 | 
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 | 
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 | 
169 |
170 | +++
171 |
172 | If you have Vue DevTools plugin
173 |
174 | 
175 |
176 | +++
177 |
178 | Look at XHR when using the app
179 |
180 | 
181 |
182 | +++
183 |
184 | Look at `todomvc/index.html` - main app DOM structure
185 |
186 | 
187 |
188 | +++
189 |
190 | Look at `todomvc/app.js`
191 |
192 | 
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 | 
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 | 
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 |
55 |
71 |
72 |
Loading data ...
73 |
74 |
75 |
114 |
115 |
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 |
--------------------------------------------------------------------------------