├── .github
└── workflows
│ ├── badges.yml
│ └── ci.yml
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── LICENSE
├── README.md
├── cypress.config.js
├── cypress
├── dataset.html
├── detach.html
├── e2e
│ ├── apply.cy.js
│ ├── as-env.cy.js
│ ├── assertions
│ │ ├── possess.cy.js
│ │ └── read.cy.js
│ ├── at.cy.js
│ ├── chain-with-assertions.cy.js
│ ├── detach.cy.js
│ ├── diff.cy.js
│ ├── difference.cy.js
│ ├── elements.cy.js
│ ├── filter-table.cy.js
│ ├── filter-table
│ │ ├── filter.js
│ │ ├── index.html
│ │ └── style.css
│ ├── filter.cy.js
│ ├── find-one.cy.js
│ ├── get-in-order.cy.js
│ ├── import-library.js
│ ├── instance.cy.js
│ ├── invoke-first.cy.js
│ ├── invoke-once.cy.js
│ ├── json-attribute
│ │ ├── index.html
│ │ └── spec.cy.js
│ ├── log-examples.cy.js
│ ├── make.cy.js
│ ├── map-chain.cy.js
│ ├── map-invoke.cy.js
│ ├── map.cy.js
│ ├── multiple-import.cy.js
│ ├── partial.cy.js
│ ├── primo.cy.js
│ ├── print.cy.js
│ ├── prop.cy.js
│ ├── reduce.cy.js
│ ├── sample.cy.js
│ ├── second.cy.js
│ ├── stable-css
│ │ ├── index.html
│ │ └── stable-css.cy.js
│ ├── stable
│ │ ├── index.html
│ │ └── stable.cy.js
│ ├── table-rows.cy.js
│ ├── table.cy.js
│ ├── tap.cy.js
│ ├── text.cy.js
│ ├── third.cy.js
│ ├── to-plain-object.cy.js
│ ├── unique.cy.js
│ └── version.cy.js
├── fixtures
│ └── people.json
├── index.html
├── list.html
├── log-examples.html
├── prices.html
└── table.html
├── images
└── table.png
├── package-lock.json
├── package.json
├── renovate.json
├── src
└── commands
│ ├── apply.js
│ ├── as-env.js
│ ├── assertions.js
│ ├── at.js
│ ├── detaches.js
│ ├── difference.js
│ ├── elements.js
│ ├── find-one.js
│ ├── get-in-order.js
│ ├── index.d.ts
│ ├── index.js
│ ├── invoke-first.js
│ ├── invoke-once.js
│ ├── make.js
│ ├── map-chain.js
│ ├── map-invoke.js
│ ├── map.js
│ ├── partial.js
│ ├── primo.js
│ ├── print.js
│ ├── prop.js
│ ├── reduce.js
│ ├── sample.js
│ ├── second.js
│ ├── stable.ts
│ ├── table.js
│ ├── tap.js
│ ├── third.js
│ ├── to-plain-object.js
│ ├── update.js
│ ├── utils.js
│ └── version-check.js
└── tsconfig.json
/.github/workflows/badges.yml:
--------------------------------------------------------------------------------
1 | name: badges
2 | on:
3 | schedule:
4 | # update badges every night
5 | # because we have a few badges that are linked
6 | # to the external repositories
7 | - cron: '0 3 * * *'
8 |
9 | jobs:
10 | badges:
11 | name: Badges
12 | runs-on: ubuntu-24.04
13 | steps:
14 | - name: Checkout 🛎
15 | uses: actions/checkout@v4
16 |
17 | - name: Update version badges 🏷
18 | run: npm run badges
19 |
20 | - name: Commit any changed files 💾
21 | uses: stefanzweifel/git-auto-commit-action@v5
22 | with:
23 | commit_message: Updated badges
24 | branch: main
25 | file_pattern: README.md
26 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on: push
3 | jobs:
4 | lint:
5 | runs-on: ubuntu-24.04
6 | steps:
7 | - name: Checkout 🛎
8 | uses: actions/checkout@v4
9 |
10 | - name: Install everything 📦
11 | # https://github.com/cypress-io/github-action
12 | uses: cypress-io/github-action@v6
13 | with:
14 | runTests: false
15 |
16 | # make sure we did not leave "it.only" accidentally
17 | # https://github.com/bahmutov/stop-only
18 | - name: Catch "it.only" 🫴
19 | run: npm run stop-only
20 |
21 | - name: Lint code ☑️
22 | run: npm run lint
23 |
24 | test:
25 | # https://github.com/bahmutov/cypress-workflows
26 | uses: bahmutov/cypress-workflows/.github/workflows/split.yml@v2
27 | with:
28 | nE2E: 3
29 | build: npm run build
30 |
31 | release:
32 | needs: [lint, test]
33 | runs-on: ubuntu-24.04
34 | if: github.ref == 'refs/heads/main'
35 | steps:
36 | - uses: actions/setup-node@v4
37 | with:
38 | node-version: 20
39 |
40 | - name: Checkout 🛎
41 | uses: actions/checkout@v4
42 |
43 | - name: Install everything 📦
44 | # https://github.com/cypress-io/github-action
45 | uses: cypress-io/github-action@v6
46 | with:
47 | runTests: false
48 |
49 | - name: Build dist 🏗
50 | run: npm run build
51 |
52 | - name: Semantic Release 🚀
53 | uses: cycjimmy/semantic-release-action@v4
54 | with:
55 | branch: main
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | cypress/videos/
3 | dist
4 | commands/
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.15.0
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true,
6 | "printWidth": 70
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 bahmutov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cypress-map [](https://github.com/bahmutov/cypress-map/actions/workflows/ci.yml) 
2 |
3 | > Extra Cypress query commands for v12+
4 |
5 | - 📺 Watch the videos:
6 | - [Cypress v12 Querying Commands Introduction](https://youtu.be/4HpEECek2OE)
7 | - [Confirm Table Column](https://youtu.be/UOLQlNmuhY0)
8 | - [Use cypress-map Queries To Validate A Row In A Table](https://youtu.be/eVe4ySgW0qw)
9 | - [How To Check Visibility Of Many Elements](https://youtu.be/puCZGCeUb5k)
10 | - [Pick A Random Menu Link](https://youtu.be/xvvL3GRjXCY)
11 | - [Confirm The Same Text In A Couple Of Elements](https://youtu.be/xvImOlCSul4)
12 | - [Map Input Elements Values](https://youtu.be/OmVzv6pJN6I)
13 | - [Iterate Over DOM Elements Using cy.each, jQuery each and map, and cypress-map Plugin Commands](https://youtu.be/tMeKIIfEhyo)
14 | - [Confirm Sorted Attributes](https://youtu.be/sVb5MU2AkqE)
15 | - [Query Multiple Elements In Order](https://youtu.be/3BjwoG1dW7o)
16 | - [Element's Text Becomes Stable](https://youtu.be/GrRUnQ2r7Wk)
17 | - [Cut Cypress Execution In Half By Running Tests In Parallel Using cypress-split And GitHub Actions](https://youtu.be/jvBzNs0pRXU)
18 | - [Fix GitHub Actions Node Version Warnings](https://youtu.be/1_jvJ3c8QAY)
19 | - [Use cy.second and cy.third Commands](https://youtu.be/gZtTN9LaD7U)
20 | - [Remove Class From Sampled Elements](https://youtu.be/zB2LYB0yFwQ)
21 | - [Check Multiple Properties At Once Using cy.difference Query](https://youtu.be/WKVaJjst_-8)
22 | - [cy.difference Command With Predicates](https://youtu.be/RKgSBN2fk_s)
23 | - [The Possess Assertion From cypress-map Plugin](https://youtu.be/HHxkL-BPyjA)
24 | - [Custom Should Read Assertion From Cypress-map Plugin](https://youtu.be/AzJx-8VD6yI)
25 | - [Introducing The cy.elements Query Command From Cypress-map Plugin](https://youtu.be/4mFZMQpzBIU)
26 | - 📝 Read the blog posts
27 | - [Cypress V12 Is A Big Deal](https://glebbahmutov.com/blog/cypress-v12/)
28 | - [Crawl Weather Using Cypress](https://glebbahmutov.com/blog/crawl-weather/)
29 | - [Pass Values Between Cypress Tests](https://glebbahmutov.com/blog/pass-values-between-tests/)
30 | - [Do Not Use SHA To Compare HTML During E2E Tests](https://glebbahmutov.com/blog/do-not-use-sha/)
31 | - [Cypress Flakiness Examples](https://glebbahmutov.com/blog/flakiness-example/)
32 | - [Solve Tough Pagination Cases Using Cypress](https://glebbahmutov.com/blog/solve-tough-pagination-cases-using-cypress/)
33 | - [Check Fees And Totals Using Cypress](https://glebbahmutov.com/blog/check-fees-using-cypress/)
34 | - [Custom Cypress Should Read Assertion](https://glebbahmutov.com/blog/cypress-map-should-read-assertion/)
35 | - 🎓 Covered in my course [Cypress Plugins](https://cypress.tips/courses/cypress-plugins)
36 | - [Lesson l1: Confirm the attribute of the last item](https://cypress.tips/courses/cypress-plugins/lessons/l1)
37 | - [Lesson l2: Confirm the extracted list of texts](https://cypress.tips/courses/cypress-plugins/lessons/l2)
38 | - [Lesson l3: Confirm the last two items in the extracted lis](https://cypress.tips/courses/cypress-plugins/lessons/l3)
39 | - [Lesson l4: Confirm the list of attributes](https://cypress.tips/courses/cypress-plugins/lessons/l4)
40 | - [Lesson l5: Confirm the sum of attributes](https://cypress.tips/courses/cypress-plugins/lessons/l5)
41 | - [Lesson l6: Compare the sum of attributes to the total](https://cypress.tips/courses/cypress-plugins/lessons/l6)
42 | - [Lesson l7: Debug chained queries using tap](https://cypress.tips/courses/cypress-plugins/lessons/l7)
43 | - [Lesson l8: Get the raw DOM element at index k](https://cypress.tips/courses/cypress-plugins/lessons/l8)
44 | - [Lesson l9: Confirm the number of elements with the given attribute](https://cypress.tips/courses/cypress-plugins/lessons/l9)
45 | - [Lesson l10: Check the parsed attribute value](https://cypress.tips/courses/cypress-plugins/lessons/l10)
46 | - [Lesson l11: Extract and convert prices](https://cypress.tips/courses/cypress-plugins/lessons/l11)
47 | - [Lesson l12: Find the element with the smallest attribute value](https://cypress.tips/courses/cypress-plugins/lessons/l12)
48 | - [Lesson l13: Check each item's text](https://cypress.tips/courses/cypress-plugins/lessons/l13)
49 | - [Lesson l14: Flexible logging using cy.print command](https://cypress.tips/courses/cypress-plugins/lessons/l14)
50 | - [Lesson l15: Confirm HTML table values](https://cypress.tips/courses/cypress-plugins/lessons/l15)
51 | - [Lesson l17: Control the network and confirm the table](https://cypress.tips/courses/cypress-plugins/lessons/l17)
52 | - 🎓 Covered in my course [Cypress Network Testing Exercises](https://cypress.tips/courses/network-testing)
53 | - [Bonus 92: Map each item from the list using Cy commands](https://cypress.tips/courses/network-testing/lessons/bonus92)
54 | - [Bonus 100: Check all network responses at once with auto retries](https://cypress.tips/courses/network-testing/lessons/bonus100)
55 | - 🎓 Covered in my course [Testing The Swag Store](https://cypress.tips/courses/swag-store)
56 | - [Lesson 23: Simplify getting attribute from a list of elements](https://cypress.tips/courses/swag-store/lessons/lesson23)
57 | - [Bonus 36: Manipulate data- attributes using cypress-map methods](https://cypress.tips/courses/swag-store/lessons/bonus36)
58 | - [Bonus 41: Pick and confirm a random product](https://cypress.tips/courses/swag-store/lessons/bonus41)
59 | - [Bonus 42: Add 3 random items to the cart](https://cypress.tips/courses/swag-store/lessons/bonus42)
60 | - [Bonus 127: Parse URLSearchParams with retries](https://cypress.tips/courses/network-testing/lessons/bonus127)
61 |
62 | ## Install
63 |
64 | Add this package as a dev dependency:
65 |
66 | ```
67 | $ npm i -D cypress-map
68 | # or using Yarn
69 | $ yarn add -D cypress-map
70 | ```
71 |
72 | Include this package in your spec or support file to use all custom query commands
73 |
74 | ```js
75 | import 'cypress-map'
76 | ```
77 |
78 | Alternative: import only the query commands you need:
79 |
80 | ```js
81 | import 'cypress-map/commands/map'
82 | import 'cypress-map/commands/tap'
83 | // and so on, see the /commands folder
84 | ```
85 |
86 | ## API
87 |
88 | ### apply
89 |
90 | ```js
91 | const double = (n) => n * 2
92 | cy.wrap(100).apply(double).should('equal', 200)
93 | ```
94 |
95 | It works like `cy.then` but `cy.apply(fn)` is a query command. Function `fn` should be synchronous, pure function that only uses the subject argument and returns new value The function callback `fn` cannot use any Cypress commands `cy`.
96 |
97 | You can pass additional _left_ arguments to the callback function. Then it puts the subject as _last argument_ before calling the function:
98 |
99 | ```js
100 | cy.wrap(8).apply(Cypress._.subtract, 4).should('equal', -4)
101 | ```
102 |
103 | ### applyRight
104 |
105 | Without arguments, `cy.applyRight` works the same as `cy.apply`. If you pass arguments, then the subject plus the arguments become the arguments to the callback. The subject is at the _left_ (first) position
106 |
107 | ```js
108 | cy.wrap(8).applyRight(Cypress._.subtract, 4).should('equal', 4)
109 | // same as
110 | cy.wrap(8)
111 | .apply((subject) => Cypress._.subtract(subject, 4))
112 | .should('equal', 4)
113 | ```
114 |
115 | ### partial
116 |
117 | Sometimes you have the callback to apply, and you know the first argument(s), and just need to put the subject at the last position. This is where you can partially apply the known arguments to the given callback.
118 |
119 | ```js
120 | // the Cypress._.add takes to arguments (a, b)
121 | // we know the first argument a = 5
122 | // so we partially apply it and wait for the subject = b argument
123 | cy.wrap(100).partial(Cypress._.add, 5).should('equal', 105)
124 | // same as
125 | cy.wrap(100)
126 | .apply((subject) => Cypress._.add(5, subject))
127 | .should('equal', 105)
128 | ```
129 |
130 | ### applyToFirst
131 |
132 | If the current subject is an array, or a jQuery object, you can apply the given callback with arguments to the _first_ item or element. The current subject will be the _last_ argument.
133 |
134 | ```js
135 | // cy.applyToFirst(callback, ...args)
136 | cy.wrap(Cypress.$('
100
200
'))
137 | .applyToFirst((base, el) => parseInt(el.innerText, base), 10)
138 | .should('equal', 100)
139 | ```
140 |
141 | ### applyToFirstRight
142 |
143 | If the current subject is an array, or a jQuery object, you can apply the given callback with arguments to the _first_ item or element. The current subject will be the _first_ argument.
144 |
145 | ```js
146 | // cy.applyToFirstRight(callback, ...args)
147 | cy.wrap(Cypress.$('100
200
'))
148 | .applyToFirstRight((el, base) => parseInt(el.innerText, base), 10)
149 | .should('equal', 100)
150 | ```
151 |
152 | ### invokeFirst
153 |
154 | We often just need to call a method on the first element / item in the current subject
155 |
156 | ```js
157 | cy.get(selector).invokeFirst('getBoundingClientRect')
158 | // compute the vertical center for example
159 | ```
160 |
161 | ### map
162 |
163 | Transforms every object in the given collection by running it through the given callback function. Can also map each object to its property. An object could be an array or a jQuery object.
164 |
165 | ```js
166 | // map elements by invoking a function
167 | cy.wrap(['10', '20', '30']).map(Number) // [10, 20, 30]
168 | // map elements by a property
169 | cy.get('.matching')
170 | .map('innerText')
171 | .should('deep.equal', ['first', 'third', 'fourth'])
172 | ```
173 |
174 | You can even map properties of an object by listing callbacks. For example, let's convert the `age` property from a string to a number
175 |
176 | ```js
177 | cy.wrap({
178 | age: '42',
179 | lucky: true,
180 | })
181 | .map({
182 | age: Number,
183 | })
184 | .should('deep.equal', {
185 | age: 42,
186 | lucky: true,
187 | })
188 | ```
189 |
190 | You can avoid any conversion to simply pick the list of properties from an object
191 |
192 | ```js
193 | const person = {
194 | name: 'Joe',
195 | age: 21,
196 | occupation: 'student',
197 | }
198 | cy.wrap(person).map(['name', 'age']).should('deep.equal', {
199 | name: 'Joe',
200 | age: 21,
201 | })
202 | ```
203 |
204 | You can extract nested paths by using "." in your property path
205 |
206 | ```js
207 | cy.wrap(people)
208 | .map('name.first')
209 | .should('deep.equal', ['Joe', 'Anna'])
210 | // equivalent to
211 | cy.wrap(people)
212 | .map('name')
213 | .map('first')
214 | .should('deep.equal', ['Joe', 'Anna'])
215 | ```
216 |
217 | If you want to pick multiple deep properties, the last segment will be the output property name
218 |
219 | ```js
220 | // each person has nested objects like
221 | // { name: { first: '...', last: '...', }, human: { age: xx, ... } }
222 | cy.wrap(people).map(['name.first', 'human.age'])
223 | // each object will have "first" and "age" properties
224 | ```
225 |
226 | ### mapInvoke
227 |
228 | ```js
229 | cy.get('#items li')
230 | .find('.price')
231 | .map('innerText')
232 | .mapInvoke('replace', '$', '')
233 | .mapInvoke('trim')
234 | ```
235 |
236 | ### reduce
237 |
238 | ```js
239 | cy.get('#items li')
240 | .find('.price')
241 | .map('innerText')
242 | .mapInvoke('replace', '$', '')
243 | .map(parseFloat)
244 | .reduce((max, n) => (n > max ? n : max))
245 | // yields the highest price
246 | ```
247 |
248 | You can provide the initial accumulator value
249 |
250 | ```js
251 | cy.wrap([1, 2, 3])
252 | .reduce((sum, n) => sum + n, 10)
253 | .should('equal', 16)
254 | ```
255 |
256 | See [reduce.cy.js](./cypress/e2e/reduce.cy.js)
257 |
258 | ### tap
259 |
260 | ```js
261 | cy.get('#items li')
262 | .find('.price')
263 | .map('innerText')
264 | .tap() // console.log by default
265 | .mapInvoke('replace', '$', '')
266 | .mapInvoke('trim')
267 | // console.info with extra label
268 | .tap(console.info, 'trimmed strings')
269 | ```
270 |
271 | **Notice:** if the label is provided, the callback function is called with label and the subject.
272 |
273 | ### make
274 |
275 | A retryable query that calls the given constructor function using the `new` keyword and the current subject as argument.
276 |
277 | ```js
278 | cy.wrap('Jan 1, 2019')
279 | // same as "new Date('Jan 1, 2019')"
280 | .make(Date)
281 | .invoke('getFullYear')
282 | .should('equal', 2019)
283 | ```
284 |
285 | ### print
286 |
287 | A better `cy.log`: yields the value, intelligently stringifies values using `%` and [string-format](https://github.com/davidchambers/string-format) notation.
288 |
289 | ```js
290 | cy.wrap(42)
291 | .print() // "42"
292 | // and yields the value
293 | .should('equal', 42)
294 | // pass formatting string
295 | cy.wrap(42).print('the answer is %d') // "the answer is 42"
296 | cy.wrap({ name: 'Joe' }).print('person %o') // 'person {"name":"Joe"}'
297 | // use {0} with dot notation, supported deep properties
298 | // https://github.com/davidchambers/string-format
299 | cy.wrap({ name: 'Joe' }).print('person name {0.name}') // "person name Joe"
300 | // print the length of an array
301 | cy.wrap(arr).print('array length {0.length}') // "array length ..."
302 | // pass your own function to return formatted string
303 | cy.wrap(arr).print((a) => `array with ${a.length} items`)
304 | // if you return a non-string, it will attempt to JSON.stringify it
305 | cy.wrap(arr).print((list) => list[2]) // JSON.stringify(arr[2])
306 | ```
307 |
308 | See [print.cy.js](./cypress/e2e/print.cy.js) for more examples
309 |
310 | ### findOne
311 |
312 | Finds a single item in the subject. Assumes subject is an array or a jQuery object. Uses Lodash `_.find` method.
313 |
314 | ```js
315 | // using predicate function
316 | const isThree = n => n === 3
317 | cy.wrap([...]).findOne(isThree).should('equal', 3)
318 | // using partial known properties of an object
319 | cy.wrap([...]).findOne({ name: 'Anna' }).should('have.property', 'name', 'Anna')
320 | ```
321 |
322 | See [find-one.cy.js](./cypress/e2e/find-one.cy.js)
323 |
324 | ### primo
325 |
326 | ```js
327 | cy.get('.matching')
328 | .map('innerText')
329 | .primo()
330 | .invoke('toUpperCase')
331 | .should('equal', 'FIRST')
332 | ```
333 |
334 | See [primo.cy.js](./cypress/e2e/primo.cy.js)
335 |
336 | ### prop
337 |
338 | Works like `cy.its` for objects, but gets the property for jQuery objects, which `cy.its` does not
339 |
340 | ```js
341 | cy.get('#items li.matching')
342 | .last()
343 | .prop('ariaLabel')
344 | .should('equal', 'four')
345 | ```
346 |
347 | See [prop.cy.js](./cypress/e2e/prop.cy.js)
348 |
349 | ### update
350 |
351 | Changes a single property inside the subject by running it through the given callback function. Useful to do type conversions, for example, let's convert the "age" property to a Number
352 |
353 | ```js
354 | cy.wrap({ age: '20' })
355 | .update('age', Number)
356 | .should('deep.equal', { age: 20 })
357 | ```
358 |
359 | ### at
360 |
361 | Returns a DOM element from jQuery object at position `k`. Returns an item from array at position `k`. For negative index, counts the items from the end.
362 |
363 | ```js
364 | cy.get('#items li').at(-1).its('innerText').should('equal', 'fifth')
365 | ```
366 |
367 | See [at.cy.js](./cypress/e2e/at.cy.js)
368 |
369 | ### sample
370 |
371 | Returns a randomly picked item or element from the current subject
372 |
373 | ```js
374 | cy.get('#items li').sample().should('have.text', 'four')
375 | ```
376 |
377 | If you pass a positive number, then it picks multiple elements or items
378 |
379 | ```js
380 | // yields jQuery object with 3 random items
381 | cy.get('#items li').sample(3).should('have.length', 3)
382 | ```
383 |
384 | See [sample.cy.js](./cypress/e2e/sample.cy.js)
385 |
386 | ### second
387 |
388 | Yields the second element from the current subject. Could be an element or an array item.
389 |
390 | ```js
391 | cy.get('#items li').second().should('have.text', 'second')
392 | ```
393 |
394 | See [second.cy.js](./cypress/e2e/second.cy.js)
395 |
396 | ### third
397 |
398 | Yields the third element from the current subject. Could be an element or an array item.
399 |
400 | ```js
401 | cy.get('#items li').third().should('have.text', 'third')
402 | ```
403 |
404 | See [third.cy.js](./cypress/e2e/third.cy.js)
405 |
406 | ### asEnv
407 |
408 | Saves current subject in `Cypress.env` object. Note: Cypress.env object is reset before the spec run, but the changed values are passed from test to test. Thus you can easily pass a value from the first test to the second.
409 |
410 | ```js
411 | it('saves value in this test', () => {
412 | cy.wrap('hello, world').asEnv('greeting')
413 | })
414 |
415 | it('saved value is available in this test', () => {
416 | expect(Cypress.env('greeting'), 'greeting').to.equal('hello, world')
417 | })
418 | ```
419 |
420 | Do you really want to make the tests dependent on each other?
421 |
422 | ### elements
423 |
424 | Often we need to find a list of elements with some sub-parts. The query `cy.elements` uses the parent selector plus child selectors and returns an array of arrays of strings.
425 |
426 | Given this HTML
427 |
428 | ```html
429 |
430 | Item A 1
431 | Item B 2
432 | Item C 3
433 | Item D 4
434 |
435 | ```
436 |
437 | Find all items and the numbers. The parent selector is `#tasks li`, and inside the parts we want to get are `.k` and `.name` (in this order)
438 |
439 | ```js
440 | cy.elements('#tasks li', '.k', '.name').should('deep.equal', [
441 | ['1', 'Item A'],
442 | ['2', 'Item B'],
443 | ['3', 'Item C'],
444 | ['4', 'Item D'],
445 | ])
446 | ```
447 |
448 | ### getInOrder
449 |
450 | Queries the page using multiple selectors and returns the found elements _in the specified_ order, no matter how they are ordered in the document. Retries if any of the selectors are not found.
451 |
452 | ```js
453 | cy.getInOrder('selector1', 'selector2', 'selector3', ...)
454 | // yields a single jQuery subject with
455 | // elements for selector1
456 | // and selector2,
457 | // and selector3, etc
458 | ```
459 |
460 | You can also use a single array of selector strings
461 |
462 | ```js
463 | cy.getInOrder(['h1', 'h2', 'h3'])
464 | ```
465 |
466 | Supports parent subject
467 |
468 | ```js
469 | cy.get('...').getInOrder('...', '...')
470 | ```
471 |
472 | ### stable
473 |
474 | Sometimes you just want to wait until the element is stable. For example, if the element's text content does not change for N milliseconds, then we can consider the element to be `text` stable.
475 |
476 | ```js
477 | cy.get('#message').stable('text')
478 | // yields the element
479 | ```
480 |
481 | Supported types: `text`, `value` (for input elements), `css`, and `element` (compares the element reference)
482 |
483 | You can control the quiet period (milliseconds), and pass the `log` and the `timeout` options
484 |
485 | ```js
486 | // stable for 500ms
487 | // without logging
488 | // with maximum retries duration of 6 seconds
489 | cy.get('#message').stable('text', 500, { log: false, timeout: 6_000 })
490 | ```
491 |
492 | When checking the CSS property to be stable, provide the name of the property:
493 |
494 | ```js
495 | // retries until the CSS animation finishes
496 | // and the background color is red
497 | cy.get('#message')
498 | .stable('css', 'background-color', 100)
499 | // yields the element
500 | .should('have.css', 'background-color', 'rgb(255, 0, 0)')
501 | ```
502 |
503 | See [stable.cy.js](./cypress/e2e/stable/stable.cy.js) and [stable-css.cy.js](./cypress/e2e/stable-css/stable-css.cy.js)
504 |
505 | ### detaches
506 |
507 | **experimental**
508 |
509 | Retries until the element with the given selector detaches from DOM.
510 |
511 | ```js
512 | cy.contains('Click to re-render').click()
513 | cy.detaches('#list')
514 | ```
515 |
516 | Sometimes the detachment can happen right with the action and the `cy.detaches(selector)` is _too late_. If you know the detachment might have already happened, you need to prepare for it by using an alias stored in the `Cypress.env` object:
517 |
518 | ```js
519 | cy.get('#name2').asEnv('name')
520 | cy.contains('Click to remove Joe').click()
521 | cy.detaches('@name')
522 | ```
523 |
524 | The jQuery object will be stored inside the `Cypress.env` under the `name` property.
525 |
526 | See [detach.cy.js](./cypress/e2e/detach.cy.js)
527 |
528 | ### difference
529 |
530 | Computes an object/arrays of the difference with the current subject object/array.
531 |
532 | ```js
533 | cy.wrap({ name: 'Joe', age: 20 })
534 | .difference({ name: 'Joe', age: 30 })
535 | .should('deep.equal', { age: { actual: 20, expected: 30 } })
536 | ```
537 |
538 | You can use synchronous predicate functions to validate properties
539 |
540 | ```js
541 | // confirm the value of the "age" property
542 | // is larger than 15
543 | .difference({ name: 'Joe', age: (n) => n > 15 })
544 | ```
545 |
546 | Reports missing and extra properties. See [difference.cy.js](./cypress/e2e/difference.cy.js)
547 |
548 | **Note:** use `have.length` to validate the number of items in an array:
549 |
550 | ```js
551 | // let's check if there are 3 objects in the array
552 | // INSTEAD OF THIS
553 | .difference([Cypress._.object, Cypress._.object, Cypress._.object])
554 | // USE AN ASSERTION
555 | .should('have.length', 3)
556 | ```
557 |
558 | You can check each item in the array subject using values / predicates from the expected object.
559 |
560 | ```js
561 | // list of people objects
562 | cy.wrap(people)
563 | .difference({
564 | name: Cypress._.isString,
565 | age: (age) => age > 1 && age < 100,
566 | })
567 | .should('be.empty')
568 | ```
569 |
570 | ### table
571 |
572 | 📝 to learn more about `cy.table` command, read the blog post [Test HTML Tables Using cy.table Query Command](https://glebbahmutov.com/blog/cy-table/).
573 |
574 | Extracts all cells from the current subject table. Yields a 2D array of strings.
575 |
576 | ```js
577 | cy.get('table').table()
578 | ```
579 |
580 | You can slice the table to yield just a region `.table(x, y, w, h)`
581 |
582 | 
583 |
584 | For example, you can get 2 by 2 subregion
585 |
586 | ```js
587 | cy.get('table')
588 | .table(0, 2, 2, 2)
589 | .should('deep.equal', [
590 | ['Cary', '30'],
591 | ['Joe', '28'],
592 | ])
593 | ```
594 |
595 | See the spec [table.cy.js](./cypress/e2e/table.cy.js) for more examples.
596 |
597 | **Tip:** you can combine `cy.table` with `cy.map`, `cy.mapInvoke` to get the parts of the table. For example, the same 2x2 part of the table could be extracted with:
598 |
599 | ```js
600 | cy.get('table')
601 | .table()
602 | .invoke('slice', 2, 4)
603 | .mapInvoke('slice', 0, 2)
604 | .should('deep.equal', [
605 | ['Cary', '30'],
606 | ['Joe', '28'],
607 | ])
608 | ```
609 |
610 | **Tip 2:** to get just the headings row, combine `.table` and `.its` queries
611 |
612 | ```js
613 | cy.get('table')
614 | .table(0, 0, 3, 1)
615 | .its(0)
616 | .should('deep.equal', ['Name', 'Age', 'Date (YYYY-MM-DD)'])
617 | ```
618 |
619 | To get the last row, you could do:
620 |
621 | ```js
622 | cy.get('table').table().invoke('slice', -1).its(0)
623 | ```
624 |
625 | To get the first column joined into a single array (instead of array of 1x1 arrays)
626 |
627 | ```js
628 | cy.get('table')
629 | .table(0, 1, 1) // skip the heading "Name" cell
630 | // combine 1x1 arrays into one array
631 | .invoke('flatMap', Cypress._.identity)
632 | .should('deep.equal', ['Dave', 'Cary', 'Joe', 'Anna'])
633 | ```
634 |
635 | ### toPlainObject
636 |
637 | A query to convert special DOM objects into plain objects. For example, to convert `DOMStringMap` instance into a plain object compatible with `deep.equal` assertion we can do
638 |
639 | ```js
640 | cy.get('article')
641 | .should('have.prop', 'dataset')
642 | .toPlainObject()
643 | .should('deep.equal', {
644 | columns: '3',
645 | indexNumber: '12314',
646 | parent: 'cars',
647 | })
648 | ```
649 |
650 | By default uses JSON stringify and parse back. If you want to convert using `entries` and `fromEntries`, add an argument:
651 |
652 | ```js
653 | cy.wrap(new URLSearchParams(searchParams)).toPlainObject('entries')
654 | ```
655 |
656 | ### invokeOnce
657 |
658 | In Cypress v12 `cy.invoke` became a query, which made working with asynchronous methods really unwieldy. The `cy.invokeOnce` is a return the old way of calling the method and yielding the resolved value.
659 |
660 | ```js
661 | cy.wrap(app)
662 | // app.fetchName is an asynchronous method
663 | // that returns a Promise
664 | .invokeOnce('fetchName')
665 | .should('equal', 'My App')
666 | ```
667 |
668 | See the spec [invoke-once.cy.js](./cypress/e2e/invoke-once.cy.js) for more examples.
669 |
670 | ### read
671 |
672 | **Assertion**
673 |
674 | Checks the exact text match or regular expression for a single element or multiple ones
675 |
676 | ```js
677 | cy.get('#name').should('read', 'Joe Smith')
678 | cy.get('#ages').should('read', ['20', '35', '15'])
679 |
680 | // equivalent to
681 | cy.get('#name').map('innerText').should('deep.equal', ['Joe Smith'])
682 | cy.get('#ages')
683 | .map('innerText')
684 | .should('deep.equal', ['20', '35', '15'])
685 | ```
686 |
687 | Using with regular expression or a mix of strings and regular expressions
688 |
689 | ```js
690 | cy.get('#name').should('read', /\sSmith$/)
691 | cy.get('#ages').should('read', [/^\d+$/, '35', '15'])
692 | ```
693 |
694 | The assertion fails if the number of elements does not match the expected number of strings.
695 |
696 | ### possess
697 |
698 | **Assertion**
699 |
700 | Checks if the subject has the given property. Can check the property value. Yields the original subject
701 |
702 | ```js
703 | // check if the subject has the "name" property
704 | cy.wrap({ name: 'Joe' }).should('possess', 'name')
705 | // yields the { name: 'Joe' } object
706 |
707 | // check if the subject has the "name" property with the value "Joe"
708 | cy.wrap({ name: 'Joe' }).should('possess', 'name', 'Joe')
709 | // yields the { name: 'Joe' } object
710 | ```
711 |
712 | The assertion supports deep object access using the dot notation
713 |
714 | ```js
715 | cy.wrap({ user: { name: 'Joe' } }).should(
716 | 'possess',
717 | 'user.name',
718 | 'Joe',
719 | )
720 | ```
721 |
722 | The assertion supports arrays using `[ ]` or simple dot notation
723 |
724 | ```js
725 | cy.wrap({ users: [{ name: 'Joe' }, { name: 'Jane' }] })
726 | .should('possess', 'users[0].name', 'Joe') // [] notation
727 | .and('possess', 'users.1.name', 'Jane') // simple dot notation
728 | ```
729 |
730 | You can also pass a predicate function instead of the value to check the property value against it.
731 |
732 | ```js
733 | const isDrinkingAge = (years) => years > 21
734 | cy.wrap({ age: 42 }).should('possess', 'age', isDrinkingAge)
735 | // yields the original subject
736 | ```
737 |
738 | 📺 You can watch this assertion explained in the video [The Possess Assertion From cypress-map Plugin](https://youtu.be/HHxkL-BPyjA)
739 |
740 | ### unique
741 |
742 | Confirms the items in the current subject array are unique (using a `Set` to check)
743 |
744 | ```js
745 | cy.wrap([1, 2, 3]).should('be.unique')
746 | cy.wrap([1, 2, 2]).should('not.be.unique')
747 | ```
748 |
749 | ## cy.invoke vs cy.map vs cy.mapInvoke
750 |
751 | Here are a few examples to clarify the different between the `cy.invoke`, `cy.map`, and `cy.mapInvoke` query commands, see [diff.cy.js](./cypress/e2e/diff.cy.js)
752 |
753 | ```js
754 | const list = ['apples', 'plums', 'bananas']
755 |
756 | // cy.invoke
757 | cy.wrap(list)
758 | // calls ".sort()" on the list
759 | .invoke('sort')
760 | .should('deep.equal', ['apples', 'bananas', 'plums'])
761 |
762 | // cy.mapInvoke
763 | cy.wrap(list)
764 | // calls ".toUpperCase()" on every string in the list
765 | .mapInvoke('toUpperCase')
766 | .should('deep.equal', ['APPLES', 'PLUMS', 'BANANAS'])
767 |
768 | // cy.map
769 | const reverse = (s) => s.split('').reverse().join('')
770 | cy.wrap(list)
771 | // reverses each string in the list
772 | .map(reverse)
773 | .should('deep.equal', ['selppa', 'smulp', 'sananab'])
774 | // grabs the "length" property from each string
775 | .map('length')
776 | .should('deep.equal', [6, 5, 7])
777 | ```
778 |
779 | ## Misc
780 |
781 | ### mapChain
782 |
783 | I have added another useful command (not a query!) to this package. It allows you to process items in the array subject one by one via synchronous, asynchronous, or `cy` command functions. This is because the common solution to fetch items using `cy.each`, for example does not work:
784 |
785 | ```js
786 | // fetch the users from a list of ids
787 | // 🚨 DOES NOT WORK
788 | cy.get(ids).each(id => cy.request('/users/' + id)).then(users => ...)
789 | // Nope, the yielded "users" result is ... still the "ids" subject
790 | // ✅ CORRECT SOLUTION
791 | cy.get(ids).mapChain(id => cy.request('/users/' + id)).then(users => ...)
792 | ```
793 |
794 | ## Types
795 |
796 | This package includes TypeScript command definitions for its custom commands in the file [commands/index.d.ts](./commands/index.d.ts). To use it from your JavaScript specs:
797 |
798 | ```js
799 | ///
800 | ```
801 |
802 | If you are using TypeScript, include this module in your types list
803 |
804 | ```json
805 | {
806 | "compilerOptions": {
807 | "types": ["cypress", "cypress-map"]
808 | }
809 | }
810 | ```
811 |
812 | ## The build process
813 |
814 | The source code is in the [src/commands](./src/commands/) folder. The build command produces ES5 code that goes into the `commands` folder (should not be checked into the source code control). The `package.json` in its NPM distribution includes `commands` plus the types from `src/commands/index.d.ts` file.
815 |
816 | ## See also
817 |
818 | - [cypress-should-really](https://github.com/bahmutov/cypress-should-really) has similar functional helpers for constructing the `should(callback)` function on the fly.
819 |
820 | **Note:** this module does not have `filter` method because Cypress API has query commands [cy.filter](https://on.cypress.io/filter) and [cy.invoke](https://on.cypress.io/invoke) that you can use to filter elements in a jQuery object or items in an array. See the examples in the [filter.cy.js](./cypress/e2e/filter.cy.js) spec. 📺 See video [Filter Elements And Items With Retries](https://youtu.be/70kRnoMuzds).
821 |
822 | ## Small print
823 |
824 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2022
825 |
826 | - [@bahmutov](https://twitter.com/bahmutov)
827 | - [glebbahmutov.com](https://glebbahmutov.com)
828 | - [blog](https://glebbahmutov.com/blog)
829 | - [videos](https://www.youtube.com/glebbahmutov)
830 | - [presentations](https://slides.com/bahmutov)
831 | - [cypress.tips](https://cypress.tips)
832 | - [Cypress Tips & Tricks Newsletter](https://cypresstips.substack.com/)
833 | - [my Cypress courses](https://cypress.tips/courses)
834 |
835 | License: MIT - do anything with the code, but don't blame me if it does not work.
836 |
837 | Support: if you find any problems with this module, email / tweet /
838 | [open issue](https://github.com/bahmutov/cypress-map/issues) on Github
839 |
--------------------------------------------------------------------------------
/cypress.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require('cypress')
2 | // https://github.com/bahmutov/cypress-split
3 | const cypressSplit = require('cypress-split')
4 |
5 | module.exports = defineConfig({
6 | e2e: {
7 | // baseUrl, etc
8 | viewportHeight: 200,
9 | viewportWidth: 200,
10 | supportFile: false,
11 | setupNodeEvents(on, config) {
12 | // implement node event listeners here
13 | // and load any plugins that require the Node environment
14 | cypressSplit(on, config)
15 | // IMPORTANT: return the config object
16 | return config
17 | },
18 | },
19 | })
20 |
--------------------------------------------------------------------------------
/cypress/dataset.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | All about electric cards
4 |
5 |
14 |
15 |
--------------------------------------------------------------------------------
/cypress/detach.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Santo
4 |
5 |
8 | Click to remove Joe
9 |
10 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/cypress/e2e/apply.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../src/commands'
5 |
6 | const double = (n) => n * 2
7 | const div = (a, b) => a / b
8 |
9 | describe('apply', () => {
10 | it('applies the given callback to the subject', () => {
11 | cy.wrap(100).apply(double).should('equal', 200)
12 | })
13 |
14 | it('applies with arguments (subject is last)', () => {
15 | cy.wrap(100).apply(div, 1000).should('equal', 10)
16 | cy.wrap(2).apply(Cypress._.add, 4).should('equal', 6)
17 | cy.wrap(8).apply(Cypress._.subtract, 4).should('equal', -4)
18 | })
19 | })
20 |
21 | describe('applyRight', () => {
22 | it('applies the given callback to the subject', () => {
23 | cy.wrap(100).applyRight(double).should('equal', 200)
24 | })
25 |
26 | it('applies with arguments (subject is first)', () => {
27 | cy.wrap(100).applyRight(div, 1000).should('equal', 0.1)
28 | cy.wrap(8).applyRight(Cypress._.subtract, 4).should('equal', 4)
29 | // same as
30 | cy.wrap(8)
31 | .apply((subject) => Cypress._.subtract(subject, 4))
32 | .should('equal', 4)
33 | })
34 | })
35 |
36 | describe('applyToFirst', () => {
37 | it('applies the given callback to the first item', () => {
38 | cy.wrap([100, 200]).applyToFirst(double).should('equal', 200)
39 | })
40 |
41 | it('applies with arguments (subject is last)', () => {
42 | cy.wrap([100]).applyToFirst(div, 1000).should('equal', 10)
43 | cy.wrap([2]).applyToFirst(Cypress._.add, 4).should('equal', 6)
44 | cy.wrap([8])
45 | .applyToFirst(Cypress._.subtract, 4)
46 | .should('equal', -4)
47 | })
48 |
49 | it('applies to the first element', () => {
50 | cy.wrap(Cypress.$('100
200
'))
51 | .applyToFirst((base, el) => parseInt(el.innerText, base), 10)
52 | .should('equal', 100)
53 | })
54 | })
55 |
56 | describe('applyToFirstRight', () => {
57 | it('applies the given callback to the subject', () => {
58 | cy.wrap([100]).applyToFirstRight(double).should('equal', 200)
59 | })
60 |
61 | it('applies with arguments (subject is first)', () => {
62 | cy.wrap([100]).applyToFirstRight(div, 1000).should('equal', 0.1)
63 | cy.wrap([8])
64 | .applyToFirstRight(Cypress._.subtract, 4)
65 | .should('equal', 4)
66 | // same as
67 | cy.wrap([8])
68 | .applyToFirst((subject) => Cypress._.subtract(subject, 4))
69 | .should('equal', 4)
70 | })
71 |
72 | it('applies to the first element', () => {
73 | cy.wrap(Cypress.$('100
200
'))
74 | .applyToFirstRight(
75 | (el, base) => parseInt(el.innerText, base),
76 | 10,
77 | )
78 | .should('equal', 100)
79 | })
80 | })
81 |
--------------------------------------------------------------------------------
/cypress/e2e/as-env.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('does not have saved value yet', () => {
7 | expect(Cypress.env('greeting'), 'greeting is undefined').to.be
8 | .undefined
9 | })
10 |
11 | it('saves value in this test', () => {
12 | cy.wrap('hello, world')
13 | .asEnv('greeting')
14 | // the value is yielded to the next command
15 | .should('equal', 'hello, world')
16 | })
17 |
18 | it('saved value is available in this test', () => {
19 | expect(Cypress.env('greeting'), 'greeting').to.equal('hello, world')
20 | })
21 |
22 | it('saved value is available in this test too', () => {
23 | expect(Cypress.env('greeting'), 'greeting').to.equal('hello, world')
24 | })
25 |
26 | it('retries like all queries', () => {
27 | const person = {}
28 | setTimeout(() => {
29 | person.name = 'Joe'
30 | }, 1000)
31 | cy.wrap(person)
32 | .asEnv('person')
33 | .its('name')
34 | .should('equal', 'Joe')
35 | .then(() => {
36 | expect(Cypress.env('person'), 'person').to.have.property(
37 | 'name',
38 | 'Joe',
39 | )
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/cypress/e2e/assertions/possess.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../../src/commands'
5 |
6 | describe('have.property assertion', () => {
7 | it('yields the property value', () => {
8 | cy.wrap({ foo: 'bar' })
9 | .should('have.property', 'foo')
10 | .should('equal', 'bar')
11 | })
12 | })
13 |
14 | describe('should possess assertion', () => {
15 | it('checks if the subject possesses a property', () => {
16 | cy.wrap({ foo: 'bar' })
17 | .should('possess', 'foo')
18 | .and('not.possess', 'fooz')
19 | })
20 |
21 | it('yields the subject', () => {
22 | cy.wrap({ foo: 'bar' })
23 | .should('possess', 'foo')
24 | .should('deep.equal', { foo: 'bar' })
25 | })
26 |
27 | it('checks the property value', () => {
28 | cy.wrap({ foo: 'bar' })
29 | .should('possess', 'foo', 'bar')
30 | // still yields the original subject
31 | .should('deep.equal', { foo: 'bar' })
32 |
33 | cy.wrap({ foo: 42 }).should('possess', 'foo', 42)
34 | })
35 |
36 | it('checks the property value using not', () => {
37 | cy.wrap({ foo: 'bar' }).should('not.possess', 'foo', 'BAR')
38 | })
39 |
40 | it('checks the property presence using not and value', () => {
41 | cy.wrap({ foo: 'bar' }).should(
42 | 'not.possess',
43 | 'some-other-name',
44 | 'BAR',
45 | )
46 | })
47 |
48 | it('supports nested properties', () => {
49 | const person = { name: { first: 'Joe' } }
50 | cy.wrap(person)
51 | .should('possess', 'name.first', 'Joe')
52 | // still yields the original subject
53 | .should('deep.equal', person)
54 | })
55 |
56 | it('supports arrays', () => {
57 | const counts = [1, 2, 3]
58 | cy.wrap(counts)
59 | .should('possess', '1', 2)
60 | // still yields the original subject
61 | .should('deep.equal', counts)
62 | .should('possess', 'length', 3)
63 | })
64 |
65 | it('supports mixing arrays and objects', () => {
66 | cy.wrap([
67 | { sum: 42 },
68 | { sum: 101 },
69 | { sum: { errors: ['Invalid operation'] } },
70 | ]).should('possess', '2.sum.errors.0', 'Invalid operation')
71 | })
72 |
73 | it('supports array bracket notation', () => {
74 | cy.wrap([
75 | { sum: 42 },
76 | { sum: 101 },
77 | { sum: { errors: ['Invalid operation'] } },
78 | ]).should('possess', '[2].sum.errors[0]', 'Invalid operation')
79 | })
80 |
81 | // unskip to see the thrown error
82 | it.skip('throws an error', () => {
83 | cy.wrap({ foo: 'bar' })
84 | // @ts-ignore
85 | .should('possess', 'foo', 'bar', 123)
86 | .should('deep.equal', { foo: 'bar' })
87 | })
88 |
89 | context('with a predicate', () => {
90 | it('checks the property value against the predicate function', () => {
91 | cy.wrap({ foo: 'bar' })
92 | .should('possess', 'foo', (val) => val === 'bar')
93 | // keeps the subject
94 | .and('deep.equal', { foo: 'bar' })
95 | })
96 |
97 | it('negates the predicate function', () => {
98 | const isDrinkingAge = (years) => years > 21
99 | cy.wrap({ age: 42 }).should('possess', 'age', isDrinkingAge)
100 | cy.wrap({ age: 1 }).should('not.possess', 'age', isDrinkingAge)
101 | })
102 | })
103 | })
104 |
--------------------------------------------------------------------------------
/cypress/e2e/assertions/read.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../../src/commands'
5 |
6 | it('compares text', () => {
7 | cy.visit('cypress/index.html')
8 | cy.get('li').first().should('read', 'first')
9 | cy.get('li').should('read', ['first', 'second', 'third'])
10 | // passes after delay
11 | cy.get('li').should('read', [
12 | 'first',
13 | 'second',
14 | 'third',
15 | 'fourth',
16 | 'fifth',
17 | ])
18 | })
19 |
20 | it('supports a single regular expression', () => {
21 | cy.visit('cypress/index.html')
22 | cy.get('li').first().should('read', /FIRST/i)
23 | })
24 |
25 | it('supports a mixture of strings and regular expressions', () => {
26 | cy.visit('cypress/index.html')
27 | cy.get('li').should('read', ['first', /^sec/, 'third'])
28 | })
29 |
30 | it(
31 | 'fails when the number of elements does not match',
32 | { defaultCommandTimeout: 2000 },
33 | () => {
34 | cy.on('fail', (err) => {
35 | console.log(err.message)
36 | const validFailureMessage =
37 | 'Timed out retrying after 2000ms: expected first, second, third, fourth, fifth to read first, second'
38 | if (err.message !== validFailureMessage) {
39 | throw err
40 | }
41 | })
42 |
43 | cy.visit('cypress/index.html')
44 | // try using the wrong number of elements
45 | cy.get('li').should('read', ['first', 'second'])
46 | },
47 | )
48 |
--------------------------------------------------------------------------------
/cypress/e2e/at.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('cy.eq yields the jQuery object', () => {
7 | cy.visit('cypress/index.html')
8 | cy.get('.matching')
9 | .eq(1)
10 | .should('satisfy', Cypress.dom.isJquery)
11 | .invoke('text')
12 | .should('equal', 'third')
13 | cy.get('.matching')
14 | .eq(2)
15 | .should('satisfy', Cypress.dom.isJquery)
16 | .and('have.length.above', 0)
17 | .invoke('text')
18 | .should('equal', 'fourth')
19 | })
20 |
21 | it('cy.at yields the DOM element', () => {
22 | cy.visit('cypress/index.html')
23 | cy.get('.matching')
24 | .at(1)
25 | .should('satisfy', Cypress.dom.isElement)
26 | .its('innerText')
27 | .should('equal', 'third')
28 | cy.get('.matching')
29 | .at(2)
30 | .should('satisfy', Cypress.dom.isElement)
31 | .its('innerText')
32 | .should('equal', 'fourth')
33 | })
34 |
35 | it('cy.at yields the last DOM element for index -1', () => {
36 | cy.visit('cypress/index.html')
37 | cy.get('#items li').at(-1).its('innerText').should('equal', 'fifth')
38 | })
39 |
40 | it('cy.at yields an element in the array', () => {
41 | cy.wrap([1, 2, 3]).at(2).should('equal', 3)
42 | })
43 |
44 | it('cy.at the last element of the array for -1', () => {
45 | cy.wrap([1, 2, 3]).at(-1).should('equal', 3)
46 | cy.wrap([1, 2, 3]).at(-2).should('equal', 2)
47 | })
48 |
--------------------------------------------------------------------------------
/cypress/e2e/chain-with-assertions.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | const double = (n) => n * 2
5 |
6 | // @ts-ignore
7 | Cypress.Commands.addQuery('apply', (fn) => (n) => fn(n))
8 |
9 | // https://github.com/cypress-io/cypress/issues/25134
10 | it.skip('fails to retry the cy.its', () => {
11 | const list = []
12 | cy.wrap(list)
13 | .its(0) // first item
14 | // this assertion breaks the query chain retries
15 | // it never "sees" the new number 5
16 | // because it never retries cy.its above
17 | .should('be.a', 'number')
18 | .apply(double)
19 | .should('equal', 10)
20 |
21 | setTimeout(() => {
22 | list[0] = 1
23 | }, 1000)
24 |
25 | setTimeout(() => {
26 | list[0] = 5
27 | }, 2000)
28 | })
29 |
30 | it('retries queries with assertions', () => {
31 | const list = []
32 | cy.wrap(list)
33 | // several queries (without assertion)
34 | .its(0) // first item
35 | .apply(double)
36 | .should('equal', 10)
37 |
38 | setTimeout(() => {
39 | list[0] = 1
40 | }, 1000)
41 |
42 | setTimeout(() => {
43 | list[0] = 5
44 | }, 2000)
45 | })
46 |
--------------------------------------------------------------------------------
/cypress/e2e/detach.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | describe('detach', () => {
8 | beforeEach(() => {
9 | cy.visit('cypress/detach.html')
10 | })
11 |
12 | it('removes the element from the DOM', () => {
13 | cy.contains('#name', 'Santo').should(
14 | 'satisfy',
15 | Cypress.dom.isDetached,
16 | )
17 | })
18 |
19 | it('removes after click', () => {
20 | // grab the initial element to prepare
21 | cy.get('#name2').then(($el) => {
22 | cy.contains('Click to remove Joe').click()
23 | // confirm the old element is gone
24 | cy.wrap(null).should(() => {
25 | expect($el[0], 'element is gone').to.satisfy(
26 | Cypress.dom.isDetached,
27 | )
28 | })
29 | // the new element should be quickly there
30 | cy.contains('#name2', 'Anna', { timeout: 0 })
31 | })
32 | })
33 |
34 | it('removes after click (query)', () => {
35 | cy.contains('Click to remove Joe').click()
36 | cy.detaches('#name2')
37 | // confirm the old element is gone
38 | // the new element should be quickly there
39 | cy.contains('#name2', 'Anna', { timeout: 0 })
40 | })
41 |
42 | it('removes after click (split query)', () => {
43 | cy.get('#name2').asEnv('name')
44 | cy.contains('Click to remove Joe').click()
45 | cy.detaches('@name')
46 | // confirm the old element is gone
47 | // the new element should be quickly there
48 | cy.contains('#name2', 'Anna', { timeout: 0 })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/cypress/e2e/diff.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | // this spec shows the difference between
7 | // cy.invoke, cy.mapInvoke, and cy.map
8 |
9 | it('invokes a method on the subject', () => {
10 | const list = ['apples', 'plums', 'bananas']
11 | cy.wrap(list)
12 | // calls ".sort()" on the list
13 | .invoke('sort')
14 | .should('deep.equal', ['apples', 'bananas', 'plums'])
15 | })
16 |
17 | it('invokes a method on the items in the subject', () => {
18 | const list = ['apples', 'plums', 'bananas']
19 | cy.wrap(list)
20 | // calls ".toUpperCase()" on every string in the list
21 | .mapInvoke('toUpperCase')
22 | .should('deep.equal', ['APPLES', 'PLUMS', 'BANANAS'])
23 | })
24 |
25 | it('maps each item by running it through the callback or property', () => {
26 | const list = ['apples', 'plums', 'bananas']
27 | const reverse = (s) => s.split('').reverse().join('')
28 | cy.wrap(list)
29 | // reverses each string in the list
30 | .map(reverse)
31 | .should('deep.equal', ['selppa', 'smulp', 'sananab'])
32 | // grabs the "length" property from each string
33 | .map('length')
34 | .should('deep.equal', [6, 5, 7])
35 | })
36 |
--------------------------------------------------------------------------------
/cypress/e2e/difference.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | chai.config.truncateThreshold = 300
7 |
8 | describe('objects', () => {
9 | it('compares two different objects', () => {
10 | cy.wrap({ name: 'Joe', age: 20 })
11 | .difference({ name: 'Joe', age: 30 })
12 | .should('deep.equal', { age: { actual: 20, expected: 30 } })
13 | })
14 |
15 | it('compares two identical objects', () => {
16 | cy.wrap({ name: 'Joe', age: 20 })
17 | .difference({ name: 'Joe', age: 20 })
18 | .should('deep.equal', {})
19 | })
20 |
21 | it('retries when comparing', () => {
22 | const p = { name: 'Joe' }
23 | setTimeout(() => {
24 | p.age = 20
25 | }, 200)
26 | cy.wrap(p)
27 | .difference({ name: 'Joe', age: 20 })
28 | .should('deep.equal', {})
29 | })
30 |
31 | it('reports a missing property', () => {
32 | cy.wrap({ name: 'Joe', age: 20 })
33 | .difference({ name: 'Joe', age: 20, extra: true })
34 | .should('deep.equal', {
35 | extra: { missing: true, expected: true },
36 | })
37 | })
38 |
39 | it('reports extra property', () => {
40 | cy.wrap({ name: 'Joe', age: 20, extra: true })
41 | .difference({ name: 'Joe', age: 20 })
42 | .should('deep.equal', {
43 | extra: { extra: true, actual: true },
44 | })
45 | })
46 | })
47 |
48 | describe('predicates', () => {
49 | it('allows to use custom predicates', () => {
50 | cy.wrap({ name: 'Joe', age: 20 })
51 | .difference({ name: 'Joe', age: (n) => n > 15 })
52 | .should('deep.equal', {})
53 | })
54 |
55 | it('checks the number', () => {
56 | cy.wrap({ name: 'Joe', age: 20 })
57 | .difference({ name: 'Joe', age: Cypress._.isNumber })
58 | .should('deep.equal', {})
59 | })
60 | })
61 |
62 | describe('compares two arrays', () => {
63 | it('compares two equal arrays', () => {
64 | cy.wrap([1, 2, 3]).difference([1, 2, 3]).should('be.empty')
65 | })
66 |
67 | it('compares two different arrays', () => {
68 | cy.wrap([1, 2, 3])
69 | .difference([1, 2, 4])
70 | .should('deep.equal', { 2: { actual: 3, expected: 4 } })
71 | })
72 |
73 | it('compares two arrays that become equal', () => {
74 | const list = []
75 | cy.wrap(list).difference([1, 2, 3]).should('be.empty')
76 | setTimeout(() => {
77 | list.push(1, 2, 3)
78 | }, 200)
79 | })
80 |
81 | it('compares two arrays with predicates', () => {
82 | const list = []
83 | cy.wrap(list)
84 | .difference([
85 | Cypress._.isNumber,
86 | (x) => x === 'foo',
87 | (x) => x === 'bar',
88 | ])
89 | .should('be.empty')
90 | setTimeout(() => {
91 | list.push(1)
92 | }, 200)
93 | setTimeout(() => {
94 | list.push('foo')
95 | }, 400)
96 | setTimeout(() => {
97 | list.push('bar')
98 | }, 600)
99 | })
100 |
101 | it('checks 3 strings in an array', () => {
102 | const list = []
103 | cy.wrap(list)
104 | .difference([
105 | Cypress._.isString,
106 | Cypress._.isString,
107 | Cypress._.isString,
108 | ])
109 | .should('be.empty')
110 | setTimeout(() => {
111 | list.push('foo', 'bar', 'baz')
112 | }, 600)
113 | })
114 |
115 | it('checks each item using deep equal', () => {
116 | const list = []
117 | cy.wrap(list)
118 | .difference([
119 | { name: 'foo', age: 20 },
120 | { name: 'bar', age: 30 },
121 | ])
122 | .should('be.empty')
123 | setTimeout(() => {
124 | list.push({ name: 'foo', age: 20 })
125 | }, 200)
126 | setTimeout(() => {
127 | list.push({ name: 'bar', age: 30 })
128 | }, 400)
129 | })
130 | })
131 |
132 | describe('check items in an array using schema object', () => {
133 | it('checks each item against predicates', () => {
134 | const list = []
135 | cy.wrap(list)
136 | .difference({
137 | name: Cypress._.isString,
138 | age: (age) => age > 15,
139 | })
140 | .should('be.empty')
141 | setTimeout(() => {
142 | list.push({ name: 'foo', age: 20 })
143 | }, 200)
144 | setTimeout(() => {
145 | list.push({ name: 'bar', age: 30 })
146 | }, 400)
147 | })
148 |
149 | it('checks each item against predicates (different value)', () => {
150 | const list = [
151 | {
152 | name: 'Joe',
153 | age: 20,
154 | },
155 | {
156 | name: 'Anna',
157 | age: 20,
158 | },
159 | ]
160 | cy.wrap(list)
161 | .difference({
162 | name: 'Joe',
163 | age: (age) => age > 15,
164 | })
165 | .should('deep.equal', {
166 | 1: {
167 | name: { actual: 'Anna', expected: 'Joe' },
168 | },
169 | })
170 | })
171 | })
172 |
--------------------------------------------------------------------------------
/cypress/e2e/elements.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | chai.config.truncateThreshold = 300
7 |
8 | describe('elements', () => {
9 | it('loads the list of elements', () => {
10 | cy.visit('cypress/list.html')
11 | cy.elements('#tasks li', '.name', '.k').should('deep.equal', [
12 | ['Item A', '1'],
13 | ['Item B', '2'],
14 | ['Item C', '3'],
15 | ['Item D', '4'],
16 | ])
17 | })
18 |
19 | it('loads the list of elements with order', () => {
20 | cy.visit('cypress/list.html')
21 | cy.elements('#tasks li', '.k', '.name').should('deep.equal', [
22 | ['1', 'Item A'],
23 | ['2', 'Item B'],
24 | ['3', 'Item C'],
25 | ['4', 'Item D'],
26 | ])
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/cypress/e2e/filter-table.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | chai.config.truncateThreshold = 500
8 |
9 | it(
10 | 'controls the network data and uses cy.table',
11 | { viewportHeight: 600, viewportWidth: 600 },
12 | () => {
13 | cy.intercept('/people', {
14 | fixture: 'people.json',
15 | delay: 1000,
16 | }).as('people')
17 | cy.visit('cypress/e2e/filter-table/index.html')
18 | cy.get('#people tbody')
19 | // do not specify height
20 | .table(0, 0, 1)
21 | .apply(Cypress._.flatten)
22 | .should('deep.equal', ['Peter', 'Pete', 'Mary', 'Mary-Ann'])
23 | cy.get('input#by-name').type('Mary')
24 | cy.get('#people tbody')
25 | // do not specify height
26 | .table(0, 0, 1)
27 | .apply(Cypress._.flatten)
28 | .should('deep.equal', ['Mary', 'Mary-Ann'])
29 | },
30 | )
31 |
--------------------------------------------------------------------------------
/cypress/e2e/filter-table/filter.js:
--------------------------------------------------------------------------------
1 | const initialList = [
2 | {
3 | name: 'Joe Z',
4 | date: '1990-02-25',
5 | age: 20,
6 | },
7 | {
8 | name: 'Anna',
9 | date: '2010-03-26',
10 | age: 37,
11 | },
12 | {
13 | name: 'Dave',
14 | date: '1997-12-23',
15 | age: 25,
16 | },
17 | {
18 | name: 'Joseph',
19 | date: '2001-01-24',
20 | age: 30,
21 | },
22 | {
23 | name: 'Jonathan',
24 | date: '2004-02-01',
25 | age: 30,
26 | },
27 | ]
28 |
29 | let list = initialList
30 | let filtered = list
31 | let nameFilter
32 |
33 | // utility methods
34 | const itemToRow = (item) =>
35 | `${item.name} ${item.date} ${item.age} `
36 | const listToHtml = (list) => list.map(itemToRow).join('\n')
37 |
38 | // the main "render" method
39 | const render = () => {
40 | if (nameFilter) {
41 | filtered = list.filter((item) => item.name.includes(nameFilter))
42 | } else {
43 | filtered = list
44 | }
45 | document.getElementById('people-data').innerHTML = listToHtml(filtered)
46 | }
47 |
48 | // set the initial table
49 | fetch('/people').then((r) => {
50 | if (r.ok) {
51 | r.json().then((data) => {
52 | list = data
53 | render()
54 | })
55 | } else {
56 | // use the default data
57 | list = initialList
58 | render()
59 | }
60 | })
61 |
62 | document.getElementById('by-name').addEventListener('input', (e) => {
63 | nameFilter = e.target.value
64 | setTimeout(() => {
65 | render()
66 | }, 1000)
67 | })
68 |
--------------------------------------------------------------------------------
/cypress/e2e/filter-table/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Filter Table
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 | Name
17 | Date
18 | Age
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/cypress/e2e/filter-table/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | height: 100vh;
7 | margin: 0;
8 | }
9 |
10 | table {
11 | border-spacing: 1px;
12 | }
13 |
14 | table td {
15 | border: 1px solid black;
16 | padding: 5px;
17 | }
18 |
19 | #sort-by-date {
20 | margin: 0px 10px;
21 | }
22 |
23 | .buttons {
24 | margin-top: 10px;
25 | display: flex;
26 | }
27 |
--------------------------------------------------------------------------------
/cypress/e2e/filter.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | chai.config.truncateThreshold = 500
8 |
9 | // 📺 watch these examples explained
10 | // in the video "Filter Elements And Items With Retries"
11 | // https://youtu.be/70kRnoMuzds
12 |
13 | describe('Filter examples', () => {
14 | it('filters array items', () => {
15 | const people = [
16 | {
17 | name: 'Joe',
18 | },
19 | {
20 | name: 'Mary',
21 | },
22 | ]
23 | // simulate a dynamic list by adding new records
24 | setTimeout(() => {
25 | people.push(
26 | {
27 | name: 'Ann',
28 | },
29 | {
30 | name: 'Kent',
31 | },
32 | )
33 | }, 1000)
34 |
35 | cy.wrap(people)
36 | // keep only the even items
37 | // the cy.invoke query command will be retried
38 | // https://on.cypress.io/invoke
39 | // Note: we cannot use cy.filter because the subject is not jQuery
40 | .invoke('filter', (x, k) => k % 2 === 0)
41 | .should('deep.equal', [
42 | {
43 | name: 'Joe',
44 | },
45 | {
46 | name: 'Ann',
47 | },
48 | ])
49 | })
50 |
51 | it(
52 | 'filters elements in a jQuery object',
53 | { viewportHeight: 600, viewportWidth: 600 },
54 | () => {
55 | cy.intercept('/people', {
56 | fixture: 'people.json',
57 | delay: 1000,
58 | }).as('people')
59 | cy.visit('cypress/e2e/filter-table/index.html')
60 | cy.get('table tbody tr')
61 | // cy.filter query command is retried
62 | // https://on.cypress.io/filter
63 | .filter((k, el) => k % 2 === 0)
64 | .find('td:first') // query
65 | .map('innerText') // query
66 | .should('deep.equal', ['Peter', 'Mary'])
67 | },
68 | )
69 | })
70 |
--------------------------------------------------------------------------------
/cypress/e2e/find-one.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | describe('findOne', () => {
8 | it('yields the element using callback fn', () => {
9 | const values = []
10 | setTimeout(() => {
11 | values.push(1)
12 | values.push(2)
13 | values.push(3)
14 | }, 1000)
15 | cy.wrap(values)
16 | .findOne((n) => n === 3)
17 | .should('equal', 3)
18 | })
19 |
20 | it('shows the callback function name if any', () => {
21 | const values = [1, 2]
22 | setTimeout(() => {
23 | values.push(3)
24 | }, 1000)
25 | cy.wrap(values)
26 | .findOne(function is3(n) {
27 | return n === 3
28 | })
29 | .should('equal', 3)
30 | })
31 |
32 | it('shows the callback arrow function name if any', () => {
33 | const values = [1, 2]
34 | const equals3 = (n) => n === 3
35 | setTimeout(() => {
36 | values.push(3)
37 | }, 1000)
38 | cy.wrap(values).findOne(equals3).should('equal', 3)
39 | })
40 |
41 | it('finds using property', () => {
42 | const values = [{ name: 'Joe' }]
43 | setTimeout(() => {
44 | values.push({ name: 'Anna' })
45 | }, 1000)
46 | cy.wrap(values).findOne({ name: 'Anna' }).should('exist')
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/cypress/e2e/get-in-order.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('queries the elements in the given order', () => {
7 | cy.visit('cypress/index.html')
8 | cy.getInOrder('li:contains("second")', 'li:contains("first")')
9 | .map('innerText')
10 | .should('deep.equal', ['second', 'first'])
11 | })
12 |
13 | it('retries each selector', () => {
14 | cy.visit('cypress/index.html')
15 | cy.getInOrder(
16 | 'li:contains("fifth")',
17 | 'li:contains("first")',
18 | 'li:contains("third")',
19 | )
20 | .should('have.length', 3)
21 | .map('innerText')
22 | .should('deep.equal', ['fifth', 'first', 'third'])
23 | })
24 |
25 | it('OR selector', () => {
26 | cy.visit('cypress/index.html')
27 | // the OR selector is a comma and it returns
28 | // elements in the order they appear in the DOM
29 | // NOT in the order of the selectors
30 | cy.get(
31 | 'li:contains("fifth"),li:contains("first"),li:contains("third")',
32 | )
33 | .should('have.length', 3)
34 | .map('innerText')
35 | // the order is the same as in the DOM
36 | .should('deep.equal', ['first', 'third', 'fifth'])
37 | })
38 |
39 | // https://github.com/bahmutov/cypress-map/issues/144
40 | it('spreads an array of strings', () => {
41 | cy.visit('cypress/index.html')
42 | const selectors = [
43 | 'li:contains("fifth")',
44 | 'li:contains("first")',
45 | 'li:contains("third")',
46 | ]
47 | cy.getInOrder(selectors)
48 | .should('have.length', 3)
49 | .map('innerText')
50 | .should('deep.equal', ['fifth', 'first', 'third'])
51 | })
52 |
53 | // https://github.com/bahmutov/cypress-map/issues/219
54 | it(
55 | 'uses the current subject',
56 | { viewportWidth: 500, viewportHeight: 500 },
57 | () => {
58 | cy.visit('cypress/e2e/filter-table/index.html')
59 | cy.get('table tbody tr').should('have.length', 5)
60 | cy.get('table tbody tr:eq(2)')
61 | .should('include.text', 'Dave')
62 | // get the age and the name cells
63 | .getInOrder('td:eq(2)', 'td:eq(0)')
64 | .map('innerText')
65 | .should('deep.equal', ['25', 'Dave'])
66 | },
67 | )
68 |
--------------------------------------------------------------------------------
/cypress/e2e/import-library.js:
--------------------------------------------------------------------------------
1 | import '../../commands'
2 |
--------------------------------------------------------------------------------
/cypress/e2e/instance.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('checks instance of array', () => {
7 | expect([1, 2, 3]).to.be.an.instanceOf(Array)
8 | })
9 |
10 | it('works before cy.map', () => {
11 | cy.wrap(['one', 'a', 'four']).should('be.an.instanceOf', Array)
12 | })
13 |
14 | it('maps to an array', () => {
15 | cy.wrap(['one', 'a', 'four'])
16 | .should('be.an.instanceOf', Array)
17 | .map('length')
18 | .should('deep.equal', [3, 1, 4])
19 | .and('be.an', 'array')
20 | .and('be.an.instanceOf', Array)
21 | })
22 |
--------------------------------------------------------------------------------
/cypress/e2e/invoke-first.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | describe('cy.invokeFirst', () => {
8 | expect('invokeFirst' in cy).to.be.true
9 |
10 | it('calls the given method on the first item', () => {
11 | cy.wrap(
12 | [
13 | {
14 | getName: cy.stub().as('first').returns('Joe'),
15 | },
16 | {
17 | getName: cy.stub().as('second').returns('Mary'),
18 | },
19 | ],
20 | { log: false },
21 | )
22 | .invokeFirst('getName')
23 | .should('equal', 'Joe')
24 |
25 | cy.log('**confirm the first item was called**')
26 | cy.get('@first').should('have.been.calledOnce')
27 | cy.get('@second').should('not.have.been.called')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/cypress/e2e/invoke-once.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | describe('invoke vs invokeOnce', () => {
8 | expect('invokeOnce' in cy).to.be.true
9 |
10 | const app = {
11 | fetchName() {
12 | return new Promise((resolve) => {
13 | setTimeout(() => {
14 | resolve('My App')
15 | }, 1000)
16 | })
17 | },
18 |
19 | add(a, b) {
20 | return Promise.resolve(a + b)
21 | },
22 | }
23 |
24 | it('cy.then yields the resolved value', () => {
25 | // call the method ourselves from "cy.then"
26 | cy.wrap(app)
27 | .then((app) => app.fetchName())
28 | .should('equal', 'My App')
29 | })
30 |
31 | it('cy.invoke yields the promise', () => {
32 | cy.wrap(app)
33 | .invoke('fetchName')
34 | .should('satisfy', (x) => typeof x.then === 'function')
35 | // then we can check the resolved value
36 | .then((s) => expect(s).to.equal('My App'))
37 |
38 | // we can use a truthy assertion to "wait" for the promise
39 | cy.wrap(app)
40 | .invoke('fetchName')
41 | .should('be.ok')
42 | // and then we can check the resolved value
43 | .then((s) => expect(s).to.equal('My App'))
44 | })
45 |
46 | it('calls the method and yields the resolved value', () => {
47 | cy.wrap(app).invokeOnce('fetchName').should('equal', 'My App')
48 | })
49 |
50 | it('passes method arguments', () => {
51 | cy.wrap(app).invokeOnce('add', 2, 3).should('equal', 5)
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/cypress/e2e/json-attribute/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | First person
4 |
5 |
6 |
--------------------------------------------------------------------------------
/cypress/e2e/json-attribute/spec.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 | import '../../../src/commands'
4 |
5 | // implementation similar to "Json data attribute" recipe
6 | // from https://glebbahmutov.com/cypress-examples
7 |
8 | it('compares the parsed data attribute object', () => {
9 | cy.visit('cypress/e2e/json-attribute/index.html')
10 | // grab the element's attribute "data-field"
11 | // convert it into a JSON object
12 | // and grab its "age" property -> should be equal 10
13 | cy.get('#person')
14 | .invoke('attr', 'data-field')
15 | .apply(JSON.parse)
16 | .its('age')
17 | .should('equal', 10)
18 | })
19 |
--------------------------------------------------------------------------------
/cypress/e2e/log-examples.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | chai.config.truncateThreshold = 200
7 |
8 | describe('cy.print', () => {
9 | it('prints the user age', () => {
10 | cy.visit('cypress/log-examples.html')
11 | cy.contains('#age', /\d+/)
12 | .invoke('text')
13 | .print('my age is %d')
14 | .then(Number)
15 | // what's my age again?
16 | .should('be.within', 1, 99)
17 | })
18 |
19 | it('prints the object', () => {
20 | cy.intercept('/users', {
21 | body: [
22 | { name: 'Joe', age: 1, role: 'student' },
23 | { name: 'Ann', age: 2, role: 'student' },
24 | { name: 'Mary', age: 3, role: 'student' },
25 | ],
26 | }).as('users')
27 | cy.visit('cypress/log-examples.html')
28 | cy.wait('@users').its('response.body').print().should('be.an', 'array')
29 | })
30 |
31 | it('prints the length and the first object', () => {
32 | cy.intercept('/users', {
33 | body: [
34 | { name: 'Joe', age: 1, role: 'student' },
35 | { name: 'Ann', age: 2, role: 'student' },
36 | { name: 'Mary', age: 3, role: 'student' },
37 | ],
38 | }).as('users')
39 | cy.visit('cypress/log-examples.html')
40 | cy.wait('@users')
41 | .its('response.body')
42 | .print('list with {0.length} users')
43 | .print('first user {0.0.name}')
44 | .should('be.an', 'array')
45 | })
46 |
47 | it('prints using custom format callback', () => {
48 | cy.intercept('/users', {
49 | body: [
50 | { name: 'Joe', age: 1, role: 'student' },
51 | { name: 'Ann', age: 2, role: 'student' },
52 | { name: 'Mary', age: 3, role: 'student' },
53 | ],
54 | }).as('users')
55 | cy.visit('cypress/log-examples.html')
56 | cy.wait('@users')
57 | .its('response.body')
58 | .print((list) => `first names only ${list.map((l) => l.name).join(',')}`)
59 | .should('be.an', 'array')
60 | })
61 |
62 | it('retries', () => {
63 | const person = {
64 | name: 'Joe',
65 | }
66 | cy.wrap(person)
67 | .print('first name is {0.name}')
68 | .its('name')
69 | .should('equal', 'Ann')
70 | setTimeout(() => {
71 | person.name = 'Ann'
72 | }, 1000)
73 | })
74 | })
75 |
76 | describe.skip('cy.log', () => {
77 | it('logs the user age (NOT)', () => {
78 | cy.visit('cypress/log-examples.html')
79 | cy.contains('#age', /\d+/)
80 | .invoke('text')
81 | .then(console.log)
82 | .then(Number)
83 | // what's my age again?
84 | .should('be.within', 1, 99)
85 | })
86 |
87 | it('retries (NOT)', () => {
88 | const person = {}
89 | cy.wrap(person)
90 | // @ts-ignore
91 | .log()
92 | .its('name')
93 | .should('equal', 'Ann')
94 | setTimeout(() => {
95 | person.name = 'Ann'
96 | }, 1000)
97 | })
98 |
99 | it('logs the object (NOT)', () => {
100 | cy.intercept('/users', {
101 | body: [
102 | { name: 'Joe', age: 1, role: 'student' },
103 | { name: 'Ann', age: 2, role: 'student' },
104 | { name: 'Mary', age: 3, role: 'student' },
105 | ],
106 | }).as('users')
107 | cy.visit('cypress/log-examples.html')
108 | cy.wait('@users').its('response.body').should('be.an', 'array')
109 | })
110 | })
111 |
--------------------------------------------------------------------------------
/cypress/e2e/make.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('uses a constructor in a query step', () => {
7 | cy.wrap('Jan 1, 2019')
8 | .make(Date)
9 | .invoke('getFullYear')
10 | .should('equal', 2019)
11 | })
12 |
13 | it('retries', () => {
14 | const list = []
15 | cy.wrap(list)
16 | .its(0)
17 | .make(Date)
18 | .invoke('getFullYear')
19 | .should('equal', 2019)
20 | setTimeout(() => {
21 | list[0] = 'Jan 1, 2019'
22 | }, 1000)
23 | })
24 |
--------------------------------------------------------------------------------
/cypress/e2e/map-chain.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 | import '../../commands'
4 |
5 | const doubleIt = (n) => n + n
6 |
7 | async function asyncDouble(n) {
8 | await Cypress.Promise.delay(500)
9 | return n + n
10 | }
11 |
12 | it('maps each item using sync function', () => {
13 | cy.wrap([1, 2, 3])
14 | .mapChain(doubleIt)
15 | .should('deep.equal', [2, 4, 6])
16 | })
17 |
18 | it('maps each item using async function', () => {
19 | cy.wrap([1, 2, 3])
20 | .mapChain(asyncDouble)
21 | .should('deep.equal', [2, 4, 6])
22 | })
23 |
24 | it('maps each item to the cy yielded value of the returned chain', () => {
25 | cy.wrap([1, 2, 3])
26 | .mapChain((x) => cy.wrap(x).then(doubleIt))
27 | .should('deep.equal', [2, 4, 6])
28 | })
29 |
30 | it('maps each item using a Promise', () => {
31 | cy.wrap([1, 2, 3])
32 | .mapChain((x) => {
33 | return new Promise((resolve) => {
34 | setTimeout(() => {
35 | resolve(doubleIt(x))
36 | }, 500)
37 | })
38 | })
39 | .should('deep.equal', [2, 4, 6])
40 | })
41 |
42 | // https://github.com/bahmutov/cypress-map/issues/27
43 | it('maps each item to the cy yielded value without returned chain', () => {
44 | cy.wrap([1, 2, 3])
45 | .mapChain((x) => {
46 | // do not return the command chain
47 | // but it still should grab the resolved value
48 | cy.wrap(x).then(doubleIt)
49 | })
50 | .should('deep.equal', [2, 4, 6])
51 | })
52 |
53 | it('works for an empty array', () => {
54 | cy.wrap([])
55 | .mapChain(() => {
56 | throw new Error('Should not call mapChain callback')
57 | })
58 | .should('deep.equal', [])
59 | })
60 |
61 | it('works for a filtered empty array', () => {
62 | cy.wrap([1, 2, 3])
63 | .invoke('filter', () => false)
64 | .mapChain(() => {
65 | throw new Error('Should not call mapChain callback')
66 | })
67 | .should('deep.equal', [])
68 | })
69 |
--------------------------------------------------------------------------------
/cypress/e2e/map-invoke.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('confirms the prices', () => {
7 | cy.visit('cypress/prices.html')
8 | cy.get('#items li')
9 | .find('.price')
10 | .map('innerText')
11 | .mapInvoke('replace', '$', '')
12 | .map(parseFloat)
13 | .should('deep.equal', [1.99, 2.99, 3.99])
14 | })
15 |
16 | it('respects the timeout option', () => {
17 | const strings = []
18 | setTimeout(() => {
19 | strings.push('a')
20 | }, 1000)
21 | setTimeout(() => {
22 | strings.push('b')
23 | }, 2000)
24 | setTimeout(() => {
25 | strings.push('c')
26 | }, 3000)
27 | setTimeout(() => {
28 | strings.push('d')
29 | }, 4000)
30 | setTimeout(() => {
31 | strings.push('e')
32 | }, 5000)
33 | setTimeout(() => {
34 | strings.push('f')
35 | }, 6000)
36 | setTimeout(() => {
37 | strings.push('g')
38 | }, 7000)
39 | cy.wrap(strings)
40 | .mapInvoke('toUpperCase', { timeout: 10_000 })
41 | .should('deep.equal', ['A', 'B', 'C', 'D', 'E', 'F', 'G'])
42 | })
43 |
--------------------------------------------------------------------------------
/cypress/e2e/map.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../src/commands'
5 |
6 | const getTexts = ($el) => {
7 | return Cypress._.map($el, 'innerText')
8 | }
9 |
10 | it.skip('confirms the list without retries', () => {
11 | cy.visit('cypress/index.html')
12 | cy.get('.matching')
13 | .then(getTexts)
14 | .should('deep.equal', ['first', 'third', 'fourth'])
15 | })
16 |
17 | it('confirms the list', () => {
18 | cy.visit('cypress/index.html')
19 | cy.get('.matching')
20 | .map('innerText')
21 | .should('deep.equal', ['first', 'third', 'fourth'])
22 | })
23 |
24 | it('confirms the last element text', () => {
25 | cy.visit('cypress/index.html')
26 | // cy.last is a query command
27 | cy.get('.matching')
28 | .last()
29 | .map('innerText')
30 | .should('deep.equal', ['fourth'])
31 | })
32 |
33 | it('confirms the last two elements text', () => {
34 | cy.visit('cypress/index.html')
35 | cy.get('.matching')
36 | .map('innerText')
37 | // cy.invoke is a query command
38 | .invoke('slice', -2)
39 | .should('deep.equal', ['third', 'fourth'])
40 | })
41 |
42 | it('makes the callback unary', () => {
43 | cy.wrap(['1', '2', '3', '4'])
44 | .map(parseInt)
45 | .should('deep.equal', [1, 2, 3, 4])
46 | })
47 |
48 | it('verifies that LI elements include 3 strings', () => {
49 | // let's say we do not care the order of strings, just that
50 | // the list includes strings "first", "fifth", "third"
51 | // in any order
52 | cy.visit('cypress/index.html')
53 | cy.get('li')
54 | .map('innerText')
55 | .should('include.members', ['first', 'fifth', 'third'])
56 | })
57 |
58 | it('maps properties of an object', () => {
59 | cy.wrap({
60 | age: '42',
61 | lucky: true,
62 | })
63 | // cast the property "age" to a number
64 | // by running it through the "Number" function
65 | .map({
66 | age: Number,
67 | lucky: Cypress._.identity,
68 | })
69 | .should('deep.equal', {
70 | age: 42,
71 | lucky: true,
72 | })
73 | })
74 |
75 | describe('Picking properties', () => {
76 | // https://github.com/bahmutov/cypress-map/issues/110
77 | it('picks the listed properties of the subject', () => {
78 | const person = {
79 | name: 'Joe',
80 | age: 21,
81 | occupation: 'student',
82 | }
83 | cy.wrap(person, { timeout: 0 })
84 | .map(['name', 'age'])
85 | .should('deep.equal', {
86 | name: 'Joe',
87 | age: 21,
88 | })
89 | })
90 |
91 | it('retries to find the picked properties', () => {
92 | const person = {}
93 | setTimeout(() => {
94 | person.name = 'Joe'
95 | person.occupation = 'student'
96 | person.age = 21
97 | }, 1000)
98 | cy.wrap(person, { timeout: 1100 })
99 | .map(['name', 'age'])
100 | .should('deep.equal', {
101 | name: 'Joe',
102 | age: 21,
103 | })
104 | })
105 |
106 | it('picks multiple nested properties', () => {
107 | const person = {}
108 | setTimeout(() => {
109 | person.name = { first: 'Joe', last: 'Smith' }
110 | person.occupation = 'student'
111 | person.age = 21
112 | }, 1000)
113 | cy.wrap(person, { timeout: 1100 })
114 | // should pick the property "age"
115 | // and should pick the nested property "name.first"
116 | // and store it under the name "first"
117 | .map(['name.first', 'age'])
118 | .should('deep.equal', {
119 | first: 'Joe',
120 | age: 21,
121 | })
122 | })
123 |
124 | it('maps nested paths', () => {
125 | const people = [
126 | {
127 | name: {
128 | first: 'Joe',
129 | last: 'Smith',
130 | },
131 | },
132 | {
133 | name: {
134 | first: 'Anna',
135 | last: 'Kova',
136 | },
137 | },
138 | ]
139 | cy.wrap(people)
140 | .map('name.first')
141 | .should('deep.equal', ['Joe', 'Anna'])
142 | })
143 |
144 | it('maps paths over an array subject', () => {
145 | const people = [
146 | {
147 | name: {
148 | first: 'Joe',
149 | last: 'Smith',
150 | },
151 | },
152 | {
153 | name: {
154 | first: 'Anna',
155 | last: 'Kova',
156 | },
157 | },
158 | ]
159 | cy.wrap(people)
160 | .map(['name.first', 'name.last'])
161 | .should('deep.equal', [
162 | { first: 'Joe', last: 'Smith' },
163 | { first: 'Anna', last: 'Kova' },
164 | ])
165 | })
166 |
167 | it('maps combo paths over an array subject', () => {
168 | const people = [
169 | {
170 | name: {
171 | first: 'Joe',
172 | last: 'Smith',
173 | },
174 | age: 21,
175 | },
176 | {
177 | name: {
178 | first: 'Anna',
179 | last: 'Kova',
180 | },
181 | age: 22,
182 | },
183 | ]
184 | cy.wrap(people)
185 | // some paths are nested, some are not
186 | .map(['name.first', 'age'])
187 | .should('deep.equal', [
188 | { first: 'Joe', age: 21 },
189 | { first: 'Anna', age: 22 },
190 | ])
191 | })
192 | })
193 |
194 | it('respects the timeout option', () => {
195 | const people = [
196 | {
197 | name: {
198 | first: 'Joe',
199 | last: 'Smith',
200 | },
201 | },
202 | ]
203 | setTimeout(() => {
204 | people.push({
205 | name: {
206 | first: 'Anna',
207 | last: 'Kova',
208 | },
209 | })
210 | }, 6000)
211 | cy.wrap(people)
212 | .map('name.first', { timeout: 7_000 })
213 | .should('deep.equal', ['Joe', 'Anna'])
214 | })
215 |
216 | it('respects the timeout option from the parent command', () => {
217 | const people = [
218 | {
219 | name: {
220 | first: 'Joe',
221 | last: 'Smith',
222 | },
223 | },
224 | ]
225 | setTimeout(() => {
226 | people.push({
227 | name: {
228 | first: 'Anna',
229 | last: 'Kova',
230 | },
231 | })
232 | }, 6000)
233 | cy.wrap(people, { timeout: 10_000 })
234 | .map('name') // should use the timeout from the parent command
235 | .map('first') // should use the timeout from the parent command
236 | .should('deep.equal', ['Joe', 'Anna'])
237 | })
238 |
239 | // enable only to see the thrown errors
240 | // https://github.com/bahmutov/cypress-map/issues/74
241 | describe.skip(
242 | 'invalid subjects',
243 | { defaultCommandTimeout: 0 },
244 | () => {
245 | it('throws on a string', () => {
246 | cy.wrap('hello').map(parseInt).print()
247 | })
248 |
249 | it('throws on a number', () => {
250 | cy.wrap(42).map(parseInt).print()
251 | })
252 |
253 | it('throws on a boolean', () => {
254 | cy.wrap(true).map(parseInt).print()
255 | })
256 | },
257 | )
258 |
--------------------------------------------------------------------------------
/cypress/e2e/multiple-import.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('works after multiple imports', () => {
7 | cy.wrap(['one', 'two']).map('length').should('deep.equal', [3, 3])
8 | })
9 |
--------------------------------------------------------------------------------
/cypress/e2e/partial.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('applies the partially applied callback', () => {
7 | cy.wrap(100).partial(Cypress._.add, 5).should('equal', 105)
8 | })
9 |
--------------------------------------------------------------------------------
/cypress/e2e/primo.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('converts the first item text to uppercase', () => {
7 | cy.visit('cypress/index.html')
8 | cy.get('.matching')
9 | .map('innerText')
10 | .primo()
11 | .invoke('toUpperCase')
12 | .should('equal', 'FIRST')
13 | })
14 |
15 | it('yields the first DOM element', () => {
16 | cy.visit('cypress/index.html')
17 | cy.get('.matching')
18 | .primo()
19 | .invoke('getAttributeNames')
20 | .should('deep.equal', ['class'])
21 | })
22 |
--------------------------------------------------------------------------------
/cypress/e2e/print.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | describe('% notation', () => {
7 | it('prints a number by default', () => {
8 | cy.wrap(42)
9 | .print() // "42"
10 | // and yields the value
11 | .should('equal', 42)
12 | })
13 |
14 | it('prints a string by default', () => {
15 | cy.wrap('hello')
16 | .print() // "hello"
17 | .should('be.a', 'string')
18 | })
19 |
20 | it('prints an object by default', () => {
21 | cy.wrap({ name: 'Joe' }).print() // {"name":"Joe"}
22 | })
23 |
24 | it('formats a number using %d', () => {
25 | cy.wrap(42)
26 | .print('the answer is %d') // "the answer is 42"
27 | .should('equal', 42)
28 | })
29 |
30 | it('prints an object using %o notation', () => {
31 | cy.wrap({ name: 'Joe' }).print('person %o') // 'person {"name":"Joe"}'
32 | })
33 |
34 | it('prints an array of numbers', () => {
35 | cy.wrap([1, 2, 3]).print()
36 | })
37 |
38 | it('prints an array of strings', () => {
39 | cy.wrap(['one', 'two', 'three']).print()
40 | })
41 | })
42 |
43 | describe('{} notation', () => {
44 | it('prints an object using {0} notation', () => {
45 | cy.wrap({ name: 'Joe' })
46 | .print('person {0}') // 'person {"name":"Joe"}'
47 | .should('deep.equal', { name: 'Joe' })
48 | })
49 |
50 | it('prints a property using {0.name} notation', () => {
51 | cy.wrap({ name: 'Joe' })
52 | .print('person name {0.name}') // "person name Joe"
53 | .should('deep.equal', { name: 'Joe' })
54 | })
55 |
56 | it('prints a nested property using {0.foo.bar} notation', () => {
57 | cy.wrap({ name: { first: 'Joe' } })
58 | .print('first name is {0.name.first}') // "first name is Joe"
59 | .its('name.first')
60 | .should('equal', 'Joe')
61 | })
62 |
63 | it('prints the length of an array', () => {
64 | const arr = [1, 2, 3]
65 | cy.wrap(arr)
66 | .print('array length {0.length}')
67 | .its('length')
68 | .should('equal', 4)
69 | setTimeout(() => {
70 | arr.push(4)
71 | }, 1000)
72 | })
73 | })
74 |
75 | describe('format callback', () => {
76 | it('passes the subject and prints the result', () => {
77 | const person = { name: 'Joe' }
78 | cy.wrap(person)
79 | .print((p) => `name is ${p.name}`)
80 | .its('name')
81 | .should('equal', 'Ann')
82 | setTimeout(() => {
83 | person.name = 'Ann'
84 | }, 1000)
85 | })
86 |
87 | it('can return an object to be stringified', () => {
88 | cy.wrap([1, 2, 3]).print((list) => list[1]) // "2"
89 | cy.wrap({ name: 'Me' }).print((x) => x) // {"name":"Me"}
90 | })
91 | })
92 |
--------------------------------------------------------------------------------
/cypress/e2e/prop.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('yields the object property using cy.its', () => {
7 | const o = {}
8 | cy.wrap(o).its('name').should('equal', 'Joe')
9 |
10 | setTimeout(() => {
11 | o.name = 'Joe'
12 | }, 1000)
13 | })
14 |
15 | it('yields the object property using cy.prop', () => {
16 | const o = {}
17 | cy.wrap(o).prop('name').should('equal', 'Joe')
18 |
19 | setTimeout(() => {
20 | o.name = 'Joe'
21 | }, 1000)
22 | })
23 |
24 | it('yields the DOM prop', () => {
25 | cy.visit('cypress/index.html')
26 | // this does not work with cy.its
27 | cy.get('#items li.matching').last().prop('ariaLabel').should('equal', 'four')
28 | })
29 |
--------------------------------------------------------------------------------
/cypress/e2e/reduce.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('confirms the highest price', () => {
7 | cy.visit('cypress/prices.html')
8 | cy.get('#items li')
9 | .find('.price')
10 | .map('innerText')
11 | .mapInvoke('replace', '$', '')
12 | .map(parseFloat)
13 | .reduce((max, price) => (price > max ? price : max))
14 | .should('equal', 3.99)
15 | })
16 |
17 | it('work with wrapped array', () => {
18 | cy.wrap([1, 10, 2, 5, 3])
19 | .reduce((max, n) => (n > max ? n : max))
20 | .should('equal', 10)
21 | })
22 |
23 | it('work with wrapped array with async added items', () => {
24 | const list = [1, 2, 5, 3]
25 | cy.wrap(list)
26 | .reduce((max, n) => (n > max ? n : max))
27 | .should('equal', 10)
28 | setTimeout(() => {
29 | list.push(10)
30 | }, 1000)
31 | })
32 |
33 | it('uses the initial value', () => {
34 | const sum = (sum, n) => sum + n
35 | cy.wrap([1, 2, 3]).reduce(sum, 10).should('equal', 16)
36 | })
37 |
--------------------------------------------------------------------------------
/cypress/e2e/sample.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('picks a random item from the list', () => {
7 | const list = ['foo', 'bar', 'baz']
8 | cy.wrap(list).sample().should('be.oneOf', list)
9 | })
10 |
11 | it('picks a random element', () => {
12 | cy.visit('cypress/index.html')
13 | // the first item is immediately in the list
14 | cy.get('#items li').sample().should('have.text', 'first')
15 | cy.log('**retries until the item appears and is picked**')
16 | cy.get('#items li').sample().should('have.text', 'fifth')
17 | })
18 |
19 | it('picks N random items', () => {
20 | const list = ['foo', 'bar', 'baz']
21 | cy.wrap(list).sample(2).should('have.length', 2)
22 | })
23 |
24 | it('picks N random elements', () => {
25 | cy.visit('cypress/index.html')
26 | // the first item is immediately in the list
27 | cy.get('#items li').sample().should('have.text', 'first')
28 | cy.log('**retries until all items appear**')
29 | cy.get('#items li')
30 | .sample(5)
31 | .should('have.length', 5)
32 | .and('satisfy', Cypress.dom.isJquery)
33 | .map('innerText')
34 | .invoke('sort')
35 | .should('deep.equal', [
36 | 'fifth',
37 | 'first',
38 | 'fourth',
39 | 'second',
40 | 'third',
41 | ])
42 | cy.log('**elements are random**')
43 | cy.get('#items li').sample(2).map('innerText').print()
44 | })
45 |
46 | it('stores value in a static alias', () => {
47 | cy.wrap('foo')
48 | .as('word', { type: 'static' })
49 | .then(console.log)
50 | .should('equal', 'foo')
51 | cy.get('@word').then(console.log).should('equal', 'foo')
52 | })
53 |
54 | // TODO: make sure the value of sample is evaluated just once
55 | // and saved as a static alias
56 | it('stores sample in the static alias', () => {
57 | let value
58 | cy.wrap(['foo', 'bar', 'baz', 'quux', 'quuz'])
59 | .sample()
60 | .then(console.log)
61 | .as('word', { type: 'static' })
62 | .should('be.a', 'string')
63 | .then((w) => (value = w))
64 |
65 | // TODO: value should be the same as the first sample
66 | cy.get('@word').should((word) => {
67 | expect(word, 'static aliased value').to.equal(value)
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/cypress/e2e/second.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('picks the second item in the list', () => {
7 | const list = ['foo', 'bar', 'baz']
8 | cy.wrap(list).second().should('equal', 'bar')
9 | })
10 |
11 | it('yields the second element', () => {
12 | cy.visit('cypress/index.html')
13 | cy.get('#items li').second().should('have.text', 'second')
14 | })
15 |
16 | it('yields the second element with retries', () => {
17 | cy.visit('cypress/index.html')
18 | // the element appears only after a delay
19 | cy.get('#items li:not(.matching)')
20 | .second()
21 | .should('have.text', 'fifth')
22 | })
23 |
--------------------------------------------------------------------------------
/cypress/e2e/stable-css/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
26 |
27 | Stable CSS
28 | Some text
29 |
30 | Click to show
31 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/cypress/e2e/stable-css/stable-css.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 | import '../../../src/commands'
4 |
5 | beforeEach(() => {
6 | cy.visit('cypress/e2e/stable-css/index.html')
7 | })
8 |
9 | it('waits for the color using have.css', () => {
10 | cy.contains('button', 'Click to show').click()
11 | cy.get('#message').should(
12 | 'have.css',
13 | 'background-color',
14 | 'rgb(255, 0, 0)',
15 | )
16 | })
17 |
18 | it('waits for the color to be stable using cy.invoke retries', () => {
19 | cy.contains('button', 'Click to show').click()
20 | // retries until the CSS animation finishes
21 | // and the background color is red
22 | cy.get('#message')
23 | .invoke('css', 'background-color')
24 | .should('equal', 'rgb(255, 0, 0)')
25 | })
26 |
27 | it('waits for the color to be stable using cy.stable', () => {
28 | cy.contains('button', 'Click to show').click()
29 | // retries until the CSS animation finishes
30 | // and the background color is red
31 | cy.get('#message')
32 | .stable('css', 'background-color', 100)
33 | // yields the element
34 | .should('have.css', 'background-color', 'rgb(255, 0, 0)')
35 | })
36 |
37 | it('waits for the text color to be stable using cy.stable', () => {
38 | cy.contains('button', 'Click to show').click()
39 | cy.get('#message')
40 | .stable('css', 'color', 100)
41 | .should('have.css', 'color', 'rgb(255, 255, 255)')
42 | })
43 |
--------------------------------------------------------------------------------
/cypress/e2e/stable/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | --
4 |
5 |
6 |
7 |
8 | Replace result
9 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/cypress/e2e/stable/stable.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 | import '../../../src/commands'
4 |
5 | beforeEach(() => {
6 | cy.visit('cypress/e2e/stable/index.html')
7 | })
8 |
9 | it('waits for the element text to be stable', () => {
10 | cy.get('#message').stable('text').should('have.text', 'Hello')
11 | })
12 |
13 | it('controls the logging', () => {
14 | cy.get('#message').stable('text', 1000, { log: false })
15 | })
16 |
17 | it('controls the timeout', () => {
18 | cy.get('#message').stable('text', 5000, { timeout: 10_000 })
19 | })
20 |
21 | it('waits for the input value to be stable', () => {
22 | cy.get('#name').stable('value')
23 | cy.get('#name', { timeout: 0 }).should('have.value', 'World')
24 | })
25 |
26 | it('waits for the element reference to be stable', () => {
27 | cy.contains('button', 'Replace result').click()
28 | cy.get('#result').stable('element', 1000, {
29 | timeout: 3_000,
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/cypress/e2e/table-rows.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // https://github.com/bahmutov/cy-spok
5 | import spok from 'cy-spok'
6 |
7 | // import cypress-map plugin
8 | import '../../commands'
9 |
10 | describe(
11 | 'table rows',
12 | { viewportWidth: 300, viewportHeight: 200, defaultCommandTimeout: 100 },
13 | () => {
14 | beforeEach(() => {
15 | cy.visit('cypress/table.html')
16 | // confirm the table headings
17 | const headings = ['Name', 'Age', 'Date (YYYY-MM-DD)']
18 | cy.get('table thead td').map('innerText').should('deep.equal', headings)
19 | })
20 |
21 | it('confirms the first row (text)', () => {
22 | cy.get('table tbody tr')
23 | .first()
24 | .find('td')
25 | // warning: no retries
26 | .then((td$) => {
27 | return {
28 | name: td$[0].innerText,
29 | age: td$[1].innerText,
30 | date: td$[2].innerText,
31 | }
32 | })
33 | .print()
34 | .should('deep.equal', {
35 | name: 'Dave',
36 | age: '20',
37 | date: '2023-12-23',
38 | })
39 | })
40 |
41 | it('confirms the first row (spread)', () => {
42 | cy.get('table tbody tr')
43 | .first()
44 | .find('td')
45 | // warning: spread is NOT a query, so it won't retry
46 | .spread((name, age, date) => {
47 | return {
48 | name: name.innerText,
49 | age: age.innerText,
50 | date: date.innerText,
51 | }
52 | })
53 | .print()
54 | .should('deep.equal', {
55 | name: 'Dave',
56 | age: '20',
57 | date: '2023-12-23',
58 | })
59 | })
60 |
61 | const props = ['name', 'age', 'date']
62 | it('confirms the first row', () => {
63 | cy.get('table tbody tr')
64 | .first()
65 | .find('td')
66 | .map('innerText')
67 | .print()
68 | .apply((values) => Cypress._.zipObject(props, values))
69 | .print()
70 | .should('deep.equal', {
71 | name: 'Dave',
72 | age: '20',
73 | date: '2023-12-23',
74 | })
75 | })
76 |
77 | it('confirms the first row (convert values)', () => {
78 | cy.get('table tbody tr')
79 | .first()
80 | .find('td')
81 | .map('innerText')
82 | .print()
83 | .apply((values) => Cypress._.zipObject(props, values))
84 | .apply((person) => {
85 | person.age = Number(person.age)
86 | return person
87 | })
88 | .print()
89 | .should('deep.include', {
90 | name: 'Dave',
91 | age: 20,
92 | })
93 | })
94 |
95 | it('confirms the first row (convert values using update)', () => {
96 | cy.get('table tbody tr')
97 | .first()
98 | .find('td')
99 | .map('innerText')
100 | .print()
101 | .apply((values) => Cypress._.zipObject(props, values))
102 | .update('age', Number)
103 | .print()
104 | .should('deep.include', {
105 | name: 'Dave',
106 | age: 20,
107 | })
108 | })
109 |
110 | it('confirms the first row with update and cy-spok', () => {
111 | cy.get('table tbody tr')
112 | .first()
113 | .find('td')
114 | .map('innerText')
115 | .print()
116 | .apply((values) => Cypress._.zipObject(props, values))
117 | .update('age', Number)
118 | .print()
119 | .should(
120 | spok({
121 | name: 'Dave',
122 | age: spok.range(1, 99),
123 | date: spok.test(/^\d\d\d\d-\d\d-\d\d$/),
124 | }),
125 | )
126 | })
127 |
128 | it('confirms the first row with partial, update, and cy-spok', () => {
129 | cy.get('table tbody tr')
130 | .first()
131 | .find('td')
132 | .map('innerText')
133 | .print()
134 | .partial(Cypress._.zipObject, props)
135 | .update('age', Number)
136 | .print()
137 | .should(
138 | spok({
139 | name: 'Dave',
140 | age: spok.range(1, 99),
141 | date: spok.test(/^\d\d\d\d-\d\d-\d\d$/),
142 | }),
143 | )
144 | })
145 |
146 | it('confirms the row with Anna', () => {
147 | cy.contains('table tbody tr', 'Anna')
148 | .find('td')
149 | .map('innerText')
150 | .print()
151 | .partial(Cypress._.zipObject, props)
152 | .update('age', Number)
153 | .print()
154 | .should(
155 | spok({
156 | name: 'Anna',
157 | age: spok.range(10, 30),
158 | date: spok.test(/^\d\d\d\d-\d\d-\d\d$/),
159 | }),
160 | )
161 | })
162 | },
163 | )
164 |
--------------------------------------------------------------------------------
/cypress/e2e/table.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | // import cypress-map plugin
5 | import '../../commands'
6 |
7 | describe('table', { viewportWidth: 300, viewportHeight: 200 }, () => {
8 | beforeEach(() => {
9 | cy.visit('cypress/table.html')
10 | })
11 |
12 | it('checks the column of cells', () => {
13 | cy.contains('table#people thead td:nth-child(2)', 'Age')
14 | cy.get('table#people tbody td:nth-child(2)')
15 | .should('have.length', 4)
16 | .map('innerText')
17 | .map(Number)
18 | .should('deep.equal', [20, 30, 28, 22])
19 | })
20 |
21 | it('gets the entire table', () => {
22 | cy.get('table')
23 | .table()
24 | .should('deep.equal', [
25 | ['Name', 'Age', 'Date (YYYY-MM-DD)'],
26 | ['Dave', '20', '2023-12-23'],
27 | ['Cary', '30', '2024-01-24'],
28 | ['Joe', '28', '2022-02-25'],
29 | ['Anna', '22', '2027-03-26'],
30 | ])
31 | })
32 |
33 | it('gets the table body', () => {
34 | cy.get('table tbody')
35 | .table()
36 | .should('deep.equal', [
37 | ['Dave', '20', '2023-12-23'],
38 | ['Cary', '30', '2024-01-24'],
39 | ['Joe', '28', '2022-02-25'],
40 | ['Anna', '22', '2027-03-26'],
41 | ])
42 | })
43 |
44 | it('gets the headings', () => {
45 | cy.get('table')
46 | .table(0, 0, 3, 1)
47 | .its(0)
48 | .print()
49 | .should('deep.equal', ['Name', 'Age', 'Date (YYYY-MM-DD)'])
50 | })
51 |
52 | it('gets the headings row', () => {
53 | cy.get('table')
54 | .table(0, 0)
55 | .its(0)
56 | .print()
57 | .should('deep.equal', ['Name', 'Age', 'Date (YYYY-MM-DD)'])
58 | })
59 |
60 | it('gets the headings row from thead', () => {
61 | cy.get('table thead')
62 | .table(0, 0)
63 | .should('have.length', 1)
64 | .its(0)
65 | .print()
66 | .should('deep.equal', ['Name', 'Age', 'Date (YYYY-MM-DD)'])
67 | })
68 |
69 | it('gets the first column', () => {
70 | cy.get('table')
71 | .table(0, 0, 1, 5)
72 | .print()
73 | .should('deep.equal', [['Name'], ['Dave'], ['Cary'], ['Joe'], ['Anna']])
74 | })
75 |
76 | it('gets the entire first column', () => {
77 | cy.get('table')
78 | .table(0, 0, 1)
79 | .print()
80 | .should('deep.equal', [['Name'], ['Dave'], ['Cary'], ['Joe'], ['Anna']])
81 | })
82 |
83 | it('joins the first column into one array', () => {
84 | cy.get('table')
85 | .table(0, 1, 1) // skip the heading "Name" cell
86 | // combine 1x1 arrays into one array
87 | .invoke('flatMap', Cypress._.identity)
88 | .print()
89 | .should('deep.equal', ['Dave', 'Cary', 'Joe', 'Anna'])
90 | })
91 |
92 | it('gets a region of the table', () => {
93 | cy.get('table')
94 | .table(0, 2, 2, 2)
95 | .print()
96 | .should('deep.equal', [
97 | ['Cary', '30'],
98 | ['Joe', '28'],
99 | ])
100 | })
101 |
102 | it('gets a region of the table using slice', () => {
103 | cy.get('table')
104 | .table()
105 | .invoke('slice', 2, 4)
106 | .mapInvoke('slice', 0, 2)
107 | .print()
108 | .should('deep.equal', [
109 | ['Cary', '30'],
110 | ['Joe', '28'],
111 | ])
112 | })
113 |
114 | it('checks the last row', () => {
115 | cy.get('table')
116 | .table()
117 | .invoke('slice', -1)
118 | .its(0)
119 | .print()
120 | .should('deep.equal', ['Anna', '22', '2027-03-26'])
121 | })
122 | })
123 |
--------------------------------------------------------------------------------
/cypress/e2e/tap.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('confirms the prices', () => {
7 | cy.visit('cypress/prices.html')
8 | cy.get('#items li')
9 | .find('.price')
10 | .map('innerText')
11 | .tap(console.log, 'text')
12 | .mapInvoke('replace', '$', '')
13 | .tap(console.log, 'without $')
14 | .map(parseFloat)
15 | .tap(console.info, 'numbers')
16 | .should('deep.equal', [1.99, 2.99, 3.99])
17 | })
18 |
19 | it('uses console.log by default', () => {
20 | cy.visit('cypress/prices.html')
21 | cy.get('#items li')
22 | .find('.price')
23 | .map('innerText')
24 | .tap()
25 | .mapInvoke('replace', '$', '')
26 | .tap()
27 | .map(parseFloat)
28 | .tap(console.info)
29 | .should('deep.equal', [1.99, 2.99, 3.99])
30 | })
31 |
32 | it('uses console.log by default with a label', () => {
33 | cy.visit('cypress/prices.html')
34 | cy.get('#items li')
35 | .find('.price')
36 | .map('innerText')
37 | .tap('text')
38 | .mapInvoke('replace', '$', '')
39 | .map(Number)
40 | .tap('prices')
41 | .should('deep.equal', [1.99, 2.99, 3.99])
42 | })
43 |
--------------------------------------------------------------------------------
/cypress/e2e/text.cy.js:
--------------------------------------------------------------------------------
1 | // confirm the first LI element has text
2 | // that is either 'first' or 'primo'
3 |
4 | // 📺 video "Check If Element Text Is One Of Possible Strings"
5 | // https://youtu.be/KSlJYjIn_AM
6 | it('checks the text is one of the variants: single should callback', () => {
7 | cy.visit('cypress/index.html')
8 | cy.get('#items li').should(($li) => {
9 | expect($li.first().text()).to.be.oneOf(['first', 'primo'])
10 | })
11 | })
12 |
13 | it('checks the text is one of the variants: regex', () => {
14 | cy.visit('cypress/index.html')
15 | cy.contains('#items li:first', /^(first|primo)$/)
16 | })
17 |
18 | it('checks the text is one of the variants: v12 queries', () => {
19 | cy.visit('cypress/index.html')
20 | cy.get('#items li')
21 | .first()
22 | .invoke('text')
23 | .should('be.oneOf', ['first', 'primo'])
24 | })
25 |
--------------------------------------------------------------------------------
/cypress/e2e/third.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | it('picks the third item in the list', () => {
7 | const list = ['foo', 'bar', 'baz']
8 | cy.wrap(list).third().should('equal', 'baz')
9 | })
10 |
11 | it('yields the third element', () => {
12 | cy.visit('cypress/index.html')
13 | cy.get('#items li').third().should('have.text', 'third')
14 | })
15 |
16 | it('yields the third element with retries', () => {
17 | cy.visit('cypress/index.html')
18 | // the element appears only after a delay
19 | cy.get('#items li.matching').third().should('have.text', 'fourth')
20 | })
21 |
--------------------------------------------------------------------------------
/cypress/e2e/to-plain-object.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 | // https://github.com/bahmutov/cy-spok
6 | import spok from 'cy-spok'
7 |
8 | describe('to plain object', () => {
9 | beforeEach(() => {
10 | cy.visit('cypress/dataset.html')
11 | })
12 |
13 | it('applies JSON stringify and parse', () => {
14 | cy.get('article')
15 | .should('have.attr', 'data-columns', '3')
16 | .invoke('prop', 'dataset')
17 | // convert from DOMStringMap to a plain object
18 | .apply(JSON.stringify)
19 | .apply(JSON.parse)
20 | .should('deep.equal', {
21 | columns: '3',
22 | indexNumber: '12314',
23 | parent: 'cars',
24 | })
25 | })
26 |
27 | it('uses cy-spok with DOMStringMap', () => {
28 | cy.get('article')
29 | .should('have.attr', 'data-columns', '3')
30 | .invoke('prop', 'dataset')
31 | .should(
32 | spok({
33 | columns: '3',
34 | indexNumber: '12314',
35 | parent: 'cars',
36 | }),
37 | )
38 | })
39 |
40 | it('uses cy.toPlainObject custom query', () => {
41 | cy.get('article')
42 | .should('have.prop', 'dataset')
43 | .toPlainObject()
44 | .should('deep.equal', {
45 | columns: '3',
46 | indexNumber: '12314',
47 | parent: 'cars',
48 | })
49 | })
50 | })
51 |
52 | it('uses entries to convert', () => {
53 | const searchParams =
54 | '?callback=%2Fmy-profile-page.html&question=what%20is%20the%20meaning%20of%20life%3F&answer=42'
55 | cy.wrap(new URLSearchParams(searchParams))
56 | .toPlainObject('entries')
57 | .should('deep.equal', {
58 | callback: '/my-profile-page.html',
59 | question: 'what is the meaning of life?',
60 | answer: '42',
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/cypress/e2e/unique.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | import '../../commands'
5 |
6 | describe('unique', () => {
7 | it('checks array elements are unique', () => {
8 | cy.wrap([1, 2, 3])
9 | .should('be.unique')
10 | .and('have.length', 3)
11 | .its(1)
12 | .should('equal', 2)
13 | cy.wrap([1, 2, 2])
14 | .should('not.be.unique')
15 | .and('have.length', 3)
16 | .its(1)
17 | .should('equal', 2)
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/cypress/e2e/version.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | it('parses Cypress version', () => {
4 | console.log(Cypress.version)
5 | const [major, minor, patch] = Cypress.version.split('.').map(Number)
6 | console.log({ major, minor, patch })
7 | expect(major, 'Cypress major version').to.be.gte(12)
8 | })
9 |
--------------------------------------------------------------------------------
/cypress/fixtures/people.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Peter",
4 | "date": "2000-01-01",
5 | "age": 23
6 | },
7 | {
8 | "name": "Pete",
9 | "date": "2000-01-01",
10 | "age": 23
11 | },
12 | {
13 | "name": "Mary",
14 | "date": "2000-01-01",
15 | "age": 23
16 | },
17 | {
18 | "name": "Mary-Ann",
19 | "date": "2000-01-01",
20 | "age": 23
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/cypress/index.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | first
11 | second
12 | third
13 |
14 |
26 |
27 |
--------------------------------------------------------------------------------
/cypress/list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Item A 1
4 | Item B 2
5 | Item C 3
6 | Item D 4
7 |
8 |
9 |
--------------------------------------------------------------------------------
/cypress/log-examples.html:
--------------------------------------------------------------------------------
1 |
2 | 42
3 |
6 |
7 |
--------------------------------------------------------------------------------
/cypress/prices.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
12 |
22 |
23 |
--------------------------------------------------------------------------------
/cypress/table.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 | Name
13 | Age
14 | Date (YYYY-MM-DD)
15 |
16 |
17 |
18 |
19 | Dave
20 | 20
21 | 2023-12-23
22 |
23 |
24 | Cary
25 | 30
26 | 2024-01-24
27 |
28 |
29 | Joe
30 | 28
31 | 2022-02-25
32 |
33 |
34 | Anna
35 | 22
36 | 2027-03-26
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/images/table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bahmutov/cypress-map/f8d89e67cc670405db46745f890bd0e9bf3f3e90/images/table.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cypress-map",
3 | "version": "0.0.0-development",
4 | "description": "Extra Cypress query commands for v12+",
5 | "main": "commands/index.js",
6 | "types": "src/commands/index.d.ts",
7 | "files": [
8 | "commands",
9 | "src/commands/index.d.ts"
10 | ],
11 | "scripts": {
12 | "test": "cypress run",
13 | "badges": "npx -p dependency-version-badge update-badge cypress",
14 | "semantic-release": "semantic-release",
15 | "stop-only": "stop-only --folder cypress/e2e",
16 | "lint": "tsc --noEmit --pretty --allowJs --esModuleInterop src/commands/*.* cypress/*/*.js cypress/*/*/*.js",
17 | "build": "tsc",
18 | "watch": "tsc --watch"
19 | },
20 | "keywords": [
21 | "cypress-plugin"
22 | ],
23 | "author": "Gleb Bahmutov ",
24 | "license": "MIT",
25 | "devDependencies": {
26 | "cy-spok": "^1.5.2",
27 | "cypress": "^14.0.0",
28 | "cypress-split": "^1.19.0",
29 | "prettier": "^3.0.0",
30 | "semantic-release": "^23.0.0",
31 | "stop-only": "^3.1.2",
32 | "typescript": "^5.0.0"
33 | },
34 | "peerDependencies": {
35 | "cypress": ">=12"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/bahmutov/cypress-map.git"
40 | },
41 | "dependencies": {
42 | "string-format": "^2.0.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "automerge": true,
4 | "major": {
5 | "automerge": false
6 | },
7 | "minor": {
8 | "automerge": true
9 | },
10 | "prConcurrentLimit": 3,
11 | "prHourlyLimit": 2,
12 | "schedule": ["every weekend"],
13 | "masterIssue": true,
14 | "labels": ["type: dependencies", "renovate"],
15 | "ignoreDeps": ["cy-spok"]
16 | }
17 |
--------------------------------------------------------------------------------
/src/commands/apply.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('apply', (callback, ...args) => {
6 | if (typeof callback !== 'function') {
7 | throw new Error('Expected a function to apply')
8 | }
9 |
10 | const log = Cypress.log({ name: 'apply', message: callback.name })
11 |
12 | return (subject) => {
13 | log.set({
14 | $el: subject,
15 | })
16 | return callback(...args, subject)
17 | }
18 | })
19 |
20 | registerQuery('applyRight', (callback, ...args) => {
21 | if (typeof callback !== 'function') {
22 | throw new Error('Expected a function to apply')
23 | }
24 |
25 | const log = Cypress.log({
26 | name: 'applyRight',
27 | message: callback.name,
28 | })
29 |
30 | return (subject) => {
31 | log.set({
32 | $el: subject,
33 | })
34 | return callback(subject, ...args)
35 | }
36 | })
37 |
38 | registerQuery('applyToFirst', (callback, ...args) => {
39 | if (typeof callback !== 'function') {
40 | throw new Error('Expected a function to apply')
41 | }
42 |
43 | const log = Cypress.log({
44 | name: 'applyToFirst',
45 | message: callback.name,
46 | })
47 |
48 | return (subject) => {
49 | if (Cypress.dom.isJquery(subject) || Array.isArray(subject)) {
50 | log.set({
51 | $el: subject,
52 | })
53 | return callback(...args, subject[0])
54 | } else {
55 | throw new Error('Expected a jQuery object or an array subject')
56 | }
57 | }
58 | })
59 |
60 | registerQuery('applyToFirstRight', (callback, ...args) => {
61 | if (typeof callback !== 'function') {
62 | throw new Error('Expected a function to apply')
63 | }
64 |
65 | const log = Cypress.log({
66 | name: 'applyToFirstRight',
67 | message: callback.name,
68 | })
69 |
70 | return (subject) => {
71 | if (Cypress.dom.isJquery(subject) || Array.isArray(subject)) {
72 | log.set({
73 | $el: subject,
74 | })
75 | return callback(subject[0], ...args)
76 | } else {
77 | throw new Error('Expected a jQuery object or an array subject')
78 | }
79 | }
80 | })
81 |
--------------------------------------------------------------------------------
/src/commands/as-env.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('asEnv', (name) => {
6 | if (typeof name !== 'string') {
7 | throw new Error(`Invalid cy.asEnv name ${index}`)
8 | }
9 | if (!name) {
10 | throw new Error(`Empty cy.asEnv name`)
11 | }
12 |
13 | const log = Cypress.log({ name: 'asEnv', message: name })
14 |
15 | return (subject) => {
16 | if (Cypress._.isNil(subject)) {
17 | throw new Error('No subject to save cy.asEnv')
18 | }
19 | Cypress.env(name, subject)
20 | return subject
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/commands/assertions.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | chai.use((_chai) => {
4 | // use "function" syntax to make sure when Chai
5 | // calls it, the "this" object points at Chai
6 |
7 | function readAssertion(strings) {
8 | if (Cypress._.isString(strings) || Cypress._.isRegExp(strings)) {
9 | strings = [strings]
10 | }
11 |
12 | if (strings.length === 0) {
13 | throw new Error(
14 | 'Expected at least one string or regular expression',
15 | )
16 | }
17 | // TODO: handle longer lists of expected strings
18 | const texts = Cypress._.map(this._obj, 'innerText')
19 | const expectedValuesMessage = strings
20 | .map((s) => {
21 | return Cypress._.isString(s) ? s : s.toString()
22 | })
23 | .join(', ')
24 |
25 | const message = `expected ${texts.join(', ')} to read ${expectedValuesMessage}`
26 | // confirm the number of elements matches the number of expected strings
27 | this.assert(texts.length === strings.length, message)
28 |
29 | // confirm the texts match the expected strings
30 | const passed = Cypress._.every(strings, (s, i) => {
31 | if (Cypress._.isString(s)) {
32 | return texts[i] === s
33 | }
34 | if (Cypress._.isRegExp(s)) {
35 | return s.test(texts[i])
36 | }
37 | return false
38 | })
39 |
40 | this.assert(passed, message)
41 | }
42 | _chai.Assertion.addMethod('read', readAssertion)
43 |
44 | function possesAssertion(propertyName, maybeValueOrPredicate) {
45 | if (typeof propertyName !== 'string') {
46 | throw new Error(
47 | `possess assertion: Expected a string, but got ${typeof propertyName}`,
48 | )
49 | }
50 |
51 | const subjectText = JSON.stringify(this._obj)
52 | const subjectShort =
53 | subjectText.length > 40
54 | ? `${subjectText.slice(0, 40)}...`
55 | : subjectText
56 |
57 | if (arguments.length === 1) {
58 | return this.assert(
59 | propertyName in this._obj,
60 | `expected ${subjectShort} to possess property "${propertyName}"`,
61 | `expected ${subjectShort} to not possess property "${propertyName}"`,
62 | )
63 | }
64 | if (arguments.length === 2) {
65 | const value = Cypress._.get(this._obj, propertyName)
66 | if (typeof maybeValueOrPredicate === 'function') {
67 | const functionName = maybeValueOrPredicate.name || 'function'
68 | return this.assert(
69 | maybeValueOrPredicate(value),
70 | `expected ${subjectShort} to pass the ${functionName} predicate`,
71 | `expected ${subjectShort} to not pass the ${functionName} predicate`,
72 | )
73 | } else {
74 | return this.assert(
75 | value === maybeValueOrPredicate,
76 | `expected ${subjectShort} to possess property ${propertyName}=${maybeValueOrPredicate}`,
77 | `expected ${subjectShort} to not possess property ${propertyName}=${maybeValueOrPredicate}`,
78 | )
79 | }
80 | }
81 |
82 | throw new Error(
83 | `Unexpected arguments to the "possess" assertion ${[...arguments].join(', ')}`,
84 | )
85 | }
86 | _chai.Assertion.addMethod('possess', possesAssertion)
87 |
88 | function uniqueAssertion() {
89 | const values = this._obj
90 |
91 | if (!Array.isArray(values)) {
92 | throw new Error(
93 | `unique assertion: Expected an array, but got ${typeof values}`,
94 | )
95 | }
96 |
97 | // report duplicate values?
98 | const uniqueValues = new Set(values)
99 | return this.assert(
100 | uniqueValues.size === values.length,
101 | `expected ${values} to be unique`,
102 | `expected ${values} to not be unique`,
103 | )
104 | }
105 | _chai.Assertion.addMethod('unique', uniqueAssertion)
106 | })
107 |
--------------------------------------------------------------------------------
/src/commands/at.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('at', (index) => {
6 | if (typeof index !== 'number') {
7 | throw new Error(`Invalid cy.at index ${index}`)
8 | }
9 | const log = Cypress.log({ name: 'at', message: String(index) })
10 |
11 | return (subject) => {
12 | if (Cypress.dom.isJquery(subject) || Array.isArray(subject)) {
13 | if (index < 0) {
14 | return subject[subject.length + index]
15 | }
16 | return subject[index]
17 | }
18 | throw new Error(`Not sure how to pick the item at ${index}`)
19 | }
20 | })
21 |
--------------------------------------------------------------------------------
/src/commands/detaches.js:
--------------------------------------------------------------------------------
1 | ///
2 | // @ts-check
3 |
4 | const { registerQuery } = require('./utils')
5 |
6 | registerQuery('detaches', (selectorOrAlias) => {
7 | if (typeof selectorOrAlias !== 'string') {
8 | throw new Error('Selector/alias must be a string')
9 | }
10 |
11 | const log = Cypress.log({
12 | name: 'detaches',
13 | message: String(selectorOrAlias),
14 | })
15 |
16 | if (selectorOrAlias[0] === '@') {
17 | return () => {
18 | const $el = Cypress.env(selectorOrAlias.slice(1))
19 | if (!$el) {
20 | throw new Error(
21 | `Element with alias "${selectorOrAlias}" not found`,
22 | )
23 | }
24 | if (!$el.length) {
25 | throw new Error(`Expected one element, found ${$el.length}`)
26 | }
27 | if (Cypress.dom.isAttached($el[0])) {
28 | throw new Error(
29 | `Expected element is still attached to the DOM`,
30 | )
31 | }
32 | }
33 | } else {
34 | let el = null
35 | return () => {
36 | if (!el) {
37 | // @ts-expect-error
38 | const doc = cy.state('document')
39 | el = doc.querySelector(selectorOrAlias)
40 | }
41 |
42 | if (!Cypress.dom.isDetached(el)) {
43 | throw new Error(`Expected element to be detached`)
44 | }
45 | }
46 | }
47 | })
48 |
--------------------------------------------------------------------------------
/src/commands/difference.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | const toDifference = (o) => {
6 | if (typeof o === 'function') {
7 | return o.name || 'fn'
8 | } else {
9 | return o
10 | }
11 | }
12 |
13 | function diffTwoObjects(expected, subject) {
14 | const names = Object.keys(expected)
15 | const diff = {}
16 |
17 | names.forEach((name) => {
18 | if (!(name in subject)) {
19 | diff[name] = { missing: true, expected: expected[name] }
20 | } else {
21 | const actual = subject[name]
22 | const expectedValue = expected[name]
23 | // console.log({ name, actual, expectedValue })
24 |
25 | if (typeof expectedValue === 'function') {
26 | if (expectedValue(actual) === false) {
27 | const predicteName = expectedValue.name
28 | diff[name] = {
29 | message: `value ${actual} did not pass predicate "${predicteName}"`,
30 | }
31 | }
32 | } else if (!Cypress._.isEqual(actual, expectedValue)) {
33 | diff[name] = { actual, expected: expectedValue }
34 | }
35 | }
36 | })
37 | Object.keys(subject).forEach((name) => {
38 | if (!(name in expected)) {
39 | diff[name] = { extra: true, actual: subject[name] }
40 | }
41 | })
42 |
43 | return diff
44 | }
45 |
46 | registerQuery('difference', (expected) => {
47 | const logOptions = {
48 | name: 'difference',
49 | type: 'child',
50 | }
51 | if (Array.isArray(expected)) {
52 | logOptions.message =
53 | '[' + expected.map(toDifference).join(', ') + ']'
54 | } else {
55 | logOptions.message = Object.entries(expected)
56 | .map(([k, v]) => `${k}: ${toDifference(v)}`)
57 | .join(', ')
58 | }
59 | const log = Cypress.log(logOptions)
60 |
61 | return (subject) => {
62 | const diff = {}
63 |
64 | if (Array.isArray(subject) && Cypress._.isPlainObject(expected)) {
65 | // check each item in the subject array
66 | // against the expected object or its predicates
67 | subject.forEach((actual, index) => {
68 | const aDiff = diffTwoObjects(expected, actual)
69 | if (!Cypress._.isEmpty(aDiff)) {
70 | diff[index] = aDiff
71 | }
72 | })
73 | } else {
74 | const aDiff = diffTwoObjects(expected, subject)
75 | Object.assign(diff, aDiff)
76 | }
77 |
78 | log.set('consoleProps', () => {
79 | return { expected, subject, diff }
80 | })
81 |
82 | return diff
83 | }
84 | })
85 |
--------------------------------------------------------------------------------
/src/commands/elements.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | function getElements(parentSelector, ...childSelectors) {
6 | const log = Cypress.log({
7 | name: 'elements',
8 | message: `"${parentSelector}" [${childSelectors.join(', ')}]`,
9 | })
10 |
11 | return () => {
12 | // TODO: add the elements to the log
13 | const doc = cy.state('document')
14 | const texts = []
15 | Cypress.$(doc)
16 | .find(parentSelector)
17 | .each((index, parent) => {
18 | const childTexts = []
19 | childSelectors.forEach((childSelector) => {
20 | const child = Cypress.$(parent).find(childSelector)
21 | if (child.length) {
22 | const text = child.text()
23 | childTexts.push(text)
24 | }
25 | })
26 | texts.push(childTexts)
27 | })
28 | return texts
29 | }
30 | }
31 |
32 | registerQuery('elements', getElements)
33 |
--------------------------------------------------------------------------------
/src/commands/find-one.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('findOne', (predicate) => {
6 | const logOptions = { name: 'findOne' }
7 | if (typeof predicate === 'function') {
8 | logOptions.message = predicate.name
9 | }
10 | const log = Cypress.log(logOptions)
11 |
12 | return (subject) => {
13 | return Cypress._.find(subject, predicate)
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/src/commands/get-in-order.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('getInOrder', (...selectors) => {
6 | // if you pass a single array of selectors, use it as the selectors
7 | if (
8 | Array.isArray(selectors) &&
9 | selectors.length === 1 &&
10 | Array.isArray(selectors[0])
11 | ) {
12 | selectors = selectors[0]
13 | }
14 |
15 | if (!Array.isArray(selectors)) {
16 | throw new Error(`Invalid cy.getInOrder selectors ${selectors}`)
17 | }
18 | const log = Cypress.log({
19 | name: 'getInOrder',
20 | message: selectors.join(','),
21 | })
22 |
23 | return ($subject) => {
24 | let found = $subject
25 | ? $subject.find(selectors[0])
26 | : Cypress.$(selectors[0])
27 |
28 | if (found.length === 0) {
29 | throw new Error(`No elements found for ${selectors[0]}`)
30 | }
31 |
32 | selectors.slice(1).forEach((selector) => {
33 | const next = $subject
34 | ? $subject.find(selector)
35 | : Cypress.$(selector)
36 | if (next.length === 0) {
37 | throw new Error(`No elements found for ${selector}`)
38 | }
39 | // avoid sorting elements by passing the elements as a single array
40 | found = Cypress.$(found.toArray().concat(next.toArray()))
41 | })
42 |
43 | return found
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/commands/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Specifies a function callback for different object properties
3 | */
4 | interface PropertyCallbacks {
5 | [key: string]: Function
6 | }
7 |
8 | /**
9 | * Type for most Cypress commands to control logging and timeout.
10 | */
11 | type CyOptions = Partial
12 |
13 | /**
14 | * How to determine if an element is stable. For example, its text content
15 | * should not change for N milliseconds, or its value. The "element" type
16 | * means the element's reference should be stable for N ms.
17 | */
18 | type StableType = 'text' | 'value' | 'element'
19 |
20 | declare namespace Cypress {
21 | interface Chainable {
22 | /**
23 | * A query command that passes each element from the list or jQuery object
24 | * through the given synchronous function (or extracts the named property)
25 | * @see https://github.com/bahmutov/cypress-map
26 | * @example
27 | * cy.get('#items li').map('innerText')
28 | * @example
29 | * cy.wrap(['10', '20']).map(Number) // [10, 20]
30 | * @example
31 | * cy.wrap({ age: '42' }).map({ age: Number }) // { age: 42 }
32 | * @example
33 | * cy.wrap({ name: 'Joe' age: '42', zip: '90210' })
34 | * .map(['name', 'age']) // { name: ..., age: ... }
35 | */
36 | map(
37 | mapper: string | string[] | Function | PropertyCallbacks,
38 | options?: CyOptions,
39 | ): Chainable
40 |
41 | /**
42 | * A query command that takes every item from the current subject
43 | * and calls a method on it by name, passing the given arguments.
44 | * @see https://github.com/bahmutov/cypress-map
45 | * @example
46 | * // remove the $ from each string
47 | * cy.get('#prices li').map('innerText').mapInvoke('replace', '$', '')
48 | */
49 | mapInvoke(propertyName: string, ...args: any[]): Chainable
50 |
51 | /**
52 | * A regular cy command that takes every item from the current subject
53 | * and calls the given function with the item and its index.
54 | * The function could be synchronous, or async. The function could
55 | * call other Cypress commands and yield the value. All async and
56 | * Cypress commands are queued up and execute one at a time.
57 | * The current subject should be an array.
58 | * Yields the final list of results.
59 | * @see https://github.com/bahmutov/cypress-map
60 | * @example
61 | * // fetch the users from a list of ids
62 | * cy.get(ids).mapChain(id => cy.request('/users/' + id)).then(users => ...)
63 | */
64 | mapChain(fn: Function): Chainable
65 |
66 | /**
67 | * A query command that can log the data without changing it. Useful
68 | * for debugging longer command chains.
69 | * @param fn Function that does not modify the data, `console.log` by default.
70 | * @param label Extra label to log to the Command Log
71 | * @see https://github.com/bahmutov/cypress-map
72 | * @example
73 | * cy.get('#items li').map('innerText').tap(console.log)
74 | */
75 | tap(fn?: Function, label?: string): Chainable
76 |
77 | /**
78 | * A query command that can log the data without changing it. Useful
79 | * for debugging longer command chains.
80 | * @param label Extra label to log to the Command Log
81 | * @see https://github.com/bahmutov/cypress-map
82 | * @example
83 | * cy.get('#items li').map('innerText').tap('text')
84 | */
85 | tap(label: string): Chainable
86 |
87 | /**
88 | * A query command that reduces the list to a single element based on the predicate.
89 | * @see https://github.com/bahmutov/cypress-map
90 | * @param fn Callback function that takes the current accumulator and item
91 | * @param initialValue Optional starting value
92 | * @example
93 | * cy.get('#items li').map('innerText')
94 | */
95 | reduce(fn: Function, initialValue?: any): Chainable
96 |
97 | /**
98 | * A query command that applies the given callback to the subject.
99 | * @see https://github.com/bahmutov/cypress-map
100 | * @param fn Callback function to call
101 | * @example
102 | * cy.wrap(2).apply(double).should('equal', 4)
103 | */
104 | apply(fn: Function): Chainable
105 | /**
106 | * Applies the given function to the arguments and subject
107 | * The subject is **the last argument**.
108 | * @example
109 | * cy.wrap(2).apply(Cypress._.add, 4).should('equal', 6)
110 | */
111 | apply(fn: Function, ...arguments: any[]): Chainable
112 |
113 | /**
114 | * A query command that applies the given callback to the subject.
115 | * Without arguments works the same as `apply`.
116 | * @see https://github.com/bahmutov/cypress-map
117 | * @param fn Callback function to call
118 | * @example
119 | * cy.wrap(2).applyRight(double).should('equal', 4)
120 | */
121 | applyRight(fn: Function): Chainable
122 | /**
123 | * Applies the given function to the arguments and subject
124 | * The subject is **the first argument**.
125 | * @example
126 | * cy.wrap(8).applyRight(Cypress._.subtract, 4).should('equal', 4)
127 | */
128 | applyRight(fn: Function, ...arguments: any[]): Chainable
129 |
130 | /**
131 | * Applies the given function to the arguments and the first item.
132 | * The first item from the current subject is **the last argument**.
133 | * @example
134 | * cy.wrap([2, 3]).applyToFirst(Cypress._.add, 4).should('equal', 6)
135 | */
136 | applyToFirst(fn: Function, ...arguments: any[]): Chainable
137 |
138 | /**
139 | * Calls the specified method on the first item in the current subject.
140 | * @example
141 | * cy.get(selector).invokeFirst('getBoundingClientRect')
142 | */
143 | invokeFirst(
144 | methodName: string,
145 | ...arguments: any[]
146 | ): Chainable
147 |
148 | /**
149 | * Applies the given function to the arguments and the first item.
150 | * The first item from the current subject is **the last argument**.
151 | * @example
152 | * cy.wrap([8, 1]).applyToFirstRight(Cypress._.subtract, 4).should('equal', 4)
153 | */
154 | applyToFirstRight(
155 | fn: Function,
156 | ...arguments: any[]
157 | ): Chainable
158 |
159 | /**
160 | * Creates a callback to apply to the subject by partially applying known arguments.
161 | * @see https://github.com/bahmutov/cypress-map
162 | * @param fn Callback function to call
163 | * @param knownArguments The first argument(s) to the callback
164 | * @example
165 | * cy.wrap(2).partial(Cypress._.add, 4).should('equal', 6)
166 | */
167 | partial(
168 | fn: Function,
169 | ...knownArguments: unknown[]
170 | ): Chainable
171 |
172 | /**
173 | * A query command that returns the first element / item from the subject.
174 | * @see https://github.com/bahmutov/cypress-map
175 | * @example
176 | * cy.get('...').primo()
177 | * @example
178 | * cy.wrap([1, 2, 3]).primo().should('equal', 1)
179 | */
180 | primo(): Chainable
181 |
182 | /**
183 | * Returns the property of the object or DOM element, skipping through jQuery abstraction.
184 | * @see https://github.com/bahmutov/cypress-map
185 | * @param name The property name to yield
186 | * @example
187 | * cy.get('#items li.matching').last().prop('ariaLabel')
188 | */
189 | prop(name: string): Chainable
190 |
191 | /**
192 | * Transforms the named property inside the current subjet
193 | * by passing it through the given callback
194 | * @see https://github.com/bahmutov/cypress-map
195 | * @param prop The name of the property you want to update
196 | * @param callback The function that receives the property value and returns the updated value
197 | * @example
198 | * cy.wrap({ age: '20' }).update('age', Number).should('deep.equal', {age: 20})
199 | */
200 | update(prop: string, callback: Function): Chainable
201 |
202 | /**
203 | * Returns an object or DOM element from the collection at index K.
204 | * Returns elements from the end of the collection for negative K.
205 | * @see https://github.com/bahmutov/cypress-map
206 | * @param index The index of the element
207 | * @example
208 | * cy.get('li').at(0) // the first DOM element
209 | * @example
210 | * cy.wrap([...]).at(-1) // the last item in the array
211 | */
212 | at(index: number): Chainable
213 |
214 | /**
215 | * Returns a randomly picked item from the current subject.
216 | * Uses `_.sample` under the hood.
217 | * @param n Maximum number of items to pick, 1 by default
218 | * @see https://github.com/bahmutov/cypress-map
219 | * @example
220 | * cy.get('li').sample() // one of the list elements
221 | * @example
222 | * cy.wrap([...]).sample() // a random item from the array
223 | */
224 | sample(n?: number): Chainable
225 |
226 | /**
227 | * Yields the second element or array item.
228 | * @example
229 | * cy.get('li').second() // the second DOM element
230 | */
231 | second(): Chainable
232 |
233 | /**
234 | * Yields the third element or array item.
235 | * @example
236 | * cy.get('li').third() // the third DOM element
237 | */
238 | third(): Chainable
239 |
240 | /**
241 | * Prints the current subject and yields it to the next command or assertion.
242 | * @see https://github.com/bahmutov/cypress-map
243 | * @see https://github.com/davidchambers/string-format
244 | * @param format Optional format string, supports "%" and "{}" notation
245 | * @example
246 | * cy.wrap(42)
247 | * .print('the answer is %d')
248 | * .should('equal', 42)
249 | * @example
250 | * cy.wrap({ name: 'Joe' }).print('person %o')
251 | * @example
252 | * cy.wrap({ name: 'Joe' }).print('person {}')
253 | * @example
254 | * cy.wrap({ name: 'Joe' }).print('person {0}')
255 | * @example
256 | * cy.wrap({ name: { first: 'Joe' } }).print('Hello, {0.name.first}')
257 | * @example
258 | * cy.wrap(arr).print('array length {0.length}')
259 | */
260 | print(format?: string | Function): Chainable
261 |
262 | /**
263 | * Collects all cells from the table subject into a 2D array of strings.
264 | * You can slice the array into a smaller region, like a single row, column,
265 | * or a 2D region.
266 | * @example cy.get('table').table()
267 | * @example cy.get('table').table(0, 0, 2, 2)
268 | */
269 | table(
270 | x?: number,
271 | y?: number,
272 | w?: number,
273 | h?: number,
274 | ): Chainable
275 |
276 | /**
277 | * Invokes the method on the current subject.
278 | * This is a COMMAND, not a query, so it won't retry, unlike the stock `cy.invoke`
279 | */
280 | invokeOnce(methodName: string, ...args: unknown[]): Chainable
281 |
282 | /**
283 | * A query command that finds an item in the array or jQuery object.
284 | * Uses Lodash _.find under the hood.
285 | * @see https://github.com/bahmutov/cypress-map
286 | * @example
287 | * cy.get('...').findOne({ innerText: '...' })
288 | * @example
289 | * cy.wrap([1, 2, 3]).findOne(n => n === 3).should('equal', 3)
290 | */
291 | findOne(predicate: object | Function): Chainable
292 |
293 | /**
294 | * A query that calls `JSON.parse(JSON.parse(subject))` or entries.
295 | * When using `entries`, it calls `Object.entries`
296 | * then constructs the object again using `Object.fromEntries`.
297 | * @see https://github.com/bahmutov/cypress-map
298 | * @param conversionType Json by default, could be 'entries'
299 | * @example
300 | * cy.get('selector')
301 | * // yields DOMStringMap
302 | * .should('have.prop', 'dataset')
303 | * .toPlainObject()
304 | * .should('deep.include', { ... })
305 | */
306 | toPlainObject(
307 | conversionType?: 'json' | 'entries',
308 | ): Chainable
309 |
310 | /**
311 | * Calls the given constructor function with "new" keyword
312 | * and the current subject as the only argument.
313 | */
314 | make(constructorFunction: Function): Chainable
315 |
316 | /**
317 | * Saves current subject in `Cypress.env` object.
318 | * Note: Cypress.env object is reset before the spec run,
319 | * but the changed values are passed from test to test.
320 | * @see https://github.com/bahmutov/cypress-map
321 | * @example
322 | * cy.wrap('hello').asEnv('greeting')
323 | */
324 | asEnv(name: string): Chainable
325 |
326 | /**
327 | * Queries each selector and returns the found elements _in the specified order_.
328 | * Retries if the elements are not found for any of the selectors.
329 | * Supports parent subject.
330 | * @example cy.getInOrder('h1', 'h2', 'h3')
331 | * @example cy.get('tr').getInOrder('td', 'th')
332 | */
333 | getInOrder(...selector: string[]): Chainable>
334 | /**
335 | * Queries each selector and returns the found elements _in the specified order_.
336 | * Retries if the elements are not found for any of the selectors.
337 | * Supports parent subject.
338 | * @example cy.getInOrder(['h1', 'h2', 'h3'])
339 | * @example cy.get('tr').getInOrder(['td', 'th'])
340 | */
341 | getInOrder(selectors: string[]): Chainable>
342 |
343 | /**
344 | * Finds the elements using the parent selector,
345 | * then for each finds the children using the child selectors in order.
346 | * Yields the array of arrays of text values.
347 | */
348 | elements(
349 | parent: string,
350 | ...children: string[]
351 | ): Chainable
352 |
353 | /**
354 | * Query the element for N milliseconds to see if its text stays the same.
355 | * If the text changes, reset the timer. Yields the original element.
356 | * @example cy.get('h1').stable('text', 1000)
357 | */
358 | stable(
359 | type: StableType,
360 | ms?: number,
361 | options?: CyOptions,
362 | ): Chainable>
363 | /**
364 | * Query the element for N milliseconds to see if its CSS property stays the same.
365 | * If the property changes, reset the timer. Yields the original element.
366 | * @example cy.get('h1').stable('css', 'background-color', 1000)
367 | */
368 | stable(
369 | type: 'css',
370 | param: string,
371 | ms?: number,
372 | options?: CyOptions,
373 | ): Chainable>
374 |
375 | /**
376 | * Checks if the element is detached from the DOM,
377 | * auto-retries until the element is detached.
378 | * @warning Experimental.
379 | * @example cy.detaches('#name2')
380 | */
381 | detaches(selector: string): void
382 |
383 | /**
384 | * Computes the object with the different properties from the current subject
385 | * and the given expected object. If there are no differences, yields the `{}`
386 | * @example cy.wrap({ name: 'Joe', age: 42 }).difference({ name: 'Joe' })
387 | * // { age: { expected: undefined, actual: 42 } }
388 | */
389 | difference(expected: Object): Chainable
390 | }
391 |
392 | interface Chainer {
393 | /**
394 | * Chai assertion that gets the text from the current subject element
395 | * and compares it to the given text value or against a regular expression.
396 | * @example cy.get('#name').should('read', 'Joe Smith')
397 | * @example cy.get('#name').should('read', /Joe/)
398 | * @see https://github.com/bahmutov/cypress-map
399 | */
400 | (chainer: 'read', text: string | RegExp): Chainable
401 |
402 | /**
403 | * Chai assertion that gets the text from the current subject elements
404 | * and compares them to the given text values or regular expressions.
405 | * @example cy.get('#ages').should('read', ['20', '35', '15'])
406 | * @example cy.get('#ages').should('read', ['20', /^\d+$/, '15'])
407 | * @see https://github.com/bahmutov/cypress-map
408 | */
409 | (chainer: 'read', texts: (string | RegExp)[]): Chainable
410 |
411 | /**
412 | * Checks the presence of the given property in the current subject object.
413 | * Yields the original subject.
414 | * Supports deep nested properties using dot notation.
415 | * Supports arrays and array indexes.
416 | *
417 | * @example cy.wrap({ name: 'Joe' }).should('possess', 'name')
418 | * @example
419 | * cy.wrap({ user: { name: 'Joe' } })
420 | * .should('possess', 'user.name')
421 | * @example
422 | * cy.wrap({ users: ['Joe', 'Jane'] })
423 | * .should('possess', 'users[0]')
424 | *
425 | * @see https://github.com/bahmutov/cypress-map
426 | */
427 | (chainer: 'possess', propertyName: string): Chainable
428 |
429 | /**
430 | * Checks the presence of the given property with the given value
431 | * in the current subject object against the given predicate function.
432 | * Yields the original subject.
433 | * Supports deep nested properties using dot notation.
434 | * Supports arrays and array indexes, brackets are optional
435 | *
436 | * @example
437 | * const isDrinkingAge = (years) => years > 21
438 | * cy.wrap({ age: 42 }).should('possess', 'age', isDrinkingAge)
439 | *
440 | * @see https://github.com/bahmutov/cypress-map
441 | */
442 | (
443 | chainer: 'possess',
444 | propertyName: string,
445 | value: Function,
446 | ): Chainable
447 |
448 | /**
449 | * Checks the presence of the given property with the given value
450 | * in the current subject object.
451 | * Yields the original subject.
452 | * Supports deep nested properties using dot notation.
453 | * Supports arrays and array indexes, brackets are optional
454 | *
455 | * @example cy.wrap({ name: 'Joe' }).should('possess', 'name', 'Joe')
456 | * @example
457 | * cy.wrap({ user: { name: 'Joe' } })
458 | * .should('possess', 'user.name', 'Joe')
459 | * @example
460 | * cy.wrap({ users: [{ name: 'Joe' }, { name: 'Jane' }] })
461 | * .should('possess', 'users[0].name', 'Joe')
462 | * .and('possess', 'users.1.name', 'Jane')
463 | *
464 | * @see https://github.com/bahmutov/cypress-map
465 | */
466 | (
467 | chainer: 'possess',
468 | propertyName: string,
469 | value: unknown,
470 | ): Chainable
471 |
472 | /**
473 | * Checks if the current subject is an array of unique values.
474 | * @example cy.wrap([1, 2, 3]).should('be.unique')
475 | * @example cy.wrap([1, 2, 2]).should('not.be.unique')
476 | * @see https://github.com/bahmutov/cypress-map
477 | */
478 | (chainer: 'unique'): Chainable
479 | }
480 | }
481 |
--------------------------------------------------------------------------------
/src/commands/index.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | require('./assertions')
4 | require('./apply')
5 | require('./as-env')
6 | require('./at')
7 | require('./detaches')
8 | require('./difference')
9 | require('./find-one')
10 | require('./get-in-order')
11 | require('./invoke-once')
12 | require('./make')
13 | require('./map-chain')
14 | require('./map-invoke')
15 | require('./map')
16 | require('./partial')
17 | require('./primo')
18 | require('./print')
19 | require('./prop')
20 | require('./reduce')
21 | require('./sample')
22 | require('./second')
23 | require('./stable')
24 | require('./table')
25 | require('./tap')
26 | require('./third')
27 | require('./to-plain-object')
28 | require('./update')
29 | require('./version-check')
30 | require('./invoke-first')
31 | require('./elements')
32 |
--------------------------------------------------------------------------------
/src/commands/invoke-first.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('invokeFirst', function (methodName, ...args) {
6 | let message = methodName
7 | if (args.length) {
8 | message += ' ' + args.map((x) => JSON.stringify(x)).join(', ')
9 | }
10 | const log = Cypress.log({ name: 'invokeFirst', message })
11 |
12 | return ($el) => {
13 | const first = $el[0]
14 | return first[methodName].apply(first, args)
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/src/commands/invoke-once.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerCommand } = require('./utils')
4 |
5 | registerCommand(
6 | 'invokeOnce',
7 | { prevSubject: true },
8 | (subject, methodName, ...args) => {
9 | let message = methodName
10 | if (args.length) {
11 | message += ' ' + args.map(JSON.stringify).join(',')
12 | }
13 |
14 | const log = Cypress.log({ name: 'invokeOnce', message })
15 |
16 | if (typeof subject[methodName] !== 'function') {
17 | throw new Error(
18 | `Cannot find method ${methodName} on the current subject`,
19 | )
20 | }
21 |
22 | return subject[methodName](...args)
23 | },
24 | )
25 |
--------------------------------------------------------------------------------
/src/commands/make.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('make', (constructorFn) => {
6 | if (typeof constructorFn !== 'function') {
7 | throw new Error('Expected a function')
8 | }
9 |
10 | const log = Cypress.log({
11 | name: 'make',
12 | message: constructorFn.name,
13 | })
14 |
15 | return (subject) => {
16 | return new constructorFn(subject)
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/src/commands/map-chain.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerCommand } = require('./utils')
4 |
5 | registerCommand('mapChain', { prevSubject: 'Array' }, (list, fn) => {
6 | if (!Array.isArray(list)) {
7 | throw new Error('Expected cy.mapChain subject to be an array')
8 | }
9 | if (!list.length) {
10 | // if there are items in the list
11 | // can quickly move on
12 | return []
13 | }
14 |
15 | const results = []
16 |
17 | const produceValue = (k) => {
18 | return cy
19 | .wrap(null, { log: false })
20 | .then(() => fn(list[k], k))
21 | .then((result) => {
22 | results.push(result)
23 | if (k >= list.length - 1) {
24 | // done
25 | } else {
26 | return produceValue(k + 1)
27 | }
28 | })
29 | }
30 |
31 | // make sure we put the possible promises into the command chain
32 | return cy
33 | .wrap(results, { log: false })
34 | .then(() => produceValue(0), { log: false })
35 | .then(() => results)
36 | })
37 |
--------------------------------------------------------------------------------
/src/commands/map-invoke.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('mapInvoke', function (methodName, ...args) {
6 | if (args.length > 0) {
7 | const lastArgument = args.at(-1)
8 | if (
9 | lastArgument &&
10 | Cypress._.isFinite(lastArgument.timeout) &&
11 | lastArgument.timeout > 0
12 | ) {
13 | // make sure this query command respects the timeout option
14 | this.set('timeout', lastArgument.timeout)
15 | }
16 | }
17 |
18 | let message = methodName
19 | if (args.length) {
20 | message += ' ' + args.map((x) => JSON.stringify(x)).join(', ')
21 | }
22 | const log = Cypress.log({ name: 'mapInvoke', message })
23 |
24 | return (list) =>
25 | Cypress._.map(list, (item) => item[methodName].apply(item, args))
26 | })
27 |
--------------------------------------------------------------------------------
/src/commands/map.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const {
4 | registerQuery,
5 | findTimeout,
6 | isArrayOfStrings,
7 | } = require('./utils')
8 |
9 | const repoUrl = 'https://github.com/bahmutov/cypress-map'
10 | const repoLink = `[${repoUrl}](${repoUrl})`
11 |
12 | registerQuery('map', function (fnOrProperty, options = {}) {
13 | const timeout = findTimeout(this, options)
14 |
15 | // make sure this query command respects the timeout option
16 | this.set('timeout', timeout)
17 |
18 | if (isArrayOfStrings(fnOrProperty)) {
19 | // the user wants to pick the listed properties from the subject
20 | const message = fnOrProperty.join(', ')
21 |
22 | const log =
23 | options.log !== false &&
24 | Cypress.log({ name: 'map', message, timeout })
25 | } else if (Cypress._.isPlainObject(fnOrProperty)) {
26 | Object.keys(fnOrProperty).forEach((key) => {
27 | if (typeof fnOrProperty[key] !== 'function') {
28 | throw new Error(`Expected ${key} to be a function`)
29 | }
30 | })
31 |
32 | const message = Cypress._.map(fnOrProperty, (fn, key) => {
33 | return key + '=>' + (fn.name ? fn.name : '?')
34 | }).join(', ')
35 |
36 | const log =
37 | options.log !== false &&
38 | Cypress.log({ name: 'map', message, timeout })
39 | } else {
40 | const message =
41 | typeof fnOrProperty === 'string'
42 | ? fnOrProperty
43 | : fnOrProperty.name
44 |
45 | const log =
46 | options.log !== false &&
47 | Cypress.log({ name: 'map', message, timeout })
48 | }
49 |
50 | return ($el) => {
51 | if (Cypress._.isString($el)) {
52 | throw new Error(
53 | `cy.map is not meant to work with a string subject, did you mean cy.apply?\n${repoLink}`,
54 | )
55 | }
56 | if (Cypress._.isNumber($el)) {
57 | throw new Error(
58 | `cy.map is not meant to work with a number subject, did you mean cy.apply?\n${repoLink}`,
59 | )
60 | }
61 | if (Cypress._.isBoolean($el)) {
62 | throw new Error(
63 | `cy.map is not meant to work with a boolean subject, did you mean cy.apply?\n${repoLink}`,
64 | )
65 | }
66 |
67 | if (isArrayOfStrings(fnOrProperty)) {
68 | if (Array.isArray($el)) {
69 | // map over the array elements
70 | return $el.map((item) => {
71 | return fnOrProperty.reduce((acc, key) => {
72 | if (key.includes('.')) {
73 | // deep property pick
74 | // use the last part of the key as the name
75 | const name = key.split('.').pop()
76 | const value = Cypress._.get(item, key)
77 | acc[name] = value
78 | } else if (!(key in item)) {
79 | throw new Error(`Cannot find property ${key}`)
80 | } else {
81 | acc[key] = item[key]
82 | }
83 | return acc
84 | }, {})
85 | })
86 | }
87 | const result = {}
88 | fnOrProperty.forEach((key) => {
89 | if (key.includes('.')) {
90 | // deep property pick
91 | // use the last part of the key as the name
92 | const name = key.split('.').pop()
93 | const value = Cypress._.get($el, key)
94 | result[name] = value
95 | } else {
96 | if (!(key in $el)) {
97 | throw new Error(`Cannot find property ${key}`)
98 | }
99 | result[key] = $el[key]
100 | }
101 | })
102 | return result
103 | } else if (Cypress._.isPlainObject(fnOrProperty)) {
104 | const result = { ...$el }
105 | Object.keys(fnOrProperty).forEach((key) => {
106 | if (!(key in $el)) {
107 | throw new Error(`Cannot find property ${key}`)
108 | }
109 | if (typeof fnOrProperty[key] !== 'function') {
110 | throw new Error(`Expected ${key} to be a function`)
111 | }
112 | result[key] = fnOrProperty[key]($el[key])
113 | })
114 | return result
115 | } else {
116 | // use a spread so that the result is an array
117 | // and used the Array constructor in the current iframe
118 | // so it passes the "instanceOf Array" assertion
119 | const list = [
120 | ...Cypress._.map($el, (item) =>
121 | typeof fnOrProperty === 'string'
122 | ? Cypress._.get(item, fnOrProperty)
123 | : fnOrProperty(item),
124 | ),
125 | ]
126 | return list
127 | }
128 | }
129 | })
130 |
--------------------------------------------------------------------------------
/src/commands/partial.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('partial', (callback, ...knownArguments) => {
6 | if (typeof callback !== 'function') {
7 | throw new Error('Expected a function to partially apply')
8 | }
9 |
10 | const applied = callback.bind(null, ...knownArguments)
11 | const log = Cypress.log({
12 | name: 'partial',
13 | message: `${callback.name} with ${knownArguments.join(',')}`,
14 | })
15 |
16 | return (subject) => {
17 | log.set({
18 | $el: subject,
19 | })
20 |
21 | return applied(subject)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/src/commands/primo.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('primo', () => {
6 | const log = Cypress.log({ name: 'primo' })
7 |
8 | return (subject) => {
9 | if (Cypress.dom.isJquery(subject)) {
10 | return subject[0]
11 | }
12 | if (Array.isArray(subject)) {
13 | return subject[0]
14 | }
15 | throw new Error('Not sure how to pick the first item')
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/src/commands/print.js:
--------------------------------------------------------------------------------
1 | // to avoid relying on old polyfills for node "format"
2 | // use a custom formatter plus our own code
3 | const format = require('string-format')
4 |
5 | function formatTitle(pattern, x) {
6 | if (pattern.includes('{}') || pattern.includes('{0}')) {
7 | x = JSON.stringify(x)
8 | }
9 | if (pattern.includes('%d')) {
10 | return pattern.replace('%d', x)
11 | }
12 | if (pattern.includes('%o')) {
13 | return pattern.replace('%o', JSON.stringify(x))
14 | }
15 | return format(pattern, x)
16 | }
17 |
18 | const { registerQuery } = require('./utils')
19 |
20 | registerQuery('print', (formatPattern) => {
21 | const log = Cypress.log({ name: 'print', message: '' })
22 |
23 | if (typeof formatPattern === 'string') {
24 | return (subject) => {
25 | const formatted = formatTitle(formatPattern, subject)
26 | log.set('message', formatted)
27 | return subject
28 | }
29 | } else if (typeof formatPattern === 'function') {
30 | return (subject) => {
31 | let formatted = formatPattern(subject)
32 | if (typeof formatted !== 'string') {
33 | formatted = JSON.stringify(formatted)
34 | }
35 | log.set('message', formatted)
36 | return subject
37 | }
38 | } else {
39 | return (subject) => {
40 | log.set('message', JSON.stringify(subject))
41 | return subject
42 | }
43 | }
44 | })
45 |
--------------------------------------------------------------------------------
/src/commands/prop.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('prop', (propertyName) => {
6 | const log = Cypress.log({ name: 'prop', message: propertyName })
7 |
8 | return (subject) => {
9 | if (Cypress.dom.isJquery(subject)) {
10 | return subject.prop(propertyName)
11 | }
12 | return subject[propertyName]
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/src/commands/reduce.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('reduce', (fn, initialValue) => {
6 | if (typeof fn !== 'function') {
7 | throw new Error('Expected a function to apply')
8 | }
9 |
10 | let message = fn.name
11 | if (typeof initialValue !== 'undefined') {
12 | message += ', ' + initialValue
13 | }
14 | const log = Cypress.log({ name: 'reduce', message })
15 |
16 | // see https://lodash.com/docs/ _.reduce documentation
17 | if (typeof initialValue !== 'undefined') {
18 | return (list) => Cypress._.reduce(list, fn, initialValue)
19 | } else {
20 | return (list) => Cypress._.reduce(list, fn)
21 | }
22 | })
23 |
24 | // hmm, should we have ".max" and ".min" helper query commands?
25 |
--------------------------------------------------------------------------------
/src/commands/sample.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('sample', (n = 1) => {
6 | if (n < 1) {
7 | throw new Error(`Sample size should be positive, was ${n}`)
8 | }
9 |
10 | if (n === 1) {
11 | const log = Cypress.log({ name: 'sample' })
12 |
13 | return (subject) => {
14 | // console.log('pick 1 sample')
15 | if (Cypress.dom.isJquery(subject)) {
16 | const randomElement = Cypress._.sample(subject.toArray())
17 | // wrap into jQuery object so other commands
18 | // can be attached, like cy.click
19 | return Cypress.$(randomElement)
20 | }
21 |
22 | // console.log('picked sample', sample)
23 | const currCommand = cy.state('current').attributes
24 | if (
25 | currCommand.name === 'as' &&
26 | currCommand.args[1]?.type === 'static'
27 | ) {
28 | // console.log(
29 | // 'current command',
30 | // cy.state('current').attributes.name,
31 | // )
32 | if (
33 | cy.state('current').attributes?.prev?.attributes?.name ===
34 | 'sample'
35 | ) {
36 | // console.log(cy.state('current').attributes.prev.attributes)
37 | const prevSubject =
38 | cy.state('current').attributes.prev.attributes.subject
39 | // console.log('returning prev sampled subject', prevSubject)
40 |
41 | return prevSubject
42 | }
43 | }
44 | const sample = Cypress._.sample(subject)
45 | // console.log(cy.state('current').attributes.name)
46 | // console.log(cy.state('current').attributes)
47 | // console.log('sampled subject', sample)
48 | // debugger
49 | return sample
50 | }
51 | } else {
52 | const log = Cypress.log({ name: 'sample', message: String(n) })
53 |
54 | return (subject) => {
55 | // console.log('pick N samples')
56 | if (Cypress.dom.isJquery(subject)) {
57 | const randomElement = Cypress._.sampleSize(
58 | subject.toArray(),
59 | n,
60 | )
61 | // wrap into jQuery object so other commands
62 | // can be attached, like cy.click
63 | return Cypress.$(randomElement)
64 | }
65 |
66 | return Cypress._.sampleSize(subject, n)
67 | }
68 | }
69 | })
70 |
--------------------------------------------------------------------------------
/src/commands/second.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('second', () => {
6 | const log = Cypress.log({ name: 'second' })
7 |
8 | return (subject) => {
9 | if (Cypress.dom.isJquery(subject)) {
10 | return subject[1]
11 | }
12 |
13 | if (Array.isArray(subject)) {
14 | return subject[1]
15 | }
16 |
17 | throw new Error('Expected an array or jQuery object')
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/src/commands/stable.ts:
--------------------------------------------------------------------------------
1 | import { registerQuery } from './utils'
2 |
3 | export type StableType = 'text' | 'value' | 'element' | 'css'
4 |
5 | const validStableTypes = ['text', 'value', 'element', 'css']
6 |
7 | // set to console.log if you want to debug the command
8 | const logger = Cypress._.noop
9 | // const logger = console.log
10 |
11 | function stableCss(
12 | param: string,
13 | ms: number = 1000,
14 | options: CyOptions = { log: true },
15 | ) {
16 | const shouldLog = 'log' in options ? options.log : true
17 |
18 | // make sure this query command respects the timeout option
19 | const timeout =
20 | 'timeout' in options
21 | ? options.timeout
22 | : Cypress.config('defaultCommandTimeout')
23 | this.set('timeout', timeout)
24 |
25 | const message = `stable ${param} for ${ms}ms`
26 | const log =
27 | shouldLog &&
28 | Cypress.log({
29 | name: `stable css`,
30 | message,
31 | timeout,
32 | })
33 | logger(log)
34 |
35 | let started = null
36 | let initialValue = null
37 | let initialAt = null
38 | return ($el) => {
39 | if (initialValue === null) {
40 | started = +new Date()
41 | initialValue = $el.css(param)
42 | initialAt = started
43 | logger('started with CSS value %o', initialValue)
44 | throw new Error('start')
45 | }
46 | if ($el.css(param) === initialValue) {
47 | const now = +new Date()
48 | if (now - started > ms) {
49 | logger(
50 | 'after %dms stable CSS %s %o',
51 | now - started,
52 | param,
53 | initialValue,
54 | )
55 | if (shouldLog) {
56 | log.set('consoleProps', () => {
57 | return {
58 | time: now - started,
59 | duration: now - initialAt,
60 | css: param,
61 | result: initialValue,
62 | }
63 | })
64 | }
65 | // yield the original element
66 | // so we can chain more commands and assertions
67 | return $el
68 | } else {
69 | throw new Error('waiting')
70 | }
71 | } else {
72 | started = +new Date()
73 | initialValue = $el.css(param)
74 | logger('CSS value %s changed to %o', param, initialValue)
75 | throw new Error('reset')
76 | }
77 | }
78 | }
79 |
80 | function stableNonCss(
81 | type: StableType,
82 | ms: number = 1000,
83 | options: CyOptions = { log: true },
84 | ) {
85 | if (!validStableTypes.includes(type)) {
86 | throw new Error(`unknown cy.stable type "${type}"`)
87 | }
88 |
89 | const shouldLog = 'log' in options ? options.log : true
90 |
91 | // make sure this query command respects the timeout option
92 | const timeout =
93 | 'timeout' in options
94 | ? options.timeout
95 | : Cypress.config('defaultCommandTimeout')
96 | this.set('timeout', timeout)
97 |
98 | const message = `stable for ${ms}ms`
99 | const log =
100 | shouldLog &&
101 | Cypress.log({
102 | name: `stable ${type}`,
103 | message,
104 | timeout,
105 | })
106 | logger(log)
107 |
108 | if (type === 'text') {
109 | let started = null
110 | let initialText = null
111 | let initialAt = null
112 | return ($el) => {
113 | if (initialText === null) {
114 | started = +new Date()
115 | initialText = $el.text()
116 | initialAt = started
117 | logger('started with text "%s"', initialText)
118 | throw new Error('start')
119 | }
120 | if ($el.text() === initialText) {
121 | const now = +new Date()
122 | if (now - started > ms) {
123 | logger(
124 | 'after %dms stable text "%s"',
125 | now - started,
126 | initialText,
127 | )
128 | if (shouldLog) {
129 | log.set('consoleProps', () => {
130 | return {
131 | time: now - started,
132 | duration: now - initialAt,
133 | result: initialText,
134 | }
135 | })
136 | }
137 | // yield the original element
138 | // so we can chain more commands and assertions
139 | return $el
140 | } else {
141 | throw new Error('waiting')
142 | }
143 | } else {
144 | started = +new Date()
145 | initialText = $el.text()
146 | logger('text changed to "%s"', initialText)
147 | throw new Error('reset')
148 | }
149 | }
150 | } else if (type === 'value') {
151 | let started = null
152 | let initialValue = null
153 | let initialAt = null
154 | return ($el) => {
155 | if (initialValue === null) {
156 | started = +new Date()
157 | initialValue = $el.val()
158 | initialAt = started
159 | logger('started with value %o', initialValue)
160 | throw new Error('start')
161 | }
162 | if ($el.val() === initialValue) {
163 | const now = +new Date()
164 | if (now - started > ms) {
165 | logger(
166 | 'after %dms stable val %o',
167 | now - started,
168 | initialValue,
169 | )
170 | if (shouldLog) {
171 | log.set('consoleProps', () => {
172 | return {
173 | time: now - started,
174 | duration: now - initialAt,
175 | result: initialValue,
176 | }
177 | })
178 | }
179 | // yield the original element
180 | // so we can chain more commands and assertions
181 | return $el
182 | } else {
183 | throw new Error('waiting')
184 | }
185 | } else {
186 | started = +new Date()
187 | initialValue = $el.val()
188 | logger('value changed to %o', initialValue)
189 | throw new Error('reset')
190 | }
191 | }
192 | } else if (type === 'element') {
193 | let started = null
194 | let initialElement = null
195 | let initialAt = null
196 | return ($el) => {
197 | if (initialElement === null) {
198 | if ($el.length !== 1) {
199 | throw new Error('Expected one element to check if stable')
200 | }
201 | started = +new Date()
202 | initialElement = $el[0]
203 | initialAt = started
204 | throw new Error('start')
205 | }
206 | if ($el[0] === initialElement) {
207 | const now = +new Date()
208 | if (now - started > ms) {
209 | logger('after %dms stable element', now - started)
210 | if (shouldLog) {
211 | log.set('consoleProps', () => {
212 | return {
213 | time: now - started,
214 | duration: now - initialAt,
215 | result: initialElement,
216 | }
217 | })
218 | }
219 | // yield the original element
220 | // so we can chain more commands and assertions
221 | return $el
222 | } else {
223 | throw new Error('waiting')
224 | }
225 | } else {
226 | started = +new Date()
227 | initialElement = $el[0]
228 | logger(
229 | 'element changed to "%s"',
230 | initialElement.innerText.substring(0, 100) + '...',
231 | )
232 | throw new Error('reset')
233 | }
234 | }
235 | }
236 | }
237 |
238 | registerQuery(
239 | 'stable',
240 | function (
241 | type: StableType,
242 | param: string,
243 | ms: number = 1000,
244 | options: CyOptions = { log: true },
245 | ) {
246 | if (!validStableTypes.includes(type)) {
247 | throw new Error(`unknown cy.stable type "${type}"`)
248 | }
249 |
250 | if (type === 'css') {
251 | return stableCss.call(this, param, ms, options)
252 | } else {
253 | if (arguments.length === 1) {
254 | return stableNonCss.call(this, type)
255 | } else if (arguments.length === 2) {
256 | return stableNonCss.call(this, type, param)
257 | } else if (arguments.length === 3) {
258 | return stableNonCss.call(this, type, param, ms)
259 | }
260 | }
261 | },
262 | )
263 |
--------------------------------------------------------------------------------
/src/commands/table.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | /**
6 | * Queries the table
7 | * @param {number?} x Top left corner X index (zero based)
8 | * @param {number?} y Top left corner Y index (zero based)
9 | * @param {number?} w Number of columns
10 | * @param {number?} h Number of rows
11 | */
12 | function cyTable(x, y, w, h) {
13 | let message
14 | if (typeof x === 'number' && typeof y === 'number') {
15 | message = `x:${x},y:${y}`
16 | }
17 | if (typeof w === 'number') {
18 | message += `,w:${w}`
19 | }
20 | if (typeof h === 'number') {
21 | message += `,h:${h}`
22 | }
23 | const log = Cypress.log({ name: 'table', message })
24 |
25 | const wSet = typeof w === 'number'
26 | const hSet = typeof h === 'number'
27 |
28 | return ($table) => {
29 | const cells = Cypress._.map($table.find('tr'), (tr) => {
30 | return Cypress._.map(tr.children, 'innerText')
31 | })
32 |
33 | if (typeof x === 'number' && typeof y === 'number') {
34 | if (!wSet && cells.length) {
35 | w = cells[0].length
36 | }
37 | if (!hSet) {
38 | h = cells.length
39 | }
40 | const slice = cells.slice(y, y + h).map((row) => {
41 | return row.slice(x, x + w)
42 | })
43 | return slice
44 | }
45 | return cells
46 | }
47 | }
48 |
49 | registerQuery('table', cyTable)
50 |
--------------------------------------------------------------------------------
/src/commands/tap.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('tap', (fn = console.log, label = undefined) => {
6 | if (typeof fn === 'string') {
7 | // the user passed the label only, like
8 | // cy.tap('numbers')
9 | label = fn
10 | fn = console.log
11 | }
12 |
13 | const logName = label ? label : fn.name
14 | const log = Cypress.log({ name: 'tap', message: logName })
15 |
16 | return (subject) => {
17 | log.set({
18 | $el: subject,
19 | })
20 |
21 | if (typeof label === 'string') {
22 | fn(label, subject)
23 | } else {
24 | fn(subject)
25 | }
26 | return subject
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/src/commands/third.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('third', () => {
6 | const log = Cypress.log({ name: 'third' })
7 |
8 | return (subject) => {
9 | if (Cypress.dom.isJquery(subject)) {
10 | return subject[2]
11 | }
12 |
13 | if (Array.isArray(subject)) {
14 | return subject[2]
15 | }
16 |
17 | throw new Error('Expected an array or jQuery object')
18 | }
19 | })
20 |
--------------------------------------------------------------------------------
/src/commands/to-plain-object.js:
--------------------------------------------------------------------------------
1 | const { registerQuery } = require('./utils')
2 |
3 | registerQuery('toPlainObject', (conversionType = 'json') => {
4 | const log = Cypress.log({
5 | name: 'toPlainObject',
6 | message: conversionType,
7 | })
8 |
9 | const jsonConversion = (subject) => {
10 | return JSON.parse(JSON.stringify(subject))
11 | }
12 | const entriesConversion = (subject) => {
13 | return Object.fromEntries(subject.entries())
14 | }
15 |
16 | if (conversionType === 'json') {
17 | return jsonConversion
18 | } else if (conversionType === 'entries') {
19 | return entriesConversion
20 | } else {
21 | throw new Error(`unknown conversion type: ${conversionType}`)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/src/commands/update.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const { registerQuery } = require('./utils')
4 |
5 | registerQuery('update', (prop, callback) => {
6 | if (typeof callback !== 'function') {
7 | throw new Error('Expected a function to apply')
8 | }
9 |
10 | const log = Cypress.log({
11 | name: 'update',
12 | message: `${prop} by ${callback.name}`,
13 | })
14 |
15 | return (subject) => {
16 | log.set({
17 | $el: subject,
18 | })
19 |
20 | return { ...subject, [prop]: callback(subject[prop]) }
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/commands/utils.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | /**
4 | * @see https://on.cypress.io/custom-commands
5 | */
6 | function registerQuery(name, fn) {
7 | // prevent double registration attempt
8 | if (!(name in cy)) {
9 | return Cypress.Commands.addQuery(name, fn)
10 | }
11 | }
12 |
13 | function registerCommand(name, options, fn) {
14 | // prevent double registration attempt
15 | if (!(name in cy)) {
16 | if (typeof options === 'function') {
17 | return Cypress.Commands.add(name, fn)
18 | } else {
19 | return Cypress.Commands.add(name, options, fn)
20 | }
21 | }
22 | }
23 |
24 | /**
25 | * Finds the timeout option from this command or from its parent command
26 | */
27 | function findTimeout(cmd, options = {}) {
28 | if (Cypress._.isFinite(options.timeout)) {
29 | return options.timeout
30 | }
31 |
32 | const defaultTimeout = Cypress.config('defaultCommandTimeout')
33 | if (!cmd) {
34 | return defaultTimeout
35 | }
36 | const prev = cmd.attributes?.prev
37 | const prevTimeout = prev?.attributes?.timeout
38 | if (Cypress._.isFinite(prevTimeout)) {
39 | return prevTimeout
40 | }
41 | if (prev.attributes.args?.length) {
42 | const lastArg = prev.attributes.args.at(-1)
43 | if (Cypress._.isFinite(lastArg?.timeout)) {
44 | return lastArg?.timeout
45 | }
46 | }
47 |
48 | return defaultTimeout
49 | }
50 |
51 | /**
52 | * Returns true if the argument is an array of strings.
53 | * Note: an empty array is not considered an array of strings.
54 | */
55 | function isArrayOfStrings(x) {
56 | return (
57 | Cypress._.isArray(x) &&
58 | x.length &&
59 | x.every((item) => typeof item === 'string')
60 | )
61 | }
62 |
63 | module.exports = {
64 | registerQuery,
65 | registerCommand,
66 | findTimeout,
67 | isArrayOfStrings,
68 | }
69 |
--------------------------------------------------------------------------------
/src/commands/version-check.js:
--------------------------------------------------------------------------------
1 | const major = Cypress.version.split('.').map(Number)[0]
2 | if (major < 12) {
3 | throw new Error(`cypress-map requires Cypress version >= 12`)
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["DOM", "ES2021"],
4 | "types": ["cypress"],
5 | "target": "ES5",
6 | "outDir": "./commands",
7 | "allowJs": true,
8 | "esModuleInterop": true,
9 | },
10 | "include": ["src/commands/*.*"],
11 | }
12 |
--------------------------------------------------------------------------------