Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup and introduce visual regression tests #13035

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e51f9d9
Setup configuration for visual tests
cnotv Dec 16, 2024
547a5d9
Add Cypress plugin and configure visual tests; add example test
cnotv Dec 16, 2024
a0f15b5
Add test example
cnotv Dec 17, 2024
d9c3ffd
Update commands
cnotv Dec 17, 2024
9e2e046
Add visual tests to CI
cnotv Dec 17, 2024
398c431
Remove unused command
cnotv Dec 18, 2024
d696c3f
Allow use of JSON for Cypress
cnotv Dec 18, 2024
f683558
Create loop for Storybook stories to be tested, allowing filtering
cnotv Dec 18, 2024
0f50698
Update CI commands to install and build storybook as well as concurre…
cnotv Dec 18, 2024
e84ee59
Merge visual tests and update as single command to simplify process a…
cnotv Dec 18, 2024
947a078
Rename automated test for convenience
cnotv Dec 18, 2024
632ebef
Add all the snapshots
cnotv Dec 18, 2024
93c2301
Update everything to new forked library
cnotv Dec 18, 2024
8f3141a
Exclude diff output screenshots
cnotv Jan 6, 2025
8ed81ff
Disable code coverage
cnotv Jan 6, 2025
7463630
Remove screenshots
cnotv Jan 6, 2025
b085197
Update visual config with screenshot path, custom support file, timeo…
cnotv Jan 6, 2025
38f4722
Generate screenshots
cnotv Jan 6, 2025
4c8671a
Move the whole visual configuration to the root to avoid confusion
cnotv Jan 6, 2025
b8c485b
Exclude Tables due rendering issue; Correct logic not excluding all t…
cnotv Jan 6, 2025
5e5962c
Correct gitignore path for error diffs
cnotv Jan 6, 2025
70b4097
Define viewport to avoid issues with CI
cnotv Jan 6, 2025
38ac05f
Remove unnecessary code from the test
cnotv Jan 6, 2025
7c4c5a6
Update snapshot viewport to 1080x720
cnotv Jan 6, 2025
61f93ed
Compare screenshots in vertical
cnotv Jan 7, 2025
da5048e
Set viewport size to default ones and fixed
cnotv Jan 9, 2025
c8819b0
Copy library files and customize logic
cnotv Jan 9, 2025
882a4c0
Kill terminal with success if all tests pass
cnotv Jan 9, 2025
cbefa11
Increasing timeout for CI
cnotv Jan 9, 2025
a43f66f
Fix linting issues
cnotv Jan 9, 2025
b201053
Remove unnecessary leftover changes from E2E configuration
cnotv Jan 9, 2025
626a294
Move Cypress config for visual within folder as possible cause of E2E…
cnotv Jan 13, 2025
91a2c2d
Correct image path after moving config to ./visual
cnotv Jan 15, 2025
00d8b25
Generate new yarn.lock plus fixesd
cnotv Jan 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Tests
on:
push:
push:
branches:
- master
- 'release-*'
Expand Down Expand Up @@ -259,3 +259,21 @@ jobs:
- name: Check e2e tags
run: |
./scripts/check-e2e-tests-for-tags

visual-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Setup env
uses: ./.github/actions/setup

- name: Run tests
shell: bash
run: |
yarn build-storybook
yarn add -D -W concurrently
yarn visual:ci

