comparadise-utils 0.0.9 → 0.0.11

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,4 +1,10 @@
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
9
  diffPixels: number;
4
10
  diff: PNG;
package/dist/images.js CHANGED
@@ -33,11 +33,19 @@ const pixelmatch_1 = __importDefault(require("pixelmatch"));
33
33
  const PIXELMATCH_OPTIONS = {
34
34
  threshold: 0.3
35
35
  };
36
+ /**
37
+ * Helper function to create reusable image resizer
38
+ */
36
39
  const createImageResizer = (width, height) => (source) => {
37
40
  const resized = new pngjs_1.PNG({ width, height, fill: true });
38
41
  pngjs_1.PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
39
42
  return resized;
40
43
  };
44
+ /**
45
+ * Fills new area added after resize with transparent black color.
46
+ * I like idea of checker board pattern, but it seems to be too complicated
47
+ * to implement considering how low-level pngjs API is.
48
+ */
41
49
  const fillSizeDifference = (width, height) => (image) => {
42
50
  const inArea = (x, y) => y > height || x > width;
43
51
  for (let y = 0; y < image.height; y++) {
@@ -53,6 +61,10 @@ const fillSizeDifference = (width, height) => (image) => {
53
61
  }
54
62
  return image;
55
63
  };
64
+ /**
65
+ * Aligns images sizes to biggest common value
66
+ * and fills new pixels with transparent pixels
67
+ */
56
68
  function alignImagesToSameSize(firstImage, secondImage) {
57
69
  // Keep original sizes to fill extended area later
58
70
  const firstImageWidth = firstImage.width;
@@ -70,6 +82,12 @@ function alignImagesToSameSize(firstImage, secondImage) {
70
82
  fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond)
71
83
  ];
72
84
  }
85
+ /**
86
+ * Compares a base and new image and returns the pixel difference
87
+ * and diff PNG for writing the diff image to.
88
+ * @param {string} basePath - Full file path to base image
89
+ * @param {string} actualPath - Full file path to new image
90
+ */
73
91
  function getDiffPixels(basePath, actualPath) {
74
92
  const rawBase = pngjs_1.PNG.sync.read(fs.readFileSync(basePath));
75
93
  const rawActual = pngjs_1.PNG.sync.read(fs.readFileSync(actualPath));
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,6 +1,32 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.matchScreenshot = void 0;
2
+ const PREFIX_DIFFERENTIATOR = '___';
3
+ const SUFFIX_TEST_IDENTIFIER = '.spec.ts';
4
+ const SCREENSHOTS_FOLDER_NAME = 'screenshots';
5
+ function forceFont() {
6
+ const iframe = window.parent.document.querySelector('iframe');
7
+ const contentDocument = iframe && iframe.contentDocument;
8
+ if (contentDocument) {
9
+ const style = contentDocument.createElement('style');
10
+ style.type = 'text/css';
11
+ style.appendChild(contentDocument.createTextNode('* { font-family: Arial !important; }'));
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
+ const testName = relativeTestPath.substring(relativeTestPath.lastIndexOf('/') + 1, relativeTestPath.lastIndexOf(SUFFIX_TEST_IDENTIFIER));
23
+ const name = rawName || testName;
24
+ const screenshotsFolder = `${SCREENSHOTS_FOLDER_NAME}/${relativeTestPath.substring(0, relativeTestPath.lastIndexOf(testName))}${name}`;
25
+ return {
26
+ name,
27
+ screenshotsFolder
28
+ };
29
+ }
4
30
  function verifyImages() {
5
31
  if (Cypress.$('img:visible').length > 0) {
6
32
  cy.document()
@@ -16,49 +42,34 @@ function verifyImages() {
16
42
  });
17
43
  }
18
44
  }
19
- function forceFont() {
20
- const iframe = window.parent.document.querySelector('iframe');
21
- const contentDocument = iframe && iframe.contentDocument;
22
- if (contentDocument) {
23
- const style = contentDocument.createElement('style');
24
- style.type = 'text/css';
25
- style.appendChild(contentDocument.createTextNode('* { font-family: Arial !important; }'));
26
- contentDocument.head.appendChild(style);
27
- }
28
- }
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!`);
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
+ });
49
61
  return;
50
62
  }
51
- cy.task('compareScreenshots', screenshotPath).then(diffPixels => {
52
- if (typeof diffPixels !== 'number')
53
- throw new Error('Result of compareScreenshots task was not a number.');
63
+ cy.task('compareScreenshots', screenshotsFolder).then(diffPixels => {
54
64
  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.`);
65
+ cy.log(`✅ Actual image of ${name} was the same as base`);
66
+ return null;
59
67
  }
68
+ const screenshotUrl = Cypress.env('BUILD_URL') ? `${Cypress.env('BUILD_URL')}artifact/${screenshotsFolder}` : screenshotsFolder;
69
+ cy.task('log', `❌ Actual image of ${name} differed by ${diffPixels} pixels.
70
+ See the diff image for more details >> ${screenshotUrl}/diff.png`);
60
71
  });
72
+ return null;
61
73
  });
62
74
  }
63
- exports.matchScreenshot = matchScreenshot;
64
75
  Cypress.Commands.add('matchScreenshot', { prevSubject: ['optional', 'element', 'window', 'document'] }, matchScreenshot);
@@ -1,2 +1,20 @@
1
- export declare function baseExists(screenshotFolder: string): boolean;
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
+ */
2
14
  export declare function compareScreenshots(screenshotFolder: string): number;
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,29 +23,49 @@ 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
  }
@@ -53,10 +73,49 @@ function compareScreenshots(screenshotFolder) {
53
73
  // Delete created new.png. Not needed if there's no diff
54
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.slice(path.lastIndexOf('___') + 3);
95
+ console.log(newPath);
96
+ if (newPath.startsWith('/')) {
97
+ newPath = `.${newPath}`;
98
+ }
99
+ return newPath;
100
+ };
101
+ const newPath = getNewPath(details.path);
102
+ const newPathDir = newPath.substring(0, newPath.lastIndexOf('/'));
103
+ try {
104
+ fs.mkdirSync(newPathDir, { recursive: true });
105
+ console.log('🧸 No screenshot folder found in the package. Created new screenshot folder:', newPathDir);
106
+ }
107
+ catch (err) {
108
+ console.error('❌ Error creating new screenshot folder:', newPathDir, err);
109
+ }
110
+ return new Promise((resolve, reject) => {
111
+ fs.rename(details.path, newPath, err => {
112
+ if (err) {
113
+ reject(err);
114
+ }
115
+ // because we renamed/moved the image, resolve with the new path
116
+ // so it is accurate in the test results
117
+ resolve({ path: newPath });
118
+ });
119
+ });
120
+ }
121
+ 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.11"
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;