├── .github └── workflows │ ├── badges.yml │ ├── lint.yml │ └── main.yml ├── .gitignore ├── .prettierrc.json ├── README.md ├── cypress-esbuild.config.js ├── cypress.config.js ├── cypress ├── README.md ├── e2e │ ├── deep-equal.cy.js │ ├── expect.cy.js │ ├── spec.cy.js │ └── two-failures.cy.js ├── fixtures │ └── example.json ├── plugins │ └── index.js └── support │ └── e2e.js ├── img ├── cy-spok.gif ├── spok.gif ├── two.png └── vs-deep-equal.png ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json └── renovate.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 4 * * *' 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: npx -p dependency-version-badge update-badge cypress 19 | 20 | - name: Commit any changed files 💾 21 | uses: stefanzweifel/git-auto-commit-action@v5 22 | with: 23 | commit_message: Updated badges 24 | branch: master 25 | file_pattern: README.md 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: [push] 3 | jobs: 4 | build-and-test: 5 | runs-on: ubuntu-latest 6 | name: Check code style 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: bahmutov/npm-install@v1 10 | - run: npm run lint 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | name: Build and test 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: cypress-io/github-action@v5 10 | 11 | test-esbuild: 12 | runs-on: ubuntu-latest 13 | name: Build and test 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cypress-io/github-action@v5 17 | with: 18 | config-file: cypress-esbuild.config.js 19 | 20 | release: 21 | needs: [test, test-esbuild] 22 | runs-on: ubuntu-24.04 23 | if: github.ref == 'refs/heads/master' 24 | steps: 25 | - name: Checkout 🛎 26 | uses: actions/checkout@v4 27 | 28 | - name: Install only the semantic release 📦 29 | run: npm install semantic-release 30 | 31 | # https://github.com/cycjimmy/semantic-release-action 32 | - name: Semantic Release 🚀 33 | uses: cycjimmy/semantic-release-action@v3 34 | with: 35 | branch: master 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cypress/videos 3 | cypress/screenshots/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cy-spok [![renovate-app badge][renovate-badge]][renovate-app] ![cypress version](https://img.shields.io/badge/cypress-14.4.0-brightgreen) 2 | 3 | > Playing with [spok](https://github.com/thlorenz/spok) inside Cypress test 4 | 5 | 6 | CI | status 7 | --- | --- 8 | [lint](.github/workflows/lint.yml) | ![Lint status](https://github.com/bahmutov/cy-spok/workflows/lint/badge.svg?branch=master) 9 | [badges](.github/workflows/badges.yml) | ![Badges status](https://github.com/bahmutov/cy-spok/workflows/badges/badge.svg?branch=master) 10 | [cy-spok](.github/workflows/main.yml) | ![GH Action status](https://github.com/bahmutov/cy-spok/workflows/main/badge.svg?branch=master) 11 | [cy-spok-example](https://github.com/bahmutov/cy-spok-example) | ![cy-spok-example status](https://github.com/bahmutov/cy-spok-example/workflows/tests/badge.svg?branch=master) 12 | 13 | 14 | ## Learn 15 | 16 | - 📺 Watch [Introduction To cy-spok Plugin For Writing Powerful Assertions For Objects](https://www.youtube.com/watch?v=MLDsqBd_gVU) 17 | - 📺 Watch [Confirm Text In Multiple Page Elements Using cy-spok](https://youtu.be/l6_OXPiqkxQ) 18 | - 📺 Watch [Verify Cypress Component Prop Calls Using Stubs, cypress-map, and cy-spok](https://youtu.be/zdjjBRt6Z74) 19 | - 📺 Watch [Refactor Cypress API Tests Part 1: Use cy-spok Plugin](https://youtu.be/zGO3LNx-agk) 20 | - 📝 Read [Asserting Network Calls from Cypress Tests](https://www.cypress.io/blog/2019/12/23/asserting-network-calls-from-cypress-tests/) 21 | - 📝 Read [Change E2E Tests From UI To API To App Actions](https://glebbahmutov.com/blog/ui-to-api-to-app-actions/) 22 | - 📝 Read [Two Simple Tricks To Make Your Cypress Tests Better](https://glebbahmutov.com/blog/two-cypress-tricks/) 23 | - 📝 Read [Crawl Weather Using Cypress](https://glebbahmutov.com/blog/crawl-weather/) 24 | - 📝 Read [Testing React Number Format Component Example](https://glebbahmutov.com/blog/test-react-number-format/) 25 | - 📝 Read [Cypress Flakiness Examples](https://glebbahmutov.com/blog/flakiness-example/) 26 | - 📝 Read [Use Cypress For API Testing](https://glebbahmutov.com/blog/use-cypress-for-api-testing/) 27 | - 📝 Read [Check Fees And Totals Using Cypress](https://glebbahmutov.com/blog/check-fees-using-cypress/) 28 | - 📝 Read [Retry Network Requests](https://glebbahmutov.com/blog/retry-network-requests/) 29 | - 🎓 Covered in my course [Cypress Plugins](https://cypress.tips/courses/cypress-plugins) 30 | - [Lesson e1: Validate network requests using cy-spok](https://cypress.tips/courses/cypress-plugins/lessons/e1) 31 | - [Lesson e2: Use your own predicates to validate object properties](https://cypress.tips/courses/cypress-plugins/lessons/e2) 32 | - [Lesson e3: Validate URL search parameters](https://cypress.tips/courses/cypress-plugins/lessons/e3) 33 | - [Lesson e4: Check and control the Redux store using cy-spok plugin](https://cypress.tips/courses/cypress-plugins/lessons/e4) 34 | - 🎓 Covered in my course [Cypress Network Testing Exercises](https://cypress.tips/courses/network-testing) 35 | - [Bonus 28: Use cy-spok plugin to write complex assertions](https://cypress.tips/courses/network-testing/lessons/bonus28) 36 | - [Bonus 101: Confirm the server supports caching using the ETag header](https://cypress.tips/courses/network-testing/lessons/bonus101) 37 | - 🎓 Covered in my course [Cypress vs Playwright](https://cypress.tips/courses/cypress-vs-playwright) 38 | - [Lesson d8: Assert complex objects](https://cypress.tips/courses/cypress-vs-playwright/lessons/d8) 39 | 40 | ## Install 41 | 42 | ``` 43 | $ npm i -D cy-spok 44 | ``` 45 | 46 | ## Use 47 | 48 | See [spok](https://github.com/thlorenz/spok#readme) docs 49 | 50 | ```js 51 | // in your Cypress spec file 52 | import spok from 'cy-spok' 53 | 54 | const object = { 55 | one: 1, 56 | two: 2, 57 | three: 3, 58 | four: 4, 59 | helloWorld: 'hello world', 60 | anyNum: 999, 61 | anotherNum: 888, 62 | anArray: [1, 2], 63 | anotherArray: [1, 2, 3], 64 | anObject: {}, 65 | id: 'abc123', 66 | list: ['one', 'two', 'three'], 67 | } 68 | 69 | // using Spok 70 | // https://github.com/thlorenz/spok#readme 71 | cy.wrap(object, { timeout: 2000 }).should( 72 | spok({ 73 | $topic: 'spok-example', // optional 74 | one: spok.ge(1), 75 | two: 2, 76 | three: spok.range(2, 6), 77 | four: spok.lt(5), 78 | helloWorld: spok.startsWith('hello'), 79 | anyNum: spok.type('number'), 80 | anotherNum: spok.number, 81 | anArray: spok.array, 82 | anObject: spok.ne(undefined), 83 | // test a string using regular expression 84 | id: spok.test(/^abc\d{3}$/), 85 | // array with 3 elements 86 | list: spok.arrayElements(3), 87 | }), 88 | ) 89 | ``` 90 | 91 | See [cypress/integration/spec.js](cypress/integration/spec.js) here and in the [cy-spok-example](https://github.com/bahmutov/cy-spok-example) repo. 92 | 93 | ![Spok in action](img/cy-spok.gif) 94 | 95 | ## vs deep.equal 96 | 97 | Spok prints a lot more information when using it compared to `deep.equal`. Note that Spok is a subset, not strict value equality. 98 | 99 | ![deep.equal vs spok for complex objects](./img/vs-deep-equal.png) 100 | 101 | See [deep-equal-spec.js](./cypress/integration/deep-equal-spec.js) 102 | 103 | ## Treat arrays as an object 104 | 105 | ```js 106 | // verify each item in an array as an object 107 | cy.wrap(['one', 42, 'three']).should( 108 | spok({ 109 | 0: spok.string, 110 | 1: spok.number, 111 | 2: spok.string, 112 | }), 113 | ) 114 | ``` 115 | 116 | ## Use Lodash predicates 117 | 118 | Lodash has many predicate functions `is...`, see [Lodash docs](https://lodash.com/docs) 119 | 120 | ```js 121 | cy.wrap({ 122 | name: 'Joe', 123 | age: 20, 124 | }).should( 125 | spok({ 126 | name: Cypress._.isString, 127 | age: Cypress._.isNumber, 128 | }), 129 | ) 130 | ``` 131 | 132 | ## Own predicate 133 | 134 | Any synchronous function that returns a boolean could be a predicate 135 | 136 | ```js 137 | // it is a list of strings 138 | const areGreetings = (list) => 139 | Array.isArray(list) && list.every(Cypress._.isString) 140 | 141 | cy.wrap({ 142 | greetings: ['hello', 'hi'], 143 | }).should( 144 | spok({ 145 | greetings: areGreetings, 146 | }), 147 | ) 148 | ``` 149 | 150 | ## First failure only 151 | 152 | If there are multiple failing predicates, only the first one is shown. All passing predicates are shown 153 | 154 | ```js 155 | cy.wrap({ 156 | name: 'Joe', 157 | age: 42, 158 | job: 'chimney sweeper', 159 | location: 'Boston', 160 | present: true, 161 | }).should( 162 | spok({ 163 | name: 'Mary', // fails 164 | age: 42, // passes 165 | job: 'secret agent', // fails 166 | location: 'Boston', // passes 167 | present: spok.type('boolean'), // passes 168 | }), 169 | ) 170 | ``` 171 | 172 | ![Only the first failed predicate is shown](./img/two.png) 173 | 174 | ## Small print 175 | 176 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2021 177 | 178 | - [@bahmutov](https://twitter.com/bahmutov) 179 | - [glebbahmutov.com](https://glebbahmutov.com) 180 | - [blog](https://glebbahmutov.com/blog) 181 | - [videos](https://www.youtube.com/glebbahmutov) 182 | - [presentations](https://slides.com/bahmutov) 183 | - [cypress.tips](https://cypress.tips) 184 | - [Cypress Advent 2021](https://cypresstips.substack.com/) 185 | 186 | License: MIT - do anything with the code, but don't blame me if it does not work. 187 | 188 | Support: if you find any problems with this module, email / tweet / 189 | [open issue](https://github.com/bahmutov/cy-spok/issues) on Github 190 | 191 | ## MIT License 192 | 193 | Copyright (c) 2021 Gleb Bahmutov <gleb.bahmutov@gmail.com> 194 | 195 | Permission is hereby granted, free of charge, to any person 196 | obtaining a copy of this software and associated documentation 197 | files (the "Software"), to deal in the Software without 198 | restriction, including without limitation the rights to use, 199 | copy, modify, merge, publish, distribute, sublicense, and/or sell 200 | copies of the Software, and to permit persons to whom the 201 | Software is furnished to do so, subject to the following 202 | conditions: 203 | 204 | The above copyright notice and this permission notice shall be 205 | included in all copies or substantial portions of the Software. 206 | 207 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 208 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 209 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 210 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 211 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 212 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 213 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 214 | OTHER DEALINGS IN THE SOFTWARE. 215 | 216 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 217 | [renovate-app]: https://renovateapp.com/ 218 | -------------------------------------------------------------------------------- /cypress-esbuild.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | // https://github.com/bahmutov/cypress-esbuild-preprocessor 3 | const createBundler = require('@bahmutov/cypress-esbuild-preprocessor') 4 | 5 | module.exports = defineConfig({ 6 | e2e: { 7 | // We've imported your old cypress plugins here. 8 | // You may want to clean this up later by importing these. 9 | setupNodeEvents(on, config) { 10 | on('file:preprocessor', createBundler()) 11 | return require('./cypress/plugins/index.js')(on, config) 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // We've imported your old cypress plugins here. 6 | // You may want to clean this up later by importing these. 7 | setupNodeEvents(on, config) { 8 | return require('./cypress/plugins/index.js')(on, config) 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/README.md: -------------------------------------------------------------------------------- 1 | # Cypress.io end-to-end tests 2 | 3 | [Cypress.io](https://www.cypress.io) is an open source, MIT licensed end-to-end test runner 4 | 5 | ## Folder structure 6 | 7 | These folders hold end-to-end tests and supporting files for the Cypress Test Runner. 8 | 9 | - [fixtures](fixtures) holds optional JSON data for mocking, [read more](https://on.cypress.io/fixture) 10 | - [integration](integration) holds the actual test files, [read more](https://on.cypress.io/writing-and-organizing-tests) 11 | - [plugins](plugins) allow you to customize how tests are loaded, [read more](https://on.cypress.io/plugins) 12 | - [support](support) file runs before all tests and is a great place to write or load additional custom commands, [read more](https://on.cypress.io/writing-and-organizing-tests#Support-file) 13 | 14 | ## `cypress.json` file 15 | 16 | You can configure project options in the [../cypress.json](../cypress.json) file, see [Cypress configuration doc](https://on.cypress.io/configuration). 17 | 18 | ## More information 19 | 20 | - [https://github.com/cypress.io/cypress](https://github.com/cypress.io/cypress) 21 | - [https://docs.cypress.io/](https://docs.cypress.io/) 22 | - [Writing your first Cypress test](http://on.cypress.io/intro) 23 | -------------------------------------------------------------------------------- /cypress/e2e/deep-equal.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import spok from '../..' 4 | 5 | it('default deep.equal vs spok', () => { 6 | const person = { 7 | name: { 8 | first: 'John', 9 | last: 'Doe', 10 | }, 11 | age: 30, 12 | address: { 13 | street: '123 Main St.', 14 | city: 'Anytown', 15 | state: 'CA', 16 | zip: 12345, 17 | }, 18 | } 19 | 20 | const expected = Cypress._.cloneDeep(person) 21 | // simulate failure by changing one of the props 22 | // expected.address.city = 'New York' 23 | // delete person.address.city 24 | 25 | cy.log('default deep.equal') 26 | cy.wrap(person, { timeout: 0 }).should('deep.equal', expected) 27 | 28 | cy.log('using spok') 29 | // note: spok only fails if the values are not equal 30 | // but can pass if the expected object has fewer properties 31 | cy.wrap(person, { timeout: 0 }).should(spok(expected)) 32 | }) 33 | 34 | it('compares arrays by value', () => { 35 | const a = [1, { name: 'Joe' }, [3, 4]] 36 | cy.wrap(a, { timeout: 0 }).should( 37 | spok({ 38 | length: 3, 39 | 0: 1, // at first position is number 1 40 | 1: { name: spok.string }, // an object with a string property "name" 41 | 2: (a) => Array.isArray(a) && a.length === 2, // array with two elements 42 | }), 43 | ) 44 | }) 45 | -------------------------------------------------------------------------------- /cypress/e2e/expect.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import spok from '@bahmutov/spok' 4 | 5 | // using the original spok 6 | // https://github.com/thlorenz/spok 7 | // const t = spok.adapters.chaiExpect(expect) 8 | 9 | // crashes with "strip is not a function" error 10 | it.skip('works with expect', () => { 11 | spok(t, { name: 'joe' }, { name: spok.string }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import spok from '../..' 4 | 5 | it('spoks', () => { 6 | const object = { 7 | one: 1, 8 | two: 2, 9 | three: 3, 10 | four: 4, 11 | helloWorld: 'hello world', 12 | anyNum: 999, 13 | anotherNum: 888, 14 | anArray: [1, 2], 15 | anotherArray: [1, 2, 3], 16 | anObject: {}, 17 | id: 'abc123', 18 | list: ['one', 'two', 'three'], 19 | } 20 | 21 | // using Spok 22 | // https://github.com/thlorenz/spok#readme 23 | cy.wrap(object, { timeout: 2000 }).should( 24 | spok({ 25 | $topic: 'spok-example', 26 | one: spok.ge(1), 27 | two: 2, 28 | three: spok.range(2, 6), 29 | four: spok.lt(5), 30 | helloWorld: spok.startsWith('hello'), 31 | anyNum: spok.type('number'), 32 | anotherNum: spok.number, 33 | anArray: spok.array, 34 | anObject: spok.ne(undefined), 35 | // test a string using regular expression 36 | id: spok.test(/^abc\d{3}$/), 37 | // array with 3 elements 38 | list: spok.arrayElements(3), 39 | }), 40 | ) 41 | }) 42 | 43 | it('spoks several times', () => { 44 | const o = { 45 | one: 1, 46 | two: 2, 47 | three: 3, 48 | } 49 | // let's check if o.three is a number between 2 and 4 50 | cy.wrap(o) 51 | .should(spok({ one: 1 })) 52 | .and(spok({ two: 2, three: spok.number })) 53 | .and(spok({ three: spok.gt(2) })) 54 | .and(spok({ three: spok.lt(4) })) 55 | }) 56 | 57 | it('retries until all pass', () => { 58 | const object = { 59 | one: 1, 60 | two: -1, // starts as incorrect value 61 | } 62 | 63 | cy.wrap(object, { timeout: 2000 }).should( 64 | spok({ 65 | $topic: 'spok-retries', 66 | one: 1, 67 | two: 2, 68 | }), 69 | ) 70 | 71 | setTimeout(() => { 72 | // fix the property 73 | object.two = 2 74 | }, 500) 75 | }) 76 | 77 | it('uses should cb', () => { 78 | const o = { 79 | name: 'Hello world', 80 | guess: 50, 81 | } 82 | 83 | // using "regular" should callback function 84 | cy.wrap(o).should((obj) => { 85 | expect(obj.name).to.match(/^Hello/) 86 | expect(obj.guess).to.be.gte(1).and.lte(10) 87 | }) 88 | 89 | setTimeout(() => { 90 | o.guess = 7 91 | }, 1000) 92 | }) 93 | 94 | // to verify that can attach multiple spoks that use chained "should" callbacks 95 | // https://github.com/cypress-io/cypress/issues/5979 96 | // https://github.com/bahmutov/cy-spok/issues/7 97 | it('two should callbacks (crashes and burns)', () => { 98 | cy.wrap(null) 99 | .should(() => { 100 | console.log('in should()') 101 | expect(true).to.be.true 102 | expect(true).to.be.true 103 | }) 104 | .and(() => { 105 | console.log('in and ()') 106 | }) 107 | }) 108 | 109 | it('checks arrays', () => { 110 | cy.wrap(42).should('be.a', 'number').and('equal', 42) 111 | // verify each item in an array as an object 112 | cy.wrap(['one', 42, 'three']).should( 113 | spok({ 114 | 0: spok.string, 115 | 1: spok.number, 116 | 2: spok.string, 117 | }), 118 | ) 119 | }) 120 | 121 | it('uses Cypress._ predicates', () => { 122 | cy.wrap({ 123 | name: 'Joe', 124 | age: 20, 125 | }).should( 126 | spok({ 127 | name: Cypress._.isString, 128 | age: Cypress._.isNumber, 129 | }), 130 | ) 131 | }) 132 | 133 | it('own predicate', () => { 134 | // it is a list of strings 135 | const areGreetings = (list) => 136 | Array.isArray(list) && list.every(Cypress._.isString) 137 | 138 | cy.wrap({ 139 | greetings: ['hello', 'hi'], 140 | }).should( 141 | spok({ 142 | greetings: areGreetings, 143 | }), 144 | ) 145 | }) 146 | -------------------------------------------------------------------------------- /cypress/e2e/two-failures.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import spok from '../..' 4 | 5 | // skip the test because it is meant to show the failures 6 | it.skip('shows the first failure only', () => { 7 | // in the test below, there are 3 passing checks 8 | // and two failures. Only the first failure 9 | // is shown, and all passing checks are shown 10 | cy.wrap({ 11 | name: 'Joe', 12 | age: 42, 13 | job: 'chimney sweeper', 14 | location: 'Boston', 15 | present: true, 16 | }).should( 17 | spok({ 18 | name: 'Mary', // fails 19 | age: 42, // passes 20 | job: 'secret agent', // fails 21 | location: 'Boston', // passes 22 | present: spok.type('boolean'), // passes 23 | }), 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | -------------------------------------------------------------------------------- /img/cy-spok.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-spok/08de890319356bf4c5dbff85b2a3ca3f4b3e7151/img/cy-spok.gif -------------------------------------------------------------------------------- /img/spok.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-spok/08de890319356bf4c5dbff85b2a3ca3f4b3e7151/img/spok.gif -------------------------------------------------------------------------------- /img/two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-spok/08de890319356bf4c5dbff85b2a3ca3f4b3e7151/img/two.png -------------------------------------------------------------------------------- /img/vs-deep-equal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cy-spok/08de890319356bf4c5dbff85b2a3ca3f4b3e7151/img/vs-deep-equal.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SpokAssertions, 3 | SpokConfig, 4 | Specifications, 5 | SpokFunctionAny, 6 | Assert 7 | } from 'spok/dist/types' 8 | import { ExpectFn } from 'spok/dist/adapter-chai-expect' 9 | 10 | declare type SpokFunction = (currentSubject: Subject) => void 11 | 12 | declare type SpokHelper = ( 13 | specifications: Specifications 14 | ) => SpokFunction 15 | 16 | declare const Spok: SpokHelper & 17 | SpokAssertions & 18 | SpokConfig & { 19 | any: SpokFunctionAny 20 | adapters: { 21 | chaiExpect: (expectFn: ExpectFn) => Assert 22 | } 23 | } 24 | 25 | export = Spok 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const spok = require('@bahmutov/spok').default 2 | const stripAnsi = require('strip-ansi') 3 | 4 | if (!Cypress) { 5 | throw new Error('Missing Cypress global') 6 | } 7 | if (!Cypress._) { 8 | throw new Error('Missing Cypress._ property') 9 | } 10 | if (!Cypress._.isEqual) { 11 | throw new Error('Missing Cypress._.isEqual method') 12 | } 13 | 14 | spok.color = false 15 | spok.printDescription = false 16 | 17 | class Assert { 18 | constructor() { 19 | this.failed = [] 20 | this.passed = [] 21 | } 22 | 23 | equal(actual, expected, msg) { 24 | if (actual !== expected) { 25 | this.failed.push(msg) 26 | } else { 27 | this.passed.push(msg) 28 | } 29 | } 30 | 31 | deepEqual(actual, expected, msg) { 32 | const pass = Cypress._.isEqual(actual, expected) 33 | if (!pass) { 34 | this.failed.push(msg) 35 | } else { 36 | this.passed.push(msg) 37 | } 38 | } 39 | } 40 | 41 | const spokHelper = (expectation) => { 42 | return function (value) { 43 | const assert = new Assert() 44 | spok(assert, value, expectation) 45 | 46 | // by default, Chai assertions will print actual and expected values 47 | // but Spok already gives us the complete error message 48 | // we overwrite util.getMessage to simply return Spok's message 49 | // Make sure to restore the original getMessage in all circumstances! 50 | const chaiUtilGetMessage = chai.util.getMessage 51 | 52 | try { 53 | chai.util.getMessage = function (assert, args) { 54 | return assert.__flags.message 55 | } 56 | 57 | assert.passed.forEach((message) => { 58 | const msg = stripAnsi(message) 59 | // create passing assertion in the Command Log 60 | expect(true, msg).to.be.true 61 | }) 62 | 63 | assert.failed.forEach((message) => { 64 | const msg = stripAnsi(message) 65 | // create failing assertion in the Command Log 66 | expect(true, msg).to.be.false 67 | }) 68 | } finally { 69 | chai.util.getMessage = chaiUtilGetMessage 70 | } 71 | } 72 | } 73 | // adds all spok conditions 74 | Object.assign(spokHelper, spok) 75 | spokHelper.spok = spok 76 | 77 | module.exports = spokHelper 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cy-spok", 3 | "version": "0.0.0-development", 4 | "description": "Wrapper for spok assertion", 5 | "main": "index.js", 6 | "private": false, 7 | "scripts": { 8 | "test": "cypress run", 9 | "lint": "prettier --check '*.js' 'cypress/**/*.js'", 10 | "fix": "prettier --write '*.js' 'cypress/**/*.js'" 11 | }, 12 | "keywords": [ 13 | "spok", 14 | "cypress-plugin" 15 | ], 16 | "author": "Gleb Bahmutov ", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@bahmutov/spok": "1.2.6", 20 | "strip-ansi": "6.0.1" 21 | }, 22 | "devDependencies": { 23 | "@bahmutov/cypress-esbuild-preprocessor": "^2.1.5", 24 | "cypress": "14.4.0", 25 | "esbuild": "^0.25.0", 26 | "prettier": "3.5.3", 27 | "semantic-release": "24.2.5" 28 | }, 29 | "files": [ 30 | "index.js", 31 | "index.d.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "labels": ["type: dependencies", "renovate"], 5 | "masterIssue": true, 6 | "prConcurrentLimit": 4, 7 | "prHourlyLimit": 4, 8 | "timezone": "America/New_York", 9 | "schedule": ["after 11pm and before 1am on every weekday", "every weekend"] 10 | } 11 | --------------------------------------------------------------------------------