1
0
Fork 0
mirror of https://github.com/datashard/snapshot.git synced 2025-06-05 12:17:21 +00:00

Compare commits

..

43 commits

Author SHA1 Message Date
Renovate Bot
6d01fdda57 fix(deps): update dependency @wildpeaks/snapshot-dom to v1.4.0 2019-12-22 01:05:28 +00:00
Renovate Bot
b65b13f8b6 chore(deps): update dependency semantic-release to v15.14.0 2019-12-22 00:09:08 +00:00
Renovate Bot
9d3211a8b0 chore(deps): update dependency mocha to v6.2.2 2019-12-21 23:08:26 +00:00
Renovate Bot
0a14849823 fix(deps): update dependency js-beautify to v1.10.2 2019-12-21 22:08:43 +00:00
Renovate Bot
28462fc9e9 chore(deps): update dependency semantic-release to v15.13.32 2019-12-21 21:05:36 +00:00
Renovate Bot
bdad90afd5 chore(deps): update dependency cypress to v3.8.0 2019-12-21 20:49:33 +00:00
Renovate Bot
0a6277cee8 chore(deps): lock file maintenance 2019-06-10 05:15:31 +00:00
Renovate Bot
9a34a0b643 chore(deps): lock file maintenance 2019-06-03 05:12:13 +00:00
Renovate Bot
8d5e82d1b6 chore(deps): lock file maintenance 2019-05-27 05:09:57 +00:00
Renovate Bot
d0b1084f98 chore(deps): update dependency cypress to v3.3.1 2019-05-25 06:09:55 +00:00
renovate[bot]
0b5a37487a chore(deps): update dependency mocha to v6 () 2019-05-20 11:31:52 -04:00
Renovate Bot
bc08a1d270 chore(deps): lock file maintenance 2019-05-20 05:18:44 +00:00
Renovate Bot
cc5c9a9fd4 chore(deps): update dependency cypress to v3.3.0 2019-05-18 05:14:36 +00:00
Renovate Bot
c1a21de66d chore(deps): lock file maintenance 2019-05-13 06:08:37 +00:00
Renovate Bot
1bc9013b0c chore(deps): lock file maintenance 2019-05-13 05:11:42 +00:00
Renovate Bot
bd560d6af9 fix(deps): update dependency snap-shot-compare to v2.8.3 2019-05-11 12:11:13 +00:00
Renovate Bot
7c8658d0ef fix(deps): update dependency snap-shot-compare to v2.8.2 2019-05-11 06:10:45 +00:00
Renovate Bot
ac9a2afb49 chore(deps): update dependency semantic-release to v15.13.12 2019-05-11 05:05:31 +00:00
Renovate Bot
40f062389c chore(deps): lock file maintenance 2019-05-06 05:09:18 +00:00
Renovate Bot
fb4d9fe89a fix(deps): update dependency js-beautify to v1.10.0 2019-05-04 05:12:26 +00:00
Gleb Bahmutov
c836e5531e
add links to a few other snapshots plugins 2019-04-30 17:34:18 -04:00
renovate[bot]
9fdc5ec16f chore(deps): update dependency cypress to v3 () 2019-04-30 12:02:29 -04:00
renovate[bot]
89000de57f chore(deps): update dependency cypress to v2 () 2019-04-30 11:27:36 -04:00
Gleb Bahmutov
4c35f7e2bf chore: remove github-post-release 2019-04-30 11:14:14 -04:00
Gleb Bahmutov
ad1ed62a71 chore: remove pre-git to avoid clashing with release 2019-04-30 11:02:02 -04:00
Renovate Bot
4481828479 chore(deps): pin dependency semantic-release to 15.13.3 2019-04-30 14:57:29 +00:00
Gleb Bahmutov
140f94d1b0 chore: update semantic release 2019-04-30 10:51:27 -04:00
Renovate Bot
9e863c08a4 chore(deps): lock file maintenance 2019-04-29 05:13:13 +00:00
Renovate Bot
c8e3ee89c1 fix(deps): update dependency js-beautify to v1.9.1 2019-04-27 15:05:30 +00:00
Renovate Bot
f927fb13fe chore(deps): update dependency pre-git to v3.17.1 2019-04-27 14:13:15 +00:00
Renovate Bot
41f76f29e5 chore(deps): update dependency mocha to v4.1.0 2019-04-27 13:07:59 +00:00
Renovate Bot
c7e36ef39b chore(deps): update dependency eslint to v4.19.1 2019-04-27 12:13:19 +00:00
Renovate Bot
a743cfbccc chore(deps): update dependency deps-ok to v1.4.1 2019-04-27 11:13:50 +00:00
Renovate Bot
7ac7e096a7 chore(deps): update dependency dependency-check to v2.10.1 2019-04-27 10:09:16 +00:00
Renovate Bot
7e3141f6dc chore(deps): update dependency debug to v3.2.6 2019-04-27 09:11:26 +00:00
Renovate Bot
ce7a6569b9 chore(deps): update dependency cypress to v1.4.2 2019-04-27 08:13:26 +00:00
Renovate Bot
6340cc2f6f chore(deps): update dependency eslint to v4.13.1 2019-04-27 07:09:24 +00:00
Renovate Bot
34c503323b chore(deps): update dependency deps-ok to v1.2.4 2019-04-27 06:05:42 +00:00
Renovate Bot
cc47c50650 chore(deps): update dependency dependency-check to v2.9.2 2019-04-27 05:07:33 +00:00
Stuart Long
f2f7d76d75 feat: add support for relative snapshots ()
* feat: Add capability for relative snapshot files