2 changes: 1 addition & 1 deletion cypress/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
downloads
downloads
2 changes: 1 addition & 1 deletion cypress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
"./**/*.ts",
"../types/*.ts"
]
}
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"lint-l10n": "./node_modules/.bin/yamllint ./shell/assets/translations",
"test": "NODE_OPTIONS=--max_old_space_size=8192 jest --watch",
"test:ci": "NODE_OPTIONS=--max_old_space_size=8192 jest --collectCoverage --silent",
"visual": "cypress run --config-file visual/cypress.visual.config.ts",
"visual:open": "cypress open --env requireSnapshots=true --config-file visual/cypress.visual.config.ts",
"visual:ci": "npx concurrently -k -s first \"yarn storybook --quiet --ci \" \"yarn visual \"",
"install:ci": "yarn install --frozen-lockfile",
"dev": "bash -c 'source ./scripts/version && NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve'",
"mem-dev": "bash -c 'source ./scripts/version && NODE_ENV=dev node --max-old-space-size=8192 ./node_modules/.bin/vue-cli-service serve'",
Expand All @@ -48,7 +51,6 @@
"coverage": "npx nyc merge coverage coverage/coverage.json",
"storybook": "cd storybook && yarn storybook",
"build-storybook": "cd storybook && yarn install --no-lockfile && NODE_OPTIONS=--max_old_space_size=4096 yarn build-storybook --quiet",
"storybook-test": "cd storybook && yarn test-storybook --stories-json ",
"docs:install": "cd docusaurus/ && yarn install",
"docs:build": "cd docusaurus/ && yarn build",
"docs:start": "cd docusaurus/ && yarn start",
Expand Down Expand Up @@ -144,6 +146,7 @@
"@cypress/grep": "3.1.5",
"@cypress/vue": "5.0.5",
"@cypress/webpack-dev-server": "3.4.1",
"@emerson-eps/cypress-image-snapshot": "^1.5.3",
"@types/copy-webpack-plugin": "5.0.3",
"@types/dompurify": "3.0.0",
"@types/is-url": "1.2.30",
Expand Down Expand Up @@ -172,6 +175,7 @@
"cypress-delete-downloads-folder": "0.0.4",
"cypress-mochawesome-reporter": "^3.8.2",
"cypress": "11.1.0",
"cypress-plugin-snapshots": "^1.4.4",
"eslint-config-standard": "16.0.3",
"eslint-import-resolver-node": "0.3.4",
"eslint-module-utils": "2.6.1",
Expand Down
2 changes: 2 additions & 0 deletions visual/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
downloads
snapshots/**/__diff_output__/
182 changes: 182 additions & 0 deletions visual/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import path from 'path';
import extend from 'just-extend';
import { CHECKSNAP, MATCH, RECORD } from './constants';
import type {
CypressImageSnapshotOptions,
DiffSnapshotResult,
SnapshotOptions,
Subject,
} from './types';

const COMMAND_NAME = 'cypress-image-snapshot';
const screenshotsFolder = Cypress.config('screenshotsFolder') || 'cypress/screenshots';
const isUpdateSnapshots: boolean = Cypress.env('updateSnapshots') || false;
const isSnapshotDebug: boolean = Cypress.env('debugSnapshots') || false;
const newImageMessage = 'A new reference Image was created for';

let currentTest: string;
let errorMessages: { [key: string]: string } = {};

export const defaultOptions: SnapshotOptions = {
screenshotsFolder,
isUpdateSnapshots,
isSnapshotDebug,
specFileName: Cypress.spec.name,
specRelativeFolder: Cypress.spec.relative,
extraFolders: '',
currentTestTitle: '',
failureThreshold: 0,
failureThresholdType: 'pixel',
timeout: 5000,
delayBetweenTries: 1000,
};

const matchImageSnapshot = (defaultOptionsOverrides: CypressImageSnapshotOptions) => (
subject: Subject,
nameOrCommandOptions: CypressImageSnapshotOptions | string,
commandOptions?: CypressImageSnapshotOptions,
) => {
const { filename, options } = getNameAndOptions(
nameOrCommandOptions,
defaultOptionsOverrides,
commandOptions,
);

if (!currentTest) {
currentTest = Cypress.currentTest.title;
} else if (
currentTest !== Cypress.currentTest.title
) {
// we need to ensure the errors messages about new references being created are kept
if (currentTest === Cypress.currentTest.title) {
errorMessages = Object.fromEntries(
Object.entries(errorMessages).filter(([, value]) => value.includes(newImageMessage)),
);
} else {
errorMessages = {};
}
currentTest = Cypress.currentTest.title;
}

recursiveSnapshot(subject, options, filename);
cy.wrap(errorMessages).as('matchImageSnapshot');
};

/**
* Add this function to your `supportFile` for e2e/component
* Accepts options that are used for all instances of `toMatchSnapshot`
*/
export const addMatchImageSnapshotCommand = (defaultOptionsOverrides: CypressImageSnapshotOptions = {}) => {
Cypress.Commands.add(
'matchImageSnapshot',
{ prevSubject: ['optional', 'element', 'document', 'window'] },
matchImageSnapshot(defaultOptionsOverrides),
);
};

