├── .npmignore ├── assets └── cypress-diff.png ├── index.ts ├── .github ├── dependabot.yml └── workflows │ └── publish.yml ├── cypress.config.ts ├── cypress ├── support │ ├── e2e.ts │ └── commands.ts └── e2e │ └── todo.cy.js ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json ├── .gitignore └── on-fail-handler.ts /.npmignore: -------------------------------------------------------------------------------- 1 | cypress 2 | *.ts 3 | *.json -------------------------------------------------------------------------------- /assets/cypress-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elaichenkov/cypress-diff/HEAD/assets/cypress-diff.png -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { onFailHandler } from './on-fail-handler'; 2 | 3 | Cypress.on('fail', onFailHandler); 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) {}, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts 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 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | import '../../index'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "target": "ES2022", 5 | "module": "commonjs", 6 | "types": ["node", "cypress"], 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "exactOptionalPropertyTypes": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUncheckedIndexedAccess": true, 19 | "skipLibCheck": true, 20 | }, 21 | "include": ["./index.ts", "on-fail-handler.ts"] 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yevhen Laichenkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-diff 2 | 3 | [![npm](https://img.shields.io/npm/dt/cypress-diff)](https://www.npmjs.com/package/cypress-diff) 4 | 5 | ![cypress-diff](assets/cypress-diff.png) 6 | 7 | > Keep in mind that it's still in beta. Please, report any issues you find. Moreover, this library is designed to work with texts and objects. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm i -D cypress-diff 13 | ``` 14 | 15 | ## Usage 16 | 17 | Add the following line to your `cypress/support/e2e.js` file: 18 | 19 | ### JavaScript 20 | 21 | ```js 22 | // cypress/support/e2e.js 23 | require('cypress-diff'); 24 | ``` 25 | 26 | ### TypeScript 27 | 28 | ```ts 29 | // cypress/support/e2e.ts 30 | import 'cypress-diff'; 31 | ``` 32 | 33 | ## Configuration (optional) 34 | 35 | In case you are using a `Cypress.on('fail')` handler in your tests already then you can configure the plugin like this: 36 | 37 | ```js 38 | // cypress/support/e2e.js 39 | const { onFailHandler } = require('cypress-diff'); 40 | 41 | Cypress.on('fail', (error, runnable) => { 42 | // ... 43 | onFailHandler(error, runnable); 44 | // ... 45 | }); 46 | ``` 47 | 48 | ## License 49 | 50 | [MIT](LICENSE) 51 | 52 | ### Author 53 | 54 | Yevhen Laichenkov 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-diff", 3 | "version": "0.0.6", 4 | "description": "The library highlights the difference between the expected and actual values in the Cypress Test Runner UI.", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "npx cypress run", 9 | "release": "release-it --github.release", 10 | "release:ci": "npm run release -- --ci --npm.skipChecks --no-git.requireCleanWorkingDir", 11 | "release:patch": "npm run release -- patch", 12 | "release:minor": "npm run release -- minor", 13 | "release:major": "npm run release -- major" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/elaichenkov/cypress-diff.git" 18 | }, 19 | "keywords": [ 20 | "cypress", 21 | "testing", 22 | "diff", 23 | "reporter", 24 | "e2e" 25 | ], 26 | "author": "Yevhen Laichenkov ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/elaichenkov/cypress-diff/issues" 30 | }, 31 | "homepage": "https://github.com/elaichenkov/cypress-diff#readme", 32 | "devDependencies": { 33 | "@types/diff": "^7.0.0", 34 | "cypress": "^15.0.0", 35 | "release-it": "^19.0.3", 36 | "typescript": "^5.3.3" 37 | }, 38 | "dependencies": { 39 | "diff": "^8.0.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM registry 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseType: 7 | description: 'Release type - major, minor or patch' 8 | required: true 9 | default: 'patch' 10 | distTag: 11 | description: 'NPM tag (e.g. use "next" to release a test version)' 12 | required: true 13 | default: 'latest' 14 | 15 | env: 16 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 17 | 18 | jobs: 19 | release: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | ref: 'main' 25 | fetch-depth: 0 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 20.x 29 | - name: NPM Setup 30 | run: | 31 | npm set registry "https://registry.npmjs.org/" 32 | npm set //registry.npmjs.org/:_authToken $NPM_TOKEN 33 | npm whoami 34 | - name: Git Setup 35 | run: | 36 | git config --global user.email "elaichenkov@gmail.com" 37 | git config --global user.name "Yevhen Laichenkov" 38 | 39 | - name: Install Dependencies 40 | run: npm ci 41 | 42 | - name: Build Project 43 | run: npm run build 44 | env: 45 | NODE_ENV: production 46 | 47 | - name: Release 48 | run: npm run release:ci -- ${{github.event.inputs.releaseType}} --npm.tag=${{github.event.inputs.distTag}} 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | lib/ -------------------------------------------------------------------------------- /on-fail-handler.ts: -------------------------------------------------------------------------------- 1 | import * as diff from 'diff'; 2 | 3 | type CypressError = Cypress.CypressError & { actual: string; expected: string }; 4 | type Runner = Mocha.Runnable & { commands: { message: string }[] }; 5 | 6 | const color = { 7 | actual: '#cc3333', 8 | expected: '#008000', 9 | }; 10 | 11 | const formatValue = (value: unknown) => (typeof value === 'object' ? JSON.stringify(value, null, 2) : value); 12 | 13 | const insertCodeBlock = (value: string) => 14 | `
${value}
`; 15 | 16 | const formatDiff = (part: Diff.Change) => 17 | `${part.value}`; 20 | 21 | export function onFailHandler(error: CypressError, runnable: Runner) { 22 | if (!error.actual) throw error; 23 | if (!error.expected) throw error; 24 | if (typeof error.actual === 'number') throw error; 25 | // @ts-expect-error - TODO: improve types 26 | if (typeof error.actual === 'object' && error.actual.constructor.name === 'jQuery') throw error; 27 | if (error.message.includes(' to contain text ')) throw error; 28 | 29 | if (error.name === 'AssertionError') { 30 | window.top!.document.querySelectorAll('.command-info').forEach((element) => { 31 | const methodSpan = element.querySelector('.command-method > span'); 32 | const messageElement = element.querySelector('.command-message-text'); 33 | const errorMessage = (runnable.commands && runnable.commands[runnable.commands.length - 1]?.message) ?? ''; 34 | const unboldedErrorMessage = errorMessage.replaceAll('**', ''); 35 | const unboldedExpectedValueErrorMessage = errorMessage.replace(/\*\*([^*]+)\*\*/, '$1'); 36 | const includesErrorMessage = (message: string) => messageElement?.textContent?.includes(message); 37 | 38 | if (methodSpan && methodSpan.textContent === 'assert') { 39 | if ( 40 | includesErrorMessage(errorMessage) || 41 | includesErrorMessage(unboldedErrorMessage) || 42 | includesErrorMessage(unboldedExpectedValueErrorMessage) 43 | ) { 44 | if (messageElement) { 45 | methodSpan?.parentElement?.remove(); 46 | 47 | const actual = formatValue(error.actual) as string; 48 | const expected = formatValue(error.expected) as string; 49 | 50 | const diffResult = diff.diffChars(actual, expected); 51 | 52 | let actualFormatted = ''; 53 | let expectedFormatted = ''; 54 | 55 | diffResult.forEach((part: Diff.Change) => { 56 | const span = formatDiff(part); 57 | actualFormatted += part.removed || !part.added ? span : ''; 58 | expectedFormatted += part.added || !part.removed ? span : ''; 59 | }); 60 | 61 | messageElement.innerHTML = ` 62 |
Expected:${insertCodeBlock(expectedFormatted)}
63 |
Actual:${insertCodeBlock(actualFormatted)}
`; 64 | } 65 | } 66 | } 67 | }); 68 | } 69 | 70 | throw error; 71 | } 72 | -------------------------------------------------------------------------------- /cypress/e2e/todo.cy.js: -------------------------------------------------------------------------------- 1 | describe('example to-do app', () => { 2 | beforeEach(() => { 3 | cy.visit('https://example.cypress.io/todo'); 4 | }); 5 | 6 | it.only('displays two todo items by default', () => { 7 | // We use the `cy.get()` command to get all elements that match the selector. 8 | // Then, we use `should` to assert that there are two matched items, 9 | // which are the two default items. 10 | cy.get('.todo-list li').should('have.length', 2); 11 | 12 | // We can go even further and check that the default todos each contain 13 | // the correct text. We use the `first` and `last` functions 14 | // to get just the first and last matched elements individually, 15 | // and then perform an assertion with `should`. 16 | 17 | // cy.wrap('content').should('match', /column content/i); 18 | 19 | cy.visit('https://example.cypress.io/commands/actions'); 20 | // https://on.cypress.io/type 21 | cy.get('.action-email') 22 | .type('fake@email.com') 23 | .should('have.value', 'fake@email.com') 24 | 25 | // .type() with special character sequences 26 | .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') 27 | .type('{del}{selectall}{backspace}') 28 | 29 | // .type() with key modifiers 30 | .type('{alt}{option}') //these are equivalent 31 | .type('{ctrl}{control}') //these are equivalent 32 | .type('{meta}{command}{cmd}') //these are equivalent 33 | .type('{shift}') 34 | 35 | // Delay each keypress by 0.1 sec 36 | .type('slow.typing@email.com', { delay: 100 }) 37 | .should('have.value', 'slow.typing@exmail.com'); 38 | 39 | cy.wrap({ name: 'Ivan' }).should((str) => { 40 | expect(str).to.eq({ name: 'John' }); 41 | }); 42 | cy.get('.todo-list li').first().should('contain.text', 'hello'); 43 | cy.get('.todo-list li').last().should('have.text', 'Drive the dog'); 44 | }); 45 | 46 | it('can add new todo items', () => { 47 | // We'll store our item text in a variable so we can reuse it 48 | const newItem = 'Feed the cat'; 49 | 50 | // Let's get the input element and use the `type` command to 51 | // input our new list item. After typing the content of our item, 52 | // we need to type the enter key as well in order to submit the input. 53 | // This input has a data-test attribute so we'll use that to select the 54 | // element in accordance with best practices: 55 | // https://on.cypress.io/selecting-elements 56 | cy.get('[data-test=new-todo]').type(`${newItem}{enter}`); 57 | 58 | // Now that we've typed our new item, let's check that it actually was added to the list. 59 | // Since it's the newest item, it should exist as the last element in the list. 60 | // In addition, with the two default items, we should have a total of 3 elements in the list. 61 | // Since assertions yield the element that was asserted on, 62 | // we can chain both of these assertions together into a single statement. 63 | cy.get('.todo-list li') 64 | .should('have.length', 3) 65 | .last() 66 | .should('have.text', newItem + ' delete me'); 67 | }); 68 | 69 | it('can check off an item as completed', () => { 70 | // In addition to using the `get` command to get an element by selector, 71 | // we can also use the `contains` command to get an element by its contents. 72 | // However, this will yield the