* feat: remove path from package

* feat: remove nsp from pre-push since it's deprecated

* feat: use Cypress config instead

* feat: fix build
2019-04-26 16:38:33 -04:00
Gleb Bahmutov
a68d97bb76
Merge pull request from stuartlong/stuartlo/add-update-instructions
feat(log): Add instructions about updating snapshot to error message
2019-04-26 16:28:19 -04:00
Gleb Bahmutov
ac3afd73b2
Merge pull request from cypress-io/renovate/pin-dependencies
chore(deps): pin dependency semantic-release to 8.2.3
2019-04-26 10:33:10 -04:00
Renovate Bot
21941f89c7
chore(deps): pin dependency semantic-release to 8.2.3 2019-04-26 14:20:30 +00:00
48 changed files with 11775 additions and 5050 deletions

12
.eslintrc Normal file
View file

@ -0,0 +1,12 @@
{
"extends": [
"plugin:cypress-dev/general"
],
"rules": {
"comma-dangle": "off",
"no-debugger": "warn"
},
"env": {
"node": true
}
}

Binary file not shown.

Before

(image error) Size: 9 KiB

Binary file not shown.

Before

(image error) Size: 14 KiB

Binary file not shown.

Before

(image error) Size: 76 KiB

Binary file not shown.

Before

(image error) Size: 41 KiB

View file

@ -1,12 +0,0 @@
name: Cypress
on: [push, pull_request, workflow_call, workflow_dispatch]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Dependencies
run: npm i
- name: Test
run: npm run cypress:run

10
.gitignore vendored
View file

@ -1,6 +1,4 @@
node_modules/
.DS_Store
npm-debug.log
cypress/videos/
cypress/screenshots/
cypress/snapshots/Arrays.json
node_modules/
.DS_Store
npm-debug.log
cypress/videos/

View file

@ -1,5 +0,0 @@
cypress/
cypress.config.js
.github/workflows/
.vscode/
renovate.json

8
.npmrc
View file

@ -1,4 +1,4 @@
registry=https://registry.npmjs.org/
save-exact=true
progress=false
package-lock=true
registry=http://registry.npmjs.org/
save-exact=true
progress=false
package-lock=true

16
.travis.yml Normal file
View file

@ -0,0 +1,16 @@
language: node_js
notifications:
email: true
node_js:
- 10
# Retry install on fail to avoid failing a build on network/disk/external errors
install:
- travis_retry npm ci
script:
- npm run test
- npm run cypress:run
after_success:
- npm run semantic-release

7
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"standard.enable": false,
"eslint.enable": true,
"eslint.autoFixOnSave": false,
"git.ignoreLimitWarning": true,
"standard.autoFixOnSave": false
}

22
LICENSE
View file

@ -1,22 +0,0 @@
Copyright (c) 2023-2024 Joshua data@shard.wtf
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

264
README.md
View file

