├── .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] 
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) | 
9 | [badges](.github/workflows/badges.yml) | 
10 | [cy-spok](.github/workflows/main.yml) | 
11 | [cy-spok-example](https://github.com/bahmutov/cy-spok-example) | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------