comparadise-utils 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Creates a file path that is relative to the root
3
+ * @param {string} type - Value to append to fileName
4
+ * @param {string} path - (optional) FileName which corresponds to a module Id
5
+ * returns a string of just the prefix or the fileName prepended by the prefix
6
+ */
7
+ export declare function createImageFileName(path: string, type: string): string;
package/dist/files.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createImageFileName = void 0;
4
+ /**
5
+ * Creates a file path that is relative to the root
6
+ * @param {string} type - Value to append to fileName
7
+ * @param {string} path - (optional) FileName which corresponds to a module Id
8
+ * returns a string of just the prefix or the fileName prepended by the prefix
9
+ */
10
+ function createImageFileName(path, type) {
11
+ let newPath = path;
12
+ if (path.startsWith('/')) {
13
+ newPath = `.${path}`;
14
+ }
15
+ return `${newPath}/${type}.png`;
16
+ }
17
+ exports.createImageFileName = createImageFileName;
package/dist/images.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { PNG } from 'pngjs';
2
+ /**
3
+ * Compares a base and new image and returns the pixel difference
4
+ * and diff PNG for writing the diff image to.
5
+ * @param {string} basePath - Full file path to base image
6
+ * @param {string} actualPath - Full file path to new image
7
+ */
2
8
  export declare function getDiffPixels(basePath: string, actualPath: string): {
3
- diffPixels: number;
9
+ diffPixels: any;
4
10
  diff: PNG;
5
11
  };
package/dist/images.js CHANGED
@@ -22,28 +22,33 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
- var __importDefault = (this && this.__importDefault) || function (mod) {
26
- return (mod && mod.__esModule) ? mod : { "default": mod };
27
- };
28
25
  Object.defineProperty(exports, "__esModule", { value: true });
29
26
  exports.getDiffPixels = void 0;
30
27
  const fs = __importStar(require("fs"));
31
28
  const pngjs_1 = require("pngjs");
32
- const pixelmatch_1 = __importDefault(require("pixelmatch"));
29
+ const pixelmatch = require('pixelmatch');
33
30
  const PIXELMATCH_OPTIONS = {
34
31
  threshold: 0.3
35
32
  };
33
+ /**
34
+ * Helper function to create reusable image resizer
35
+ */
36
36
  const createImageResizer = (width, height) => (source) => {
37
37
  const resized = new pngjs_1.PNG({ width, height, fill: true });
38
38
  pngjs_1.PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
39
39
  return resized;
40
40
  };