@ -1,92 +1,172 @@
# @datashard/snapshot
> Adds JSON Snapshot comparison to Cypress
## ⚠️ Breaking Changes ⚠️
- With V3, the `readFileMaybe` task has been removed, we now rely on `cy.fixture` internally.
- the previous `SNAPSHOT_UPDATE` Environment/Config Variable has been changed to `updateSnapshots`
> [!DANGER]
This means that previous tests will likely be broken, *please make sure that your tests pass before updating to the latest version of this Plugin*.
This current release will be released as `3.0.0-beta`, should Bugs be found by me or my Employer, I will open Issues/PRs to fix those, should anyone else find Bugs/Edgecases, etc. please open an Issue.
## Install
Requires Node 16 or above
```sh
npm i --save-dev @datashard/snapshot
```
## Import
After Installing, you'll need to add the following import into your Commands/Support File
> by default this will be `cypress/support/e2e.js`
```js
require('@datashard/snapshot').regsiter()
```
This will register a new Command `.snapshot()`, to create and compare JSON Snapshots.
## Config
You can pass `updateSnapshots` and `useFolders` as options in the `cypress.config.js` file
![Example Settings for the Module](./.github/assets/config.png)
Alternatively, you can also add `snapshotUpdate` as an Environment Variable to update your snapshots.
Simply pass `--env updateSnapshots=true` when running Cypress.
## Usage
If properly added, usage of this plugin is rather simple, just add `.snapshot()` to cypress functions that return valid JSON. (i.e. `cy.wrap`)
When properly added, you can chain `.snapshot()` off of `cy` functions like `cy.wrap`, just make sure they return valid JSON.
### Example
```js
describe("my test", () => {
it("works", () => {
cy.log("first snapshot");
cy.wrap({ foo: 42 }).snapshot("foo");
cy.log("second snapshot");
cy.wrap({ bar: 101 }).snapshot("bar");
});
});
```
This Plugin will then save your snapshots as
```ts
// useFolders: false
cypress/fixtures/my-test__works__foo.json
cypress/fixtures/my-test__works__bar.json
// useFolders: true
cypress/fixtures/my-test/works/foo.json
cypress/fixtures/my-test/works/bar.json
// {fixtureFolder}/<Context>-<Describe>-<It>-<Name?>.json
// {fixtureFolder}/<Context>/<Describe>/<It>/<Name?>.json
```
Snapshots will generally be saved using this the Convention mentioned in the Comment of the above Codeblock, which is provided by the named Cypress Test Steps.
Passing a name to the Snapshot function is required, but not checked, if you want to take multiple snapshots in the same block.
If you have two or more Snapshots in the same Block, the next one ***WILL*** always overwrite the previous one while updating, causing the First Snapshot in the Block to Fail.
While running your Tests, if a value changed, it will, of course, no longer match the snapshot and throw an Error.
Which will look like this:
![](./.github/assets/Error.png)
When the Test succeeds, it will instead log a Success in the Log and let you know where the File has been saved to, relative to the Fixture Snapshot Folder
![](./.github/assets/Correct.png)
# @cypress/snapshot
> Adds value / object / DOM element snapshot testing support to Cypress test runner
[![NPM][npm-icon] ][npm-url]
[![Build status][ci-image] ][ci-url]
[![semantic-release][semantic-image] ][semantic-url]
[![renovate-app badge][renovate-badge]][renovate-app]
## Note
Please take a look at a few other Cypress snapshot plugins: [cypress-plugin-snapshots](https://github.com/meinaart/cypress-plugin-snapshots), [cypress-image-snapshot](https://github.com/palmerhq/cypress-image-snapshot).
## Install
Requires [Node](https://nodejs.org/en/) version 6 or above.
```sh
npm install --save-dev @cypress/snapshot
```
## Use
After installing, add the following to your `cypress/support/commands.js` file
```js
require('@cypress/snapshot').register()
```
This registers a new command to create new snapshot or compare value to old snapshot
```js
describe('my tests', () => {
it('works', () => {
cy.log('first snapshot')
cy.wrap({ foo: 42 }).snapshot()
cy.log('second snapshot')
cy.wrap({ bar: 101 }).snapshot()
})
})
describe('focused input field', () => {
it('is empty and then typed into', () => {
cy.visit('http://todomvc.com/examples/react/')
cy
.focused()
.snapshot('initial')
.type('eat healthy breakfast')
.snapshot('after typing')
})
})
```
By default, the snapshot object can be found in file `snapshots.js`. In the above case it would look something like this
```js
module.exports = {
"focused input field": {
"is empty and then typed into": {
"initial": {
"tagName": "input",
"attributes": {
"class": "new-todo",
"placeholder": "What needs to be done?",
"value": ""
}
},
"after typing": {
"tagName": "input",
"attributes": {
"class": "new-todo",
"placeholder": "What needs to be done?",
"value": "eat healthy breakfast"
}
}
}
},
"my tests": {
"works": {
"1": {
"foo": 42
},
"2": {
"bar": 101
}
}
}
}
```
If you change the site values, the saved snapshot will no longer match, throwing an error
![Snapshot mismatch](img/snapshot-mismatch.png)
Click on the `SNAPSHOT` step in the Command Log to see expected and current value printed in the DevTools.
### Options
You can control snapshot comparison and behavior through a few options.
```js
cy.get(...).snapshot({
name: 'human snapshot name', // to use in the snapshot file
json: false // convert DOM elements into JSON
// when storing in the snapshot file
})
```
### Configuration
This module provides some configuration options:
#### useRelativeSnapshots
Set to true in order to store your snapshots for each test run next to the inital test caller rather
than at the base working directory.
**Note:** requires the `readFileMaybe` plugin to be configured see https://on.cypress.io/task#Read-a-file-that-might-not-exist
#### snapshotFileName
Set to a string to name your snapshot something other than 'snapshots.js'
#### Usage
Set the configuration options as part of the Cypress config.
See https://docs.cypress.io/guides/references/configuration.html
## Debugging
To debug this module run with environment variable `DEBUG=@cypress/snapshot`
### Small print
Author: Gleb Bahmutov &lt;gleb@cypress.io&gt; &copy; Cypress.io 2017
License: MIT - do anything with the code, but don't blame u if it does not work.
Support: if you find any problems with this module, email / tweet /
[open issue](https://github.com/cypress-io/snapshot/issues) on Github
## MIT License
Copyright (c) 2017 Cypress.io &lt;gleb@cypress.io&gt;
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
[npm-icon]: https://nodei.co/npm/@cypress/snapshot.svg?downloads=true
[npm-url]: https://npmjs.org/package/@cypress/snapshot
[ci-image]: https://travis-ci.org/cypress-io/snapshot.svg?branch=master
[ci-url]: https://travis-ci.org/cypress-io/snapshot
[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[semantic-url]: https://github.com/semantic-release/semantic-release
[renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg
[renovate-app]: https://renovateapp.com/

View file

@ -1,16 +0,0 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
// fixturesFolder: "cypress/fixtures",
snapshot: {
// updateSnapshots: true,
useFolders: true,
// useSnapshotFolder: true
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

1
cypress.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1,78 +0,0 @@
/* eslint-env mocha */
/* global cy */
describe("datashard/snapshot", () => {
context("simple types"
// , { env: { updateSnapshots: true } }
, () => {
it("works with objects", () => {
cy.wrap({
"foo": "bar",
"Fizzy Drink": "Pop"
}).snapshot();
});
it("works with numbers", () => {
cy.wrap(42).snapshot({
snapshotPath: "cypress/fixtures/snapshots",
snapshotName: "Numbers",
});
});
it("works with strings", () => {
cy.wrap("foo-bar").snapshot({
snapshotPath: "cypress/fixtures/snapshots",
snapshotName: "Strings",
});
});
it(
"works with arrays",
{
env: {
updateSnapshots: true,
},
},
() => {
cy.wrap([1, 2, 3, 4]).snapshot({
snapshotPath: "cypress/fixtures/snapshots",
snapshotName: "Arrays",
});
}
);
});
context("complex types"
// , { env: { SNAPSHOT_UPDATE: true } }
, () => {
it('works with more "complicated" Objects', () => {
cy.wrap({
"status": 200,
"response": {
"array": [0, 1, 2, "4"],
"object": {
"with": "more details"
}
},
"thisisnew": "wtf"
}).snapshot()
})
it("works based on fixtures", () => {
cy
.wrap({
"jsonapi": {
"version": "2.0"
},
"included": [
{
"type": "users",
"id": "2",
"attributes": {
"name": "Test"
}
}
]
})
.snapshot();
});
})
});

View file

@ -1,23 +0,0 @@
/* eslint-env mocha */
/* global cy */
describe("Random Describe", () => {
context("Random Context", () => {
it("Random It", () => {
cy.wrap(42).snapshot("Numbers");
cy.wrap("foo-bar").snapshot("Strings");
cy.wrap([1, 2, 3]).snapshot("Arrays");
});
// it("works with numbers", () => {
// console.log(cy.wrap(42))
// });
// it("works with strings", () => {
// console.log(cy.wrap("foo-bar"))
// });
// it("works with arrays", () => {
// console.log(cy.wrap([1, 2, 3]))
// });
});
});

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -1,7 +0,0 @@
{
"data": [
1,
2,
3
]
}

View file

@ -1,14 +0,0 @@
{
"jsonapi": {
"version": "2.0"
},
"included": [
{
"type": "users",
"id": "2",
"attributes": {
"name": "Test"
}
}
]
}

View file

@ -1,15 +0,0 @@
{
"status": 200,
"response": {
"array": [
0,
1,
2,
"4"
],
"object": {
"with": "more details"
}
},
"thisisnew": "wtf"
}

View file

@ -1,8 +0,0 @@
{
"data": [
1,
2,
3,
4
]
}

View file

@ -1,4 +0,0 @@
{
"foo": "bar",
"Fizzy Drink": "Pop"
}

View file

@ -0,0 +1,25 @@
/* eslint-env mocha */
/* global cy */
describe('@cypress/snapshot', () => {
beforeEach(() => {
cy.visit('https://example.cypress.io')
})
context('simple types', () => {
it('works with objects', () => {
cy.wrap({ foo: 42 }).snapshot()
})
it('works with numbers', () => {
cy.wrap(42).snapshot()
})
it('works with strings', () => {
cy.wrap('foo-bar').snapshot()
})
it('works with arrays', () => {
cy.wrap([1, 2, 3]).snapshot()
})
})
})

View file

@ -1,17 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View file

@ -1,4 +1,2 @@
// register .snapshot() command
require('../../src/index').register()
// register .snapshot() command
require('../..').register()

View file

@ -1,20 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

BIN
img/snapshot-mismatch.png Normal file

Binary file not shown.

After

(image error) Size: 84 KiB

12
issue_template.md Normal file
View file

@ -0,0 +1,12 @@
Thank you for taking time to open a new issue. Please answer a few questions to help us fix it faster. You can delete text that is irrelevant to the issue.
## Is this a bug report or a feature request?
If this is a bug report, please provide as much info as possible
- version
- platform
- expected behavior
- actual behavior
If this is a new feature request, please describe it below

15288
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,50 +1,72 @@
{
"name": "@datashard/snapshot",
"description": "Adds JSON Snapshot testing support to Cypress",
"version": "3.0.0-beta.1",
"author": "Joshua <data@shard.wtf>",
"bugs": "https://github.com/datashard/snapshot/issues",
"engines": {
"node": ">=6"
},
"files": [
"img",
"src/*",
"src/*/**",
"!src/*-spec.js"
],
"homepage": "https://shard.wtf/snapshot",
"keywords": [
"cypress",
"cypress-io",
"plugin",
"snapshot",
"testing", "json"
],
"license": "MIT",
"main": "src/index.js",
"private": false,
"repository": {
"type": "git",
"url": "https://github.com/datashard/snapshot.git"
},
"scripts": {
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"unused-deps": "dependency-check --unused --no-dev . --entry src/index.js",
"semantic-release": "semantic-release",
"cypress:open": "cypress open",
"cypress:update": "cypress run --env updateSnapshots=true",
"cypress:run": "cypress run"
},
"devDependencies": {
"cypress": "12.13.0",
"debug": "3.2.7",
"dependency-check": "2.10.1"
},
"dependencies": {
"@wildpeaks/snapshot-dom": "1.6.0",
"check-more-types": "2.24.0",
"js-beautify": "1.13.13",
"lazy-ass": "1.6.0"
}
}
{
"name": "@cypress/snapshot",
"description": "Adds value / object / DOM element snapshot testing support to Cypress test runner",
"version": "0.0.0-development",
"author": "Gleb Bahmutov <gleb@cypress.io>",
"bugs": "https://github.com/cypress-io/snapshot/issues",
"engines": {
"node": ">=6"
},
"files": [
"img",
"src/*.js",
"!src/*-spec.js"
],
"homepage": "https://github.com/cypress-io/snapshot#readme",
"keywords": [
"cypress",
"cypress-io",
"plugin",
"snapshot",
"testing"
],
"license": "MIT",
"main": "src/",
"private": false,
"publishConfig": {
"registry": "http://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/cypress-io/snapshot.git"
},
"scripts": {
"ban": "ban",
"deps": "deps-ok && dependency-check --no-dev .",
"issues": "git-issues",
"license": "license-checker --production --onlyunknown --csv",
"lint": "eslint --fix src/*.js",
"pretest": "npm run lint",
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"test": "npm run unit",
"unit": "mocha src/*-spec.js",
"unused-deps": "dependency-check --unused --no-dev . --entry src/add-initial-snapshot-file.js",
"postinstall": "node src/add-initial-snapshot-file.js",
"semantic-release": "semantic-release",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
},
"devDependencies": {
"ban-sensitive-files": "1.9.2",
"cypress": "3.8.0",
"debug": "3.2.6",
"dependency-check": "2.10.1",
"deps-ok": "1.4.1",
"eslint": "4.19.1",
"eslint-plugin-cypress-dev": "1.1.2",
"git-issues": "1.3.1",
"license-checker": "15.0.0",
"mocha": "6.2.2",
"semantic-release": "15.14.0"
},
"dependencies": {
"@wildpeaks/snapshot-dom": "1.4.0",
"am-i-a-dependency": "1.1.2",
"check-more-types": "2.24.0",
"its-name": "1.0.0",
"js-beautify": "1.10.2",
"lazy-ass": "1.6.0",
"snap-shot-compare": "2.8.3",
"snap-shot-store": "1.2.3"
}
}

View file

@ -1,17 +1,24 @@
{
"extends": ["config:base"],
"automerge": true,
"major": {
"automerge": false
},
"updateNotScheduled": false,
"timezone": "Europe/Berlin",
"schedule": ["every weekend"],
"lockFileMaintenance": {
"enabled": true
},
"separatePatchReleases": true,
"separateMultipleMajor": true,
"masterIssue": true,
"labels": ["type: dependencies", "renovate"]
}
{
"extends": [
"config:base"
],
"automerge": true,
"major": {
"automerge": false
},
"updateNotScheduled": false,
"timezone": "America/New_York",
"schedule": [
"every weekend"
],
"lockFileMaintenance": {
"enabled": true
},
"separatePatchReleases": true,
"separateMultipleMajor": true,
"masterIssue": true,
"labels": [
"type: dependencies",
"renovate"
]
}

25
snapshots.js Normal file
View file

@ -0,0 +1,25 @@
module.exports = {
"@cypress/snapshot": {
"simple types": {
"works with objects": {
"1": {
"foo": 42
}
},
"works with numbers": {
"1": 42
},
"works with strings": {
"1": "foo-bar"
},
"works with arrays": {
"1": [
1,
2,
3
]
}
}
},
"__version": "1.1.4"
}

View file

@ -0,0 +1,25 @@
const debug = require('debug')('@cypress/snapshot')
const fs = require('fs')
const path = require('path')
const utils = require('./utils')
const amDependency = require('am-i-a-dependency')()
if (amDependency) {
// yes, do something interesting
// someone is executing "npm install foo"
debug('post install - in folder', process.cwd())
// we are in <owner>/node_modules/@cypress/snapshot
// but want to be simply in <owner> folder
const ownerFolder = path.normalize(path.join(process.cwd(), '..', '..', '..'))
const filename = path.join(ownerFolder, utils.SNAPSHOT_FILE_NAME)
if (!fs.existsSync(filename)) {
// save initial empty snapshot object
debug('writing initial file', filename)
fs.writeFileSync(filename, '{}\n')
} else {
debug('file %s already exists', filename)
}
} else {
debug('not a dependency install')
}

View file

@ -1,6 +1,211 @@
// global cy, Cypress
const register = require("./register");
module.exports = {
register
};
'use strict'
/* global cy, Cypress */
const itsName = require('its-name')
const { initStore } = require('snap-shot-store')
const la = require('lazy-ass')
const is = require('check-more-types')
const compare = require('snap-shot-compare')
const path = require('path')
const {
serializeDomElement,
serializeReactToHTML,
identity,
countSnapshots
} = require('./utils')
const DEFAULT_CONFIG_OPTIONS = {
// using relative snapshots requires a simple
// 'readFileMaybe' plugin to be configured
// see https://on.cypress.io/task#Read-a-file-that-might-not-exist
useRelativeSnapshots: false,
snapshotFileName: 'snapshots.js'
}
/* eslint-disable no-console */
function compareValues ({ expected, value }) {
const noColor = true
const json = true
return compare({ expected, value, noColor, json })
}
function registerCypressSnapshot () {
la(is.fn(global.before), 'missing global before function')
la(is.fn(global.after), 'missing global after function')
la(is.object(global.Cypress), 'missing Cypress object')
const useRelative = Cypress.config('useRelativeSnapshots')
const config = {
useRelativeSnapshots: useRelative === undefined ? DEFAULT_CONFIG_OPTIONS.useRelativeSnapshots : useRelative,
snapshotFileName: Cypress.config('snapshotFileName') || DEFAULT_CONFIG_OPTIONS.snapshotFileName
}
console.log('registering @cypress/snapshot')
let storeSnapshot
// for each full test name, keeps number of snapshots
// allows using multiple snapshots inside single test
// without confusing them
// eslint-disable-next-line immutable/no-let
let counters = {}
function getSnapshotIndex (key) {
if (key in counters) {
// eslint-disable-next-line immutable/no-mutation
counters[key] += 1
} else {
// eslint-disable-next-line immutable/no-mutation
counters[key] = 1
}
return counters[key]
}
let snapshotFileName = config.snapshotFileName
if (config.useRelativeSnapshots) {
let relative = Cypress.spec.relative
if (Cypress.platform === 'win32') {
relative = relative.replace(/\\/g, path.sep)
}
snapshotFileName = path.join(path.dirname(relative), config.snapshotFileName)
}
function evaluateLoadedSnapShots (js) {
la(is.string(js), 'expected JavaScript snapshot source', js)
console.log('read snapshots.js file')
const store = eval(js) || {}
console.log('have %d snapshot(s)', countSnapshots(store))
storeSnapshot = initStore(store)
}
global.before(function loadSnapshots () {
let readFile
if (config.useRelativeSnapshots) {
readFile = cy
.task('readFileMaybe', snapshotFileName)
.then(function (contents) {
if (!contents) {
return cy.writeFile(snapshotFileName, '', 'utf-8', { log: false })
}
return contents
})
} else {
readFile = cy
.readFile(snapshotFileName, 'utf-8')
}
readFile.then(evaluateLoadedSnapShots)
// no way to catch an error yet
})
function getTestName (test) {
const names = itsName(test)
// la(is.strings(names), 'could not get name from current test', test)
return names
}
function getSnapshotName (test, humanName) {
const names = getTestName(test)
const key = names.join(' - ')
const index = humanName || getSnapshotIndex(key)
names.push(String(index))
return names
}
function setSnapshot (name, value, $el) {
// snapshots were not initialized
if (!storeSnapshot) {
return
}
// show just the last part of the name list (the index)
const message = Cypress._.last(name)
console.log('current snapshot name', name)
const devToolsLog = {
value
}
if (Cypress.dom.isJquery($el)) {
// only add DOM elements, otherwise "expected" value is enough
devToolsLog.$el = $el
}
const options = {
name: 'snapshot',
message,
consoleProps: () => devToolsLog
}
if ($el) {
options.$el = $el
}
const cyRaiser = ({ value, expected }) => {
const result = compareValues({ expected, value })
result.orElse((json) => {
// by deleting property and adding it at the last position
// we reorder how the object is displayed
// We want convenient:
// - message
// - expected
// - value
devToolsLog.message = json.message
devToolsLog.expected = expected
delete devToolsLog.value
devToolsLog.value = value
throw new Error(`Snapshot difference. To update, delete snapshot and rerun test.\n${json.message}`)
})
}
Cypress.log(options)
storeSnapshot({
value,
name,
raiser: cyRaiser
})
}
const pickSerializer = (asJson, value) => {
if (Cypress.dom.isJquery(value)) {
return asJson ? serializeDomElement : serializeReactToHTML
}
return identity
}
function snapshot (value, { name, json } = {}) {
console.log('human name', name)
const snapshotName = getSnapshotName(this.test, name)
const serializer = pickSerializer(json, value)
const serialized = serializer(value)
setSnapshot(snapshotName, serialized, value)
// always just pass value
return value
}
Cypress.Commands.add('snapshot', { prevSubject: true }, snapshot)
global.after(function saveSnapshots () {
if (storeSnapshot) {
const snapshots = storeSnapshot()
console.log('%d snapshot(s) on finish', countSnapshots(snapshots))
console.log(snapshots)
snapshots.__version = Cypress.version
const s = JSON.stringify(snapshots, null, 2)
const str = `module.exports = ${s}\n`
cy.writeFile(snapshotFileName, str, 'utf-8', { log: false })
}
})
return snapshot
}
module.exports = {
register: registerCypressSnapshot
}

View file

@ -1,119 +0,0 @@
const lazy = require("lazy-ass");
const is = require("check-more-types");
const snapshot = require("./snapshot");
const baseStyles = [
{
name: 'info',
color: '#cbd5e1',
},
{
name: 'success',
color: '#10b981',
},
{
name: 'warning',
color: '#fbbf24',
},
{
name: 'error',
color: '#dc2626',
},
]
/**
* Helper function to convert hex colors to rgb
* @param {string} hex - hex color
* @returns {string}
*
* @example
* // returns "255 255 255"
* hex2rgb("#ffffff")
*/
function hex2rgb(hex) {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `${r} ${g} ${b}`
}
baseStyles.forEach((style) => {
createCustomLog(style.name, style.color)
})
/**
* Create a custom log
* @param {string} name - Name of the custom log
* @param {string} baseColor - Base color of the custom log in hex format
*
* @example
* // Create a custom log with name "misc" and base color "#9333ea"
* createCustomLog("misc", "#9333ea")
*/
function createCustomLog(name, baseColor) {
if (!name || !baseColor) {
throw new Error('Missing parameters')
}
const logStyle = document.createElement('style')
logStyle.textContent = `
.command.command-name-log-${name} span.command-method {
margin-right: 0.5rem;
min-width: 10px;
border-radius: 0.125rem;
border-width: 1px;
padding-left: 0.375rem;
padding-right: 0.375rem;
padding-top: 0.125rem;
padding-bottom: 0.125rem;
text-transform: uppercase;
border-color: rgb(${hex2rgb(baseColor)} / 1);
background-color: rgb(${hex2rgb(baseColor)} / 0.2);
color: rgb(${hex2rgb(baseColor)} / 1) !important;
}
.command.command-name-log-${name} span.command-message{
color: rgb(${hex2rgb(baseColor)} / 1);
font-weight: normal;
}
.command.command-name-log-${name} span.command-message strong,
.command.command-name-log-${name} span.command-message em {
color: rgb(${hex2rgb(baseColor)} / 1);
}
`
Cypress.$(window.top.document.head).append(logStyle)
}
/**
* Print a message with a formatted style
* @param {Object} log - The message to be printed.
* @param {string} log.title - The title of the message.
* @param {string} log.message - The content of the message.
* @param {('info' | 'warning' | 'error' | 'success')} log.type - The type of the message.
*
* @example
* // Print a message with a formatted style e.g. success
* cy.print({ title: 'foo', message: 'bar', type: 'success' })
*/
function cyPrint({ title, message, type }) {
Cypress.log({
name: `log-${type}`,
displayName: `${title}`,
message,
})
}
module.exports = () => {
lazy(is.fn(global.before), "Missing global before function");
lazy(is.fn(global.after), "Missing global after function");
lazy(is.object(global.Cypress), "Missing Cypress object");
Cypress.Commands.add("snapshot", { prevSubject: "optional" }, snapshot);
Cypress.Commands.add('SNAPSHOT_prettyprint', cyPrint)
};

16
src/snapshot-spec.js Normal file
View file

@ -0,0 +1,16 @@
'use strict'
/* eslint-env mocha */
const api = require('.')
const la = require('lazy-ass')
const is = require('check-more-types')
describe('@cypress/snapshot', () => {
it('is an object', () => {
la(is.object(api))
})
it('has register', () => {
la(is.fn(api.register))
})
})

View file

@ -1,124 +0,0 @@
const serializeDomElement = require("./utils/serializers/serializeDomElement");
const serializeToHTML = require("./utils/serializers/serializeToHTML");
const compareValues = require("./utils/compareValues");
const path = require("path");
const identity = (x) => x;
const pickSerializer = (asJson, value) => {
if (Cypress.dom.isJquery(value)) {
return asJson ? serializeDomElement : serializeToHTML;
}
return identity;
};
/**
*
* @param {string} text
* @returns {string}
*/
const parseTextToJSON = (text) => text.replace(/\| [✅➖➕⭕]/g, "").trim().replace(/(.*?),\s*(\}|])/g, "$1$2").replace(/},(?!")$/g, "}").replaceAll(/[╺┿╳]/g, "")
const store_snapshot = ({ value, name, raiser } = { value, name, raiser }) => {
if (Cypress.env().updateSnapshots || Cypress.config('snapshot').updateSnapshots) {
cy.SNAPSHOT_prettyprint({ title: "INFO", type: "info", message: "Saving Snapshot" })
cy.writeFile(path.join(Cypress.config().fixturesFolder, `${name}.json`), JSON.stringify(value, null, 2))
} else {
cy.fixture(name).then(expected => raiser({ value, expected }))
}
};
const set_snapshot = ({ snapshotName, serialized, value }) => {
let devToolsLog = { $el: serialized };
if (Cypress.dom.isJquery(value)) {
devToolsLog.$el = value;
}
let options = {
name: "snapshot",
message: snapshotName,
consoleProps: () => { return devToolsLog },
...(value && { $el: value })
};
const raiser = ({ value, expected }) => {
const result = compareValues({ expected, value });
if ((!Cypress.env().updateSnapshots || !Cypress.config('snapshot').updateSnapshots) && !result.success) {
devToolsLog = () => {
return { expected, value }
};
const error = (inError) => `
Snapshot Difference found.\nPlease Update the Snapshot ${inError ? `\n${JSON.stringify(JSON.parse(parseTextToJSON(result.result)), null, 3).replaceAll(' ', "&nbsp;")}` : ""}`
Cypress.log({ message: error(true) })
throw new Error(error());
} else {
cy.SNAPSHOT_prettyprint({
title: "SUCCESS",
message: snapshotName,
type: "success"
})
}
};
store_snapshot({
value,
name: snapshotName,
raiser,
});
};
function replaceCharacters(str, asFolder, sep) {
if (asFolder) {
if (!sep) throw new Error("Separator not Passed.")
return str
.replace(/ /gi, '-')
.replace(/\//gi, "-")
.replaceAll('"', '')
.replaceAll(sep, '/')
} else {
return str
.replaceAll(' ', '-')
.replaceAll('/', '-')
.replaceAll('"', '')
}
}
const get_snapshot_name = (asFolder, stepName) => {
const names = Cypress.currentTest.titlePath;
const sep = ">>datashard.work<<"
if (stepName && typeof stepName !== 'object') {
names.push(stepName)
}
if (asFolder) return replaceCharacters(names.join(sep), true, sep)
else return replaceCharacters(names.join('__'), false)
};
module.exports = (value, stepName, options = { json: true }) => {
if (typeof stepName === 'object') options = { ...options, ...stepName }
if (typeof value !== "object" || Array.isArray(value))
value = { data: value };
const serializer = pickSerializer(options.json, value);
const serialized = serializer(value);
let useFolders;
if(Cypress.config('snapshot').useSnapshotFolder === undefined || Cypress.config('snapshot').useSnapshotFolder === true) {
useFolders = true
} else {
useFolders = false
}
options.asFolder = Cypress.config('snapshot').useFolders || false
set_snapshot({
snapshotName: `/${useFolders ? "snapshots/" : ""}${get_snapshot_name(options.asFolder, stepName)}`,
serialized,
value,
});
};

61
src/utils.js Normal file
View file

@ -0,0 +1,61 @@
const sd = require('@wildpeaks/snapshot-dom')
const beautify = require('js-beautify').html
// converts DOM element to a JSON object
function serializeDomElement ($el) {
// console.log('snapshot value!', $el)
const json = sd.toJSON($el[0])
// console.log('as json', json)
// hmm, why is value not serialized?
if ($el.context.value && !json.attributes.value) {
json.attributes.value = $el.context.value
}
return deleteReactIdFromJson(json)
}
// remove React id, too transient
function deleteReactIdFromJson (json) {
if (json.attributes) {
delete json.attributes['data-reactid']
}
if (Array.isArray(json.childNodes)) {
json.childNodes.forEach(deleteReactIdFromJson)
}
return json
}
const stripReactIdAttributes = (html) => {
const dataReactId = /data\-reactid="[\.\d\$\-abcdfef]+"/g
return html.replace(dataReactId, '')
}
const serializeReactToHTML = (el$) => {
const html = el$[0].outerHTML
const stripped = stripReactIdAttributes(html)
const options = {
wrap_line_length: 80,
indent_inner_html: true,
indent_size: 2,
wrap_attributes: 'force'
}
const pretty = beautify(stripped, options)
return pretty
}
const identity = (x) => x
const publicProps = (name) => !name.startsWith('__')
const countSnapshots = (snapshots) =>
Object.keys(snapshots).filter(publicProps).length
module.exports = {
SNAPSHOT_FILE_NAME: 'snapshots.js',
serializeDomElement,
serializeReactToHTML,
identity,
countSnapshots
}

View file

@ -1,79 +0,0 @@
const containsDiffChars = str => ["╺", "┿", ""].some(emoji => str.includes(emoji));
const isNestedData = (expected, value) => expected && value && typeof expected == 'object' && typeof value == 'object';
const checkDataState = (expected, value) => {
let result;
if (expected === value) {
result = `| ✅ "${value}",`;
} else {
result = `| ⭕ "⭕╳ ${JSON.stringify(expected)?.replaceAll('"', "'")} | ${value?.replaceAll('"', "'")}",`;
}
return result;
};
const compare = (expected, value) => {
let compareResult = "";
if (isNestedData(expected, value)) {
if (Array.isArray(expected)) {
compareResult += `[`;
let dataX, dataY;
if (expected.length >= value.length) {
dataX = expected;
dataY = value;
} else {
dataX = value;
dataY = expected;
}
dataX.forEach(function (item, index) {
const resultset = compare(item, dataY[index]);
compareResult += resultset.result;
});
compareResult += `],`;
} else {
let dataX, dataY;
compareResult += `{`;
if (Object.keys(expected).length >= Object.keys(value).length) {
dataX = expected;
dataY = value;
} else {
dataX = value;
dataY = expected;
}
Object.keys(dataX).forEach((key) => {
const resultset = compare(dataX[key], dataY[key]);
compareResult += `"${key}": ${resultset.result}`;
});
compareResult += `},`;
}
} else {
compareResult = checkDataState(expected, value);
}
try {
return {
success: !containsDiffChars(compareResult),
result: compareResult,
};
} catch (error) {
return { success: false, result: compareResult };
}
};
/**
*
* @param {{
* expected: { [k:string]: any},
* value: {[k:string]:any}
* }} values
*/
module.exports = function compareValues(values) {
return compare(values.expected, values.value);
};

View file

@ -1,16 +0,0 @@
// remove React and Angular ids, which are transient
module.exports = function deleteTransientIdsFromJson(json) {
if (json.attributes) {
delete json.attributes["data-reactid"];
Object.keys(json.attributes)
.filter((key) => key.startsWith("_ng"))
.forEach((attr) => delete json.attributes[attr]);
delete json.attributes[""];
}
if (Array.isArray(json.childNodes)) {
json.childNodes.forEach(deleteTransientIdsFromJson);
}
return json;
};

View file

@ -1,16 +0,0 @@
const sd = require("@wildpeaks/snapshot-dom");
const deleteTransientIdsFromJson = require("./deleteTransientIdsFromJson");
// converts DOM element to a JSON object
module.exports = function serializeDomElement($el) {
// console.log('snapshot value!', $el)
const json = sd.toJSON($el[0]);
// console.log('as json', json)
// hmm, why is value not serialized?
if ($el.context.value && !json.attributes.value) {
json.attributes.value = $el.context.value;
}
return deleteTransientIdsFromJson(json);
};

View file

@ -1,15 +0,0 @@
const stripTransientIdAttributes = require("./stripTransientIdAttributes");
const beautify = require("js-beautify").html;
module.exports = (el$) => {
const html = el$[0].outerHTML;
const stripped = stripTransientIdAttributes(html);
const options = {
wrap_line_length: 80,
indent_inner_html: true,
indent_size: 2,
wrap_attributes: "force",
};
const pretty = beautify(stripped, options);
return pretty;
};

View file

@ -1,5 +0,0 @@
module.exports = (html) => {
const dataReactId = /data\-reactid="[\.\d\$\-abcdfef]+"/g;
const angularId = /_ng(content|host)\-[0-9a-z-]+(="")?/g;
return html.replace(dataReactId, "").replace(angularId, "");
};