mirror of
https://github.com/datashard/snapshot.git
synced 2024-12-21 09:27:35 +00:00
Merge branch 'master' of datashard:/datashard/snapshot
This commit is contained in:
commit
a953a3c9e6
35 changed files with 572 additions and 1167 deletions
BIN
.github/assets/Correct.png
vendored
Normal file
BIN
.github/assets/Correct.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 9 KiB |
BIN
.github/assets/Error.png
vendored
Normal file
BIN
.github/assets/Error.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
.github/assets/config.png
vendored
BIN
.github/assets/config.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 76 KiB |
|
@ -1,3 +1,5 @@
|
|||
cypress/
|
||||
cypress.config.js
|
||||
.github/workflows/
|
||||
.github/workflows/
|
||||
.vscode/
|
||||
renovate.json
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
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.
|
183
README.md
183
README.md
|
@ -1,55 +1,56 @@
|
|||
# @datashard/snapshot
|
||||
|
||||
> Adds value / object / DOM element snapshot testing support to Cypress test runner
|
||||
> Adds JSON Snapshot comparison to Cypress
|
||||
|
||||
<details>
|
||||
<summary>Changes between <code>@datashard/snapshot</code> and <code>@cypress/snapshot</code></summary>
|
||||
<br>
|
||||
They're mostly the same, as this is a fork of the Latter, though it's not a drop-in replacement.
|
||||
## ⚠️ Breaking Changes ⚠️
|
||||
|
||||
Unlike `@cypress/snapshot`, this saves snapshots in their own files with a sensible default and strives to have ongoing Support for future Cypress Versions
|
||||
- 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`
|
||||
|
||||
</details>
|
||||
> [!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*.
|
||||
|
||||
<!-- [![NPM][npm-icon] ][npm-url] -->
|
||||
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](https://nodejs.org/en/) version 10 or above.
|
||||
Requires Node 16 or above
|
||||
|
||||
```sh
|
||||
npm install --save-dev @datashard/snapshot
|
||||
npm i --save-dev @datashard/snapshot
|
||||
```
|
||||
|
||||
## Import
|
||||
|
||||
After installing, you need to add this snippet within your Cypress Support File (default: `cypress/support/e2e.{js,ts}`)
|
||||
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").register();
|
||||
require('@datashard/snapshot').regsiter()
|
||||
```
|
||||
|
||||
This registers a new command to create new snapshot or compare value to old snapshot
|
||||
This will register a new Command `.snapshot()`, to create and compare JSON Snapshots.
|
||||
|
||||
and add the following to your `cypress.config.{js,ts}`
|
||||
## 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
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
require("@datashard/snapshot").tasks(on, config)
|
||||
},
|
||||
```
|
||||
|
||||
> **Note** \
|
||||
> \
|
||||
> `@datashard/snapshot` **requires** the `readFileMaybe` plugin to be included, which can be easily done using the code above
|
||||
|
||||
# Usage
|
||||
|
||||
Currently, if you want to take more than one snapshot, you need to pass a Step Name to prevent overwrites / test failures
|
||||
|
||||
```js
|
||||
describe("my tests", () => {
|
||||
describe("my test", () => {
|
||||
it("works", () => {
|
||||
cy.log("first snapshot");
|
||||
cy.wrap({ foo: 42 }).snapshot("foo");
|
||||
|
@ -59,113 +60,33 @@ describe("my tests", () => {
|
|||
});
|
||||
```
|
||||
|
||||
In the above case, you can find the stored snapshot in their own files, mentioned above them
|
||||
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
|
||||
|
||||
```jsonc
|
||||
// cypress/snapshots/my-tests-works-foo.json
|
||||
{ "foo": 42 }
|
||||
// cypress/snapshots/my-tests-works-bar.json
|
||||
{ "bar": 101 }
|
||||
```
|
||||
|
||||
If you change the site values, the saved snapshot will no longer match, throwing an error
|
||||
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.
|
||||
|
||||
(picture taken from `cypress/snapshots/Arrays.json`)
|
||||
![Snapshot mismatch](.github/assets/updated-mismatch.png)
|
||||
Passing a name to the Snapshot function is required, but not checked, if you want to take multiple snapshots in the same block.
|
||||
|
||||
Click on the `SNAPSHOT` step in the Command Log to see expected and current value printed in the DevTools.
|
||||
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.
|
||||
|
||||
### Options
|
||||
Which will look like this:
|
||||
|
||||
You can control snapshot comparison and behavior through a few options.
|
||||
![](./.github/assets/Error.png)
|
||||
|
||||
```js
|
||||
cy.get(...).snapshot({
|
||||
snapshotName: 'Snapshot Name', // Overwrite the generated Snapshot name
|
||||
snapshotPath: 'cypress/not_snapshots', // Overwrite where the Snapshot should be stored
|
||||
json: false // convert DOM elements into JSON
|
||||
}) // when storing in the snapshot file
|
||||
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
|
||||
|
||||
// will save as
|
||||
// cypress/not_snapshots/Snapshot-Name.json
|
||||
```
|
||||
|
||||
You can also pass a "Step Name" to the Function
|
||||
|
||||
```js
|
||||
cy.get(...).snapshot("Intercepted API Request")
|
||||
// will save as
|
||||
// cypress/snapshots/<context>-<describe>-<it>-Intercepted-API-Request.json
|
||||
// to prevent duplications
|
||||
```
|
||||
|
||||
or both
|
||||
|
||||
```js
|
||||
cy.get(...).snapshot("Intercepted API Request", {
|
||||
snapshotPath: "cypress/snapshots/api",
|
||||
snapshotName: "first_intercept"
|
||||
})
|
||||
|
||||
// will save as
|
||||
// cypress/snapshots/api/first_intercept.json
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
This module provides some configuration options:
|
||||
|
||||
#### snapshot.snapshotPath
|
||||
|
||||
Sets the default Path for saving Snapshots (default: `cypress/snapshots`)
|
||||
|
||||
![Config Screenshot](./.github/assets/config.png)
|
||||
|
||||
#### `ENV` CYPRESS_UPDATE_SNAPSHOTS
|
||||
|
||||
Lets you pass a Env Variable to update failing Tests with the new Data
|
||||
|
||||
#
|
||||
|
||||
### Small print
|
||||
|
||||
Authors:
|
||||
|
||||
- Joshua <[data@shard.wtf](mailto:data@shard.wtf)>
|
||||
- Gleb Bahmutov <gleb@cypress.io>
|
||||
|
||||
<br>
|
||||
License: MIT - do anything with the code, but don't blame us if it does not work.
|
||||
|
||||
Support: If you find any problems with this module [open an issue](https://github.com/datashard/snapshot/issues) on Github
|
||||
|
||||
## MIT License
|
||||
|
||||
Copyright (c) 2017-2022 Cypress.io <hello@cypress.io> & 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.
|
||||
|
||||
[npm-icon]: https://nodei.co/npm/@datashard/snapshot.svg?downloads=true
|
||||
[npm-url]: https://npmjs.org/package/@datashard/snapshot
|
||||
[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/
|
||||
![](./.github/assets/Correct.png)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
const { defineConfig } = require("cypress");
|
||||
const { functions } = require("./src/utils");
|
||||
|
||||
module.exports = defineConfig({
|
||||
// fixturesFolder: "cypress/fixtures",
|
||||
snapshot: {
|
||||
snapshotPath: "cypress/snapshots/",
|
||||
// updateSnapshots: true,
|
||||
useFolders: true,
|
||||
// useSnapshotFolder: true
|
||||
},
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
functions.tasks(on, config);
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
|
@ -1,47 +1,78 @@
|
|||
/* eslint-env mocha */
|
||||
/* global cy */
|
||||
describe("@datashard/snapshot", () => {
|
||||
context("simple types", () => {
|
||||
it("works with objects", () => {
|
||||
cy.fixture("File2").snapshot({
|
||||
snapshotPath: "cypress/snapshots",
|
||||
snapshotName: "Objects",
|
||||
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/snapshots",
|
||||
snapshotName: "Numbers",
|
||||
});
|
||||
});
|
||||
|
||||
it("works with strings", () => {
|
||||
cy.wrap("foo-bar").snapshot({
|
||||
snapshotPath: "cypress/snapshots",
|
||||
snapshotName: "Strings",
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"works with arrays",
|
||||
{
|
||||
env: {
|
||||
SNAPSHOT_UPDATE: true,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
cy.wrap([1, 2, 3, 4]).snapshot({
|
||||
snapshotPath: "cypress/snapshots",
|
||||
snapshotName: "Arrays",
|
||||
it("works with numbers", () => {
|
||||
cy.wrap(42).snapshot({
|
||||
snapshotPath: "cypress/fixtures/snapshots",
|
||||
snapshotName: "Numbers",
|
||||
});
|
||||
}
|
||||
);
|
||||
it('works with more "complicated" Objects', () => {
|
||||
cy.fixture("Complex").snapshot({
|
||||
snapshotPath: 'cypress/snapshots',
|
||||
snapshotName: "Complex"
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,25 +3,21 @@
|
|||
describe("Random Describe", () => {
|
||||
context("Random Context", () => {
|
||||
it("Random It", () => {
|
||||
cy.fixture("File").snapshot("Fixture File", {
|
||||
snapshotName: "Random Fixture File"
|
||||
});
|
||||
// cy.fixture("File2").snapshot("Fixture File",
|
||||
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))
|
||||
// cy.wrap(42).snapshot();
|
||||
// });
|
||||
|
||||
// it("works with strings", () => {
|
||||
// console.log(cy.wrap("foo-bar"))
|
||||
// cy.wrap("foo-bar").snapshot();
|
||||
// });
|
||||
|
||||
// it("works with arrays", () => {
|
||||
// console.log(cy.wrap([1, 2, 3]))
|
||||
// cy.wrap([1, 2, 3]).snapshot();
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"foo": "bar",
|
||||
"Fizzy Drink": "Soda"
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"foo": "bar",
|
||||
"Fizzy Drink": "Pop"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"data": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"jsonapi": {
|
||||
"version": "2.0"
|
||||
},
|
||||
"included": [
|
||||
{
|
||||
"type": "users",
|
||||
"id": "2",
|
||||
"attributes": {
|
||||
"name": "Test"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -5,10 +5,11 @@
|
|||
0,
|
||||
1,
|
||||
2,
|
||||
"Three"
|
||||
"4"
|
||||
],
|
||||
"object": {
|
||||
"with": "more details"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thisisnew": "wtf"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"data": 42
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"data": "foo-bar"
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"status": 200,
|
||||
"response": {
|
||||
"array": [0, 1, 2, "Three"],
|
||||
"object": {
|
||||
"with": "more details"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"foo":"bar","Fizzy Drink":"Soda"}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"foo": "bar",
|
||||
"Fizzy Drink": "Soda"
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
// register .snapshot() command
|
||||
require('../..').register()
|
||||
|
||||
|
||||
require('../../src/index').register()
|
||||
|
|
726
package-lock.json
generated
726
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,13 +13,13 @@
|
|||
"src/*/**",
|
||||
"!src/*-spec.js"
|
||||
],
|
||||
"homepage": "https://github.com/datashard/snapshot#readme",
|
||||
"homepage": "https://shard.wtf/snapshot",
|
||||
"keywords": [
|
||||
"cypress",
|
||||
"cypress-io",
|
||||
"plugin",
|
||||
"snapshot",
|
||||
"testing"
|
||||
"testing", "json"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
|
@ -33,11 +33,11 @@
|
|||
"unused-deps": "dependency-check --unused --no-dev . --entry src/index.js",
|
||||
"semantic-release": "semantic-release",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:update": "cypress run --env SNAPSHOT_UPDATE=true",
|
||||
"cypress:update": "cypress run --env updateSnapshots=true",
|
||||
"cypress:run": "cypress run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "10.6.0",
|
||||
"cypress": "12.13.0",
|
||||
"debug": "3.2.7",
|
||||
"dependency-check": "2.10.1"
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// global cy, Cypress
|
||||
const { functions } = require("./utils/index");
|
||||
const register = require("./register");
|
||||
|
||||
module.exports = {
|
||||
register: functions.register,
|
||||
tasks: functions.tasks,
|
||||
register
|
||||
};
|
||||
|
|
119
src/register.js
Normal file
119
src/register.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
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)
|
||||
|
||||
};
|
124
src/snapshot.js
Normal file
124
src/snapshot.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
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(' ', " ")}` : ""}`
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
|
@ -1,115 +1,79 @@
|
|||
// const { expect: chaiExpect } = require("chai");
|
||||
|
||||
const isNestedData = (expected, value) => {
|
||||
return (
|
||||
expected && value && typeof expected == `object` && typeof value == `object`
|
||||
);
|
||||
};
|
||||
|
||||
const checkDataState = (expected, value) => {
|
||||
let result;
|
||||
|
||||
if (expected === value) {
|
||||
result = `| ✅ "${value}",`;
|
||||
} else if (expected == undefined) {
|
||||
result = `| ➖ "➖╺ ${expected}|${value}",`;
|
||||
} else if (value == undefined) {
|
||||
result = `| ➕ "➕┿ ${expected}|${value}",`;
|
||||
} else {
|
||||
result = `| ⭕ "⭕╳ ${expected}|${value}",`;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
function parseTextToJSON(text) {
|
||||
const lines =
|
||||
text
|
||||
.replace(/\| [✅➖➕⭕]/g, "").trim()
|
||||
.replace(/(.*?),\s*(\}|])/g, "$1$2");
|
||||
return lines;
|
||||
// return JSON.stringify(lines, null, 2);
|
||||
}
|
||||
|
||||
function containsDiffChars(str) {
|
||||
const emojis = ["╺", "┿", "╳"];
|
||||
return emojis.some(emoji => str.includes(emoji));
|
||||
}
|
||||
|
||||
const compare = (expected, value) => {
|
||||
let compareResult = "";
|
||||
let compareSuccess = true;
|
||||
|
||||
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]);
|
||||
compareSuccess = resultset.success;
|
||||
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]);
|
||||
|
||||
compareSuccess = resultset.success;
|
||||
compareResult += `"${key}": ${resultset.result}`;
|
||||
});
|
||||
compareResult += `}`;
|
||||
}
|
||||
} else {
|
||||
compareSuccess = false;
|
||||
compareResult = checkDataState(expected, value);
|
||||
}
|
||||
let result = parseTextToJSON(compareResult);
|
||||
// let result = compareResult;
|
||||
|
||||
try {
|
||||
return {
|
||||
success: !containsDiffChars(result),
|
||||
result,
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, result };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* expected: { [k:string]: any},
|
||||
* value: {[k:string]:any}
|
||||
* }} values
|
||||
*/
|
||||
|
||||
module.exports = function compareValues(values) {
|
||||
return compare(values.expected, values.value);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
const readFileMaybe = require("../tasks/readFileMaybe");
|
||||
|
||||
module.exports = (on, config) => {
|
||||
on("task", { readFileMaybe });
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
const lazy = require("lazy-ass");
|
||||
const is = require("check-more-types");
|
||||
const snapshot = require("../snapshots/snapshot");
|
||||
|
||||
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: true }, snapshot);
|
||||
|
||||
return snapshot;
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
const serializeToHTML = require("./serializers/serializeToHTML");
|
||||
const serializeDomElement = require("./serializers/serializeDomElement");
|
||||
const compareValues = require("./snapshots/compareValues");
|
||||
const readFileMaybe = require("./tasks/readFileMaybe");
|
||||
const identity = (x) => x;
|
||||
const publicProps = (name) => !name.startsWith("__");
|
||||
const countSnapshots = (snapshots) =>
|
||||
Object.keys(snapshots).filter(publicProps).length;
|
||||
|
||||
module.exports = {
|
||||
serializers: {
|
||||
serializeDomElement,
|
||||
serializeToHTML,
|
||||
identity,
|
||||
countSnapshots,
|
||||
},
|
||||
snapshots: {
|
||||
compareValues,
|
||||
},
|
||||
|
||||
functions: {
|
||||
register: require("./functions/register"),
|
||||
tasks: require("./functions/addTasks"),
|
||||
},
|
||||
tasks: {
|
||||
readFileMaybe,
|
||||
},
|
||||
};
|
|
@ -1,111 +0,0 @@
|
|||
const serializeDomElement = require("../serializers/serializeDomElement");
|
||||
const serializeToHTML = require("../serializers/serializeToHTML");
|
||||
const compareValues = require("./compareValues");
|
||||
const path = require("path");
|
||||
const identity = (x) => x;
|
||||
|
||||
const pickSerializer = (asJson, value) => {
|
||||
if (Cypress.dom.isJquery(value)) {
|
||||
return asJson ? serializeDomElement : serializeToHTML;
|
||||
}
|
||||
return identity;
|
||||
};
|
||||
|
||||
const store_snapshot = (props = { value, name, path, raiser }) => {
|
||||
const expectedPath = path.join(
|
||||
props.path ||
|
||||
Cypress.config("snapshot").snapshotPath ||
|
||||
"cypress/snapshots",
|
||||
`${props.name.join("_").replace(/ /gi, "-").replace(/\//gi, "-")}.json`
|
||||
);
|
||||
cy.task("readFileMaybe", expectedPath).then((exist) => {
|
||||
if (exist && !Cypress.env().SNAPSHOT_UPDATE) {
|
||||
props.raiser({ value: props.value, expected: JSON.parse(exist) });
|
||||
} else {
|
||||
cy.writeFile(expectedPath, JSON.stringify(props.value, null, 2));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const set_snapshot = (
|
||||
{ snapshotName, snapshotPath, serialized, value }
|
||||
) => {
|
||||
let devToolsLog = { $el: serialized };
|
||||
|
||||
if (Cypress.dom.isJquery(value)) {
|
||||
devToolsLog.$el = value;
|
||||
}
|
||||
|
||||
const options = {
|
||||
name: "snapshot",
|
||||
message: Cypress._.last(snapshotName),
|
||||
consoleProps: () => devToolsLog,
|
||||
};
|
||||
|
||||
if (value) options.$el = value;
|
||||
|
||||
const raiser = ({ value, expected }) => {
|
||||
const result = compareValues({ expected, value });
|
||||
|
||||
if (!Cypress.env().SNAPSHOT_UPDATE && !result.success) {
|
||||
devToolsLog = {
|
||||
...devToolsLog,
|
||||
message: result,
|
||||
expected,
|
||||
value,
|
||||
};
|
||||
|
||||
// ╺
|
||||
// ┿
|
||||
// ╳
|
||||
|
||||
throw new Error(
|
||||
`Snapshot Difference found.\nPlease Update the Snapshot\n\n${JSON.stringify(
|
||||
JSON.parse(result.result),
|
||||
null,
|
||||
2
|
||||
)
|
||||
.replaceAll(" ", " ")
|
||||
.replaceAll(/[╺┿╳]/g, "")}`
|
||||
// `Snapshot Difference found.\nPlease Update the Snapshot\n\n${result.result}`
|
||||
);
|
||||
}
|
||||
};
|
||||
Cypress.log(options);
|
||||
|
||||
store_snapshot({
|
||||
value,
|
||||
name: snapshotName,
|
||||
path: snapshotPath,
|
||||
raiser,
|
||||
});
|
||||
};
|
||||
|
||||
const get_snapshot_name = (test, custom_name) => {
|
||||
const names = test.titlePath;
|
||||
|
||||
const index = custom_name;
|
||||
names.push(String(index));
|
||||
|
||||
if (custom_name) return [custom_name];
|
||||
return names;
|
||||
};
|
||||
|
||||
module.exports = (value, step, options) => {
|
||||
if (typeof step === "object") options = step;
|
||||
if (typeof value !== "object" || Array.isArray(value))
|
||||
value = { data: value };
|
||||
|
||||
const serializer = pickSerializer(options.json, value);
|
||||
const serialized = serializer(value);
|
||||
|
||||
set_snapshot({
|
||||
snapshotName: get_snapshot_name(
|
||||
Cypress.currentTest,
|
||||
options.snapshotName || step
|
||||
),
|
||||
snapshotPath: options.snapshotPath,
|
||||
serialized,
|
||||
value,
|
||||
});
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
const fs = require("fs");
|
||||
|
||||
module.exports = (filename) => {
|
||||
if (fs.existsSync(filename)) {
|
||||
return fs.readFileSync(filename, "utf8");
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
Loading…
Reference in a new issue