41
+ /**
42
+ * Fills new area added after resize with transparent black color.
43
+ * I like idea of checker board pattern, but it seems to be too complicated
44
+ * to implement considering how low-level pngjs API is.
45
+ */
41
46
  const fillSizeDifference = (width, height) => (image) => {
42
47
  const inArea = (x, y) => y > height || x > width;
43
48
  for (let y = 0; y < image.height; y++) {
44
49
  for (let x = 0; x < image.width; x++) {
45
50
  if (inArea(x, y)) {
46
- const idx = (image.width * y + x) << 2;
51
+ const idx = ((image.width * y) + x) << 2;
47
52
  image.data[idx] = 0;
48
53
  image.data[idx + 1] = 0;
49
54
  image.data[idx + 2] = 0;
@@ -53,6 +58,10 @@ const fillSizeDifference = (width, height) => (image) => {
53
58
  }
54
59
  return image;
55
60
  };
61
+ /**
62
+ * Aligns images sizes to biggest common value
63
+ * and fills new pixels with transparent pixels
64
+ */
56
65
  function alignImagesToSameSize(firstImage, secondImage) {
57
66
  // Keep original sizes to fill extended area later
58
67
  const firstImageWidth = firstImage.width;
@@ -67,16 +76,25 @@ function alignImagesToSameSize(firstImage, secondImage) {
67
76
  // Fill resized area with black transparent pixels
68
77
  return [
69
78
  fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
70
- fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond)
79
+ fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond),
71
80
  ];
72
81
  }
82
+ /**
83
+ * Compares a base and new image and returns the pixel difference
84
+ * and diff PNG for writing the diff image to.
85
+ * @param {string} basePath - Full file path to base image
86
+ * @param {string} actualPath - Full file path to new image
87
+ */
73
88
  function getDiffPixels(basePath, actualPath) {
74
89
  const rawBase = pngjs_1.PNG.sync.read(fs.readFileSync(basePath));
75
90
  const rawActual = pngjs_1.PNG.sync.read(fs.readFileSync(actualPath));
76
- const hasSizeMismatch = rawBase.height !== rawActual.height || rawBase.width !== rawActual.width;
77
- const [base, actual] = hasSizeMismatch ? alignImagesToSameSize(rawBase, rawActual) : [rawBase, rawActual];
91
+ const hasSizeMismatch = (rawBase.height !== rawActual.height
92
+ || rawBase.width !== rawActual.width);
93
+ const [base, actual] = hasSizeMismatch
94
+ ? alignImagesToSameSize(rawBase, rawActual)
95
+ : [rawBase, rawActual];
78
96
  const diff = new pngjs_1.PNG({ width: base.width, height: base.height });
79
- const diffPixels = (0, pixelmatch_1.default)(actual.data, base.data, diff.data, diff.width, diff.height, PIXELMATCH_OPTIONS);
97
+ const diffPixels = pixelmatch(actual.data, base.data, diff.data, diff.width, diff.height, PIXELMATCH_OPTIONS);
80
98
  return { diffPixels, diff };
81
99
  }
82
100
  exports.getDiffPixels = getDiffPixels;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  /// <reference types="cypress" />
2
2
  export declare function setupVisualTests(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.PluginConfigOptions;
3
+ declare global {
4
+ namespace Cypress {
5
+ interface Chainable {
6
+ matchScreenshot(args?: MatchScreenshotArgs): Chainable;
7
+ }
8
+ }
9
+ }
package/dist/index.js CHANGED
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setupVisualTests = void 0;
4
4
  const screenshots_1 = require("./screenshots");
5
5
  function setupVisualTests(on, config) {
6
+ on('after:screenshot', screenshots_1.onAfterScreenshot);
6
7
  on('task', {
7
8
  baseExists: screenshots_1.baseExists,
8
9
  compareScreenshots: screenshots_1.compareScreenshots,
10
+ createNewScreenshot: screenshots_1.createNewScreenshot,
9
11
  log: (message) => {
10
12
  console.log(message);
11
13
  return null;
@@ -1,13 +1,15 @@
1
1
  /// <reference types="cypress" />
2
- export type MatchScreenshotArgs = {
2
+ declare const PREFIX_DIFFERENTIATOR = "___";
3
+ declare const SUFFIX_TEST_IDENTIFIER = ".spec.ts";
4
+ declare const SCREENSHOTS_FOLDER_NAME = "screenshots";
5
+ declare function forceFont(): false | HTMLStyleElement;
6
+ declare function getTestFolderPathFromScripts(rawName?: string): {
7
+ name: string;
8
+ screenshotsFolder: string;
9
+ };
10
+ interface MatchScreenshotArgs {
3
11
  rawName?: string;
4
12
  options?: Partial<Cypress.ScreenshotOptions>;
5
- };
6
- export declare function matchScreenshot(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
7
- declare global {
8
- namespace Cypress {
9
- interface Chainable {
10
- matchScreenshot(args?: MatchScreenshotArgs): Chainable;
11
- }
12
- }
13
13
  }
14
+ declare function verifyImages(): void;
15
+ declare function matchScreenshot(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
@@ -1,64 +1,74 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.matchScreenshot = void 0;
4
- function verifyImages() {
5
- if (Cypress.$('img:visible').length > 0) {
6
- cy.document()
7
- .its('body')
8
- .find('img')
9
- .filter(':visible')
10
- .then(images => {
11
- if (images) {
12
- cy.wrap(images).each($img => {
13
- cy.wrap($img).should('exist').and('have.prop', 'naturalWidth');
14
- });
15
- }
16
- });
17
- }
18
- }
2
+ const PREFIX_DIFFERENTIATOR = '___';
3
+ const SUFFIX_TEST_IDENTIFIER = '.spec.ts';
4
+ const SCREENSHOTS_FOLDER_NAME = 'screenshots';
19
5
  function forceFont() {
20
- const iframe = window.parent.document.querySelector('iframe');
6
+ const iframe = window.parent.document.querySelector("iframe");
21
7
  const contentDocument = iframe && iframe.contentDocument;
22
8
  if (contentDocument) {
23
9
  const style = contentDocument.createElement('style');
24
10
  style.type = 'text/css';
25
11
  style.appendChild(contentDocument.createTextNode('* { font-family: Arial !important; }'));
26
12
  contentDocument.head.appendChild(style);
13
+ return style;
14
+ }
15
+ return false;
16
+ }
17
+ function getTestFolderPathFromScripts(rawName) {
18
+ const relativeTestPath = Cypress.spec.relative;
19
+ if (!relativeTestPath) {
20
+ throw new Error('❌ Could not find matching script in the Cypress DOM to infer the test folder path');
21
+ }
22
+ // i.e. payment-card-cvvdialog
23
+ const testName = relativeTestPath.substring(relativeTestPath.lastIndexOf('/') + 1, relativeTestPath.lastIndexOf(SUFFIX_TEST_IDENTIFIER));
24
+ const name = rawName || testName;
25
+ // i.e. screenshots/packages/flights/forced-choice/test/visual/forced-choice/payment-card-cvvdialog
26
+ const screenshotsFolder = `${SCREENSHOTS_FOLDER_NAME}/${relativeTestPath.substring(0, relativeTestPath.lastIndexOf(testName))}${name}`;
27
+ return {
28
+ name,
29
+ screenshotsFolder
30
+ };
31
+ }
32
+ function verifyImages() {
33
+ if (Cypress.$('img:visible').length > 0) {
34
+ cy.document().its('body').find('img').filter(':visible').then((images) => {
35
+ if (images) {
36
+ cy.wrap(images).each(($img) => {
37
+ cy.wrap($img)
38
+ .should('exist')
39
+ .and('have.prop', 'naturalWidth');
40
+ });
41
+ }
42
+ });
27
43
  }
28
44
  }
29
45
  function matchScreenshot(subject, args) {
30
- const { rawName, options = {} } = args || {};
46
+ const { rawName, options = {} } = args ?? {};
47
+ // Set up screen
31
48
  forceFont();
49
+ // Making sure each image is visible before taking screenshots
32
50
  verifyImages();
33
- const screenshotsFolder = 'cypress/screenshots';
34
- const testPath = Cypress.spec.relative;
35
- const lastSlashIndex = testPath.lastIndexOf('/');
36
- const testPathWithoutFileName = testPath.substring(0, lastSlashIndex);
37
- const testFileName = testPath.substring(lastSlashIndex + 1);
38
- const testFileNameWithoutExtension = testFileName.split('.')[0];
39
- const testName = rawName || testFileNameWithoutExtension;
40
- const screenshotPath = `${screenshotsFolder}/${testPathWithoutFileName}/${testName}`;
41
- cy.task('baseExists', screenshotPath).then(hasBase => {
42
- if (typeof hasBase !== 'boolean')
43
- throw new Error('Result of baseExists task was not a boolean.');
51
+ const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
52
+ cy.task('baseExists', screenshotsFolder).then((hasBase) => {
53
+ const type = 'new';
44
54
  const target = subject ? cy.wrap(subject) : cy;
45
- // Cypress prepends the configured screenshotsFolder automatically here, so we must omit it
46
- target.screenshot(`${testPathWithoutFileName}/${testName}/new`, { ...options, overwrite: true });
55
+ // For easy slicing of path ignoring the root screenshot folder
56
+ target.screenshot(`${PREFIX_DIFFERENTIATOR}${screenshotsFolder}/${type}`, options);
47
57
  if (!hasBase) {
48
- cy.task('log', `❌ A new base image was created at ${screenshotPath}. Add this as a new base image via Comparadise!`);
49
- return;
58
+ cy.task('createNewScreenshot', screenshotsFolder).then(() => {
59
+ cy.task('log', `✅ A new base image was created for ${name}. Create this as a new base image via Comparadise!`);
60
+ });
50
61
  }
51
- cy.task('compareScreenshots', screenshotPath).then(diffPixels => {
52
- if (typeof diffPixels !== 'number')
53
- throw new Error('Result of compareScreenshots task was not a number.');
62
+ cy.task('compareScreenshots', screenshotsFolder).then((diffPixels) => {
54
63
  if (diffPixels === 0) {
55
- cy.log('✅ Actual image was the same as base.');
56
- }
57
- else {
58
- cy.task('log', `❌ Actual image of differed by ${diffPixels} pixels.`);
64
+ cy.log(`✅ Actual image of ${name} was the same as base`);
65
+ return null;
59
66
  }
67
+ const screenshotUrl = Cypress.env('BUILD_URL') ? `${Cypress.env('BUILD_URL')}artifact/${screenshotsFolder}` : screenshotsFolder;
68
+ cy.task('log', `❌ Actual image of ${name} differed by ${diffPixels} pixels.
69
+ See the diff image for more details >> ${screenshotUrl}/diff.png`);
60
70
  });
71
+ return null;
61
72
  });
62
73
  }
63
- exports.matchScreenshot = matchScreenshot;
64
74
  Cypress.Commands.add('matchScreenshot', { prevSubject: ['optional', 'element', 'window', 'document'] }, matchScreenshot);
@@ -1,2 +1,20 @@
1
- export declare function baseExists(screenshotFolder: string): boolean;
2
- export declare function compareScreenshots(screenshotFolder: string): number;
1
+ /// <reference types="cypress" />
2
+ /**
3
+ * Checks if a base image exists
4
+ * @param path - Folder path where you can find the base.png image
5
+ * @returns true if path/base.png exists, false if not.
6
+ */
7
+ export declare function baseExists(path: string): boolean;
8
+ export declare function createNewScreenshot(screenshotFolder: string): null;
9
+ /**
10
+ * Runs a visual regression test.
11
+ * @param screenshotFolder - Full screenshots folder where the base/new/diff
12
+ * images will be compared and written to.
13
+ */
14
+ export declare function compareScreenshots(screenshotFolder: string): any;
15
+ /**
16
+ * Renames all root cypress screenshots to where the test was actually run.
17
+ * Should NOT be used standalone. Works with the matchScreenshot task.
18
+ * @param {Cypress.ScreenshotDetails} details
19
+ */
20
+ export declare function onAfterScreenshot(details: Cypress.ScreenshotDetails): Promise<Cypress.AfterScreenshotReturnObject>;
@@ -23,40 +23,100 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.compareScreenshots = exports.baseExists = void 0;
26
+ exports.onAfterScreenshot = exports.compareScreenshots = exports.createNewScreenshot = exports.baseExists = void 0;
27
27
  const fs = __importStar(require("fs"));
28
- const path_1 = require("path");
29
28
  const pngjs_1 = require("pngjs");
30
29
  const images_1 = require("./images");
31
- function baseExists(screenshotFolder) {
32
- const fileName = (0, path_1.join)(screenshotFolder, 'base.png');
30
+ const files_1 = require("./files");
31
+ /**
32
+ * Checks if a base image exists
33
+ * @param path - Folder path where you can find the base.png image
34
+ * @returns true if path/base.png exists, false if not.
35
+ */
36
+ function baseExists(path) {
37
+ const fileName = (0, files_1.createImageFileName)(path, 'base');
33
38
  const exists = fs.existsSync(fileName);
34
39
  if (!exists) {
35
- console.log(`Base image does not exist at ${fileName}. This means a new one will be created. If your base should exist, something went wrong.`);
40
+ console.log('Base image does not exist. This means a new one will be created. If your base should exist, something went wrong.');
36
41
  }
37
42
  return exists;
38
43
  }
39
44
  exports.baseExists = baseExists;
45
+ function createNewScreenshot(screenshotFolder) {
46
+ const newImage = pngjs_1.PNG.sync.read(fs.readFileSync((0, files_1.createImageFileName)(screenshotFolder, 'new')));
47
+ fs.writeFile((0, files_1.createImageFileName)(screenshotFolder, 'new'), pngjs_1.PNG.sync.write(newImage), (err) => {
48
+ if (err) {
49
+ console.error('❌Unable to create new.png', err);
50
+ }
51
+ });
52
+ return null;
53
+ }
54
+ exports.createNewScreenshot = createNewScreenshot;
55
+ /**
56
+ * Runs a visual regression test.
57
+ * @param screenshotFolder - Full screenshots folder where the base/new/diff
58
+ * images will be compared and written to.
59
+ */
40
60
  function compareScreenshots(screenshotFolder) {
41
- const basePath = (0, path_1.join)(screenshotFolder, 'base.png');
42
- const actualPath = (0, path_1.join)(screenshotFolder, 'new.png');
61
+ const basePath = (0, files_1.createImageFileName)(screenshotFolder, 'base');
62
+ const actualPath = (0, files_1.createImageFileName)(screenshotFolder, 'new');
43
63
  const { diffPixels, diff } = (0, images_1.getDiffPixels)(basePath, actualPath);
44
64
  if (diffPixels) {
45
65
  // Create diff.png next to base and new for review
46
- fs.writeFile((0, path_1.join)(screenshotFolder, 'diff.png'), pngjs_1.PNG.sync.write(diff), err => {
66
+ fs.writeFile((0, files_1.createImageFileName)(screenshotFolder, 'diff'), pngjs_1.PNG.sync.write(diff), (err) => {
47
67
  if (err) {
48
- console.error('❌ Diff exists but unable to create diff.png', err);
68
+ console.error('❌Diff exists but unable to create diff.png', err);
49
69
  }
50
70
  });
51
71
  }
52
72
  else {
53
73
  // Delete created new.png. Not needed if there's no diff
54
- fs.unlink(actualPath, err => {
74
+ fs.unlink(actualPath, (err) => {
55
75
  if (err) {
56
- console.error('❌ No diff but unable to delete actualPath}', err);
76
+ console.error('❌No diff but unable to deleteactualPath}', err);
57
77
  }
58
78
  });
59
79
  }
60
80
  return diffPixels;
61
81
  }
62
82
  exports.compareScreenshots = compareScreenshots;
83
+ /**
84
+ * Renames all root cypress screenshots to where the test was actually run.
85
+ * Should NOT be used standalone. Works with the matchScreenshot task.
86
+ * @param {Cypress.ScreenshotDetails} details
87
+ */
88
+ function onAfterScreenshot(details) {
89
+ console.log('🧸 Screenshot was saved to:', details.path);
90
+ if (!details.path.match('cypress')) {
91
+ return Promise.resolve({});
92
+ }
93
+ const getNewPath = (path) => {
94
+ let newPath = path
95
+ .slice(path.lastIndexOf('___') + 3);
96
+ console.log(newPath);
97
+ if (newPath.startsWith('/')) {
98
+ newPath = `.${newPath}`;
99
+ }
100
+ return newPath;
101
+ };
102
+ const newPath = getNewPath(details.path);
103
+ const newPathDir = newPath.substring(0, newPath.lastIndexOf('/'));
104
+ try {
105
+ fs.mkdirSync(newPathDir, { recursive: true });
106
+ console.log('🧸 No screenshot folder found in the package. Created new screenshot folder:', newPathDir);
107
+ }
108
+ catch (err) {
109
+ console.error('❌ Error creating new screenshot folder:', newPathDir, err);
110
+ }
111
+ return new Promise((resolve, reject) => {
112
+ fs.rename(details.path, newPath, (err) => {
113
+ if (err) {
114
+ reject(err);
115
+ }
116
+ // because we renamed/moved the image, resolve with the new path
117
+ // so it is accurate in the test results
118
+ resolve({ path: newPath });
119
+ });
120
+ });
121
+ }
122
+ exports.onAfterScreenshot = onAfterScreenshot;
package/package.json CHANGED
@@ -29,5 +29,5 @@
29
29
  "build": "tsc",
30
30
  "postbuild": "echo \"require('./dist/match-screenshot');\" > commands.js"
31
31
  },
32
- "version": "0.0.9"
32
+ "version": "0.0.10"
33
33
  }
@@ -1,2 +0,0 @@
1
- /// <reference types="cypress" />
2
- export declare function onAfterScreenshot(details: Cypress.ScreenshotDetails): Promise<Cypress.AfterScreenshotReturnObject>;
@@ -1,51 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.onAfterScreenshot = void 0;
27
- const fs = __importStar(require("fs"));
28
- function onAfterScreenshot(details) {
29
- console.log('🧸 Screenshot was saved to:', details.path);
30
- if (!details.path.match('visual')) {
31
- return Promise.resolve({});
32
- }
33
- const newPath = details.path.substring(details.path.lastIndexOf('cypress/screenshots'));
34
- const newPathDir = newPath.substring(0, newPath.lastIndexOf('/'));
35
- try {
36
- fs.mkdirSync(newPathDir, { recursive: true });
37
- console.log('🧸 No screenshot folder found in the package. Created new screenshot folder:', newPathDir);
38
- }
39
- catch (err) {
40
- console.error('❌ Error creating new screenshot folder:', newPathDir, err);
41
- }
42
- return new Promise((resolve, reject) => {
43
- fs.rename(details.path, newPath, err => {
44
- if (err) {
45
- reject(err);
46
- }
47
- resolve({ path: newPath });
48
- });
49
- });
50
- }
51
- exports.onAfterScreenshot = onAfterScreenshot;