const recursiveSnapshot = (subject: Subject, options: SnapshotOptions, filename: string): void => {
const screenshotName = getScreenshotFilename(filename);

cy.task(MATCH, {
...options,
currentTestTitle: Cypress.currentTest.title,
});

cy.task<boolean>(CHECKSNAP, { screenshotName, options }).then((exist) => {
if (!exist) {
// base image does not exist yes
// so make sure we have a valid image by waiting the maximum timeout
cy.wait(options.timeout);
}
cy.wrap(subject).screenshot(screenshotName, options);
cy.task<DiffSnapshotResult>(RECORD).then(({
added,
pass,
updated,
imageDimensions,
diffPixelCount,
diffRatio,
diffSize,
diffOutputPath,
}) => {
if (pass) return;

if (added) {
const message = `New snapshot: '${ screenshotName }' was added`;

Cypress.log({ name: COMMAND_NAME, message });
// An after each hook should check if @matchImageSnapshot is defined, if yes it should fail the tests
errorMessages[screenshotName] = `${ newImageMessage } ${ screenshotName }`;

return;
}

if (!pass && !added && !updated) {
const message = diffSize ? `Image size (${ imageDimensions.baselineWidth }x${ imageDimensions.baselineHeight }) different than saved snapshot size (${ imageDimensions.receivedWidth }x${ imageDimensions.receivedHeight }).\nSee diff for details: ${ diffOutputPath }` : `Image was ${ diffRatio * 100 }% different from saved snapshot with ${ diffPixelCount } different pixels.\nSee diff for details: ${ diffOutputPath }`;

// An after each hook should check if @matchImageSnapshot is defined, if yes it should fail the tests
errorMessages[screenshotName] = message;

Cypress.log({ name: COMMAND_NAME, message });
}
});
});
};

const getNameAndOptions = (
nameOrCommandOptions: CypressImageSnapshotOptions | string,
defaultOptionsOverrides: CypressImageSnapshotOptions,
commandOptions?: CypressImageSnapshotOptions,
) => {
let filename: string | undefined;
let options = extend(
true,
{},
defaultOptions,
defaultOptionsOverrides,
) as SnapshotOptions;

if (typeof nameOrCommandOptions === 'string' && commandOptions) {
filename = nameOrCommandOptions;
options = extend(
true,
{},
defaultOptions,
defaultOptionsOverrides,
commandOptions,
) as SnapshotOptions;
}
if (typeof nameOrCommandOptions === 'string') {
filename = nameOrCommandOptions;
}
if (typeof nameOrCommandOptions === 'object') {
options = extend(
true,
{},
defaultOptions,
defaultOptionsOverrides,
nameOrCommandOptions,
) as SnapshotOptions;
}

return {
filename,
options,
};
};

const getScreenshotFilename = (filename: string | undefined) => {
if (filename) {
return filename;
}

return Cypress.currentTest.titlePath.join(' -- ');
};

/**
* replaces forward slashes (/) and backslashes (\) in a given input string with the appropriate path separator based on the operating system
* @param input string to replace
*/
export const replaceSlashes = (input: string): string => {
return input.replace(/\\/g, path.sep).replace(/\//g, path.sep);
};
3 changes: 3 additions & 0 deletions visual/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const MATCH = 'Matching image snapshot';
export const RECORD = 'Recording snapshot result';
export const CHECKSNAP = 'Check if base Image already exits';
50 changes: 50 additions & 0 deletions visual/cypress.visual.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable no-console */
import { defineConfig } from 'cypress';
import { addMatchImageSnapshotPlugin } from './plugin';

// Required for env vars to be available in cypress
require('dotenv').config();

// const baseUrl = (process.env.TEST_BASE_URL || 'https://localhost:8005').replace(/\/$/, '');
const baseUrl = 'http://localhost:6006/'; // Storybook URL

/**
* CONFIGURATION
*/
export default defineConfig({
trashAssetsBeforeRuns: true,
chromeWebSecurity: false,
video: false,
screenshotOnRunFailure: false,
// Define viewport to avoid issues with CI
viewportWidth: 1000,
viewportHeight: 660,
retries: 0,
env: {
baseUrl,
coverage: false
},

// https://docs.cypress.io/app/references/configuration#component
component: {
devServer: {
bundler: 'webpack',
framework: 'vue-cli',
webpackConfig: require('@vue/cli-service/webpack.config.js')
},
specPattern: './visual/*.visual.{js,jsx,ts,tsx}',
supportFile: false
},

// https://docs.cypress.io/app/references/configuration#Testing-Type-Specific-Options
e2e: {
experimentalSessionAndOrigin: true,
supportFile: './visual/support.ts',
specPattern: './visual/tests/*.visual.{js,jsx,ts,tsx}',
baseUrl,
screenshotOnRunFailure: false,
setupNodeEvents(on, config) {
addMatchImageSnapshotPlugin(on);
},
},
});
Loading
Loading