Skip to content

Commit 02f6672

Browse files
committed
refactor: separate image.utils and tasks for better readability
Signed-off-by: Jakub Freisler <[email protected]>
1 parent 86538fb commit 02f6672

File tree

3 files changed

+239
-207
lines changed

3 files changed

+239
-207
lines changed

src/image.utils.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import fs from "fs";
2+
import { PNG, PNGWithMetadata } from "pngjs";
3+
import sharp from "sharp";
4+
5+
const inArea = (x: number, y: number, height: number, width: number) =>
6+
y > height || x > width;
7+
8+
export const fillSizeDifference =
9+
(width: number, height: number) => (image: PNG) => {
10+
for (let y = 0; y < image.height; y++) {
11+
for (let x = 0; x < image.width; x++) {
12+
if (inArea(x, y, height, width)) {
13+
const idx = (image.width * y + x) << 2;
14+
image.data[idx] = 0;
15+
image.data[idx + 1] = 0;
16+
image.data[idx + 2] = 0;
17+
image.data[idx + 3] = 64;
18+
}
19+
}
20+
}
21+
return image;
22+
};
23+
24+
export const createImageResizer =
25+
(width: number, height: number) => (source: PNG) => {
26+
const resized = new PNG({ width, height, fill: true });
27+
PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
28+
return resized;
29+
};
30+
31+
export const importAndScaleImage = async (cfg: {
32+
scaleFactor: number;
33+
path: string;
34+
}) => {
35+
const imgBuffer = fs.readFileSync(cfg.path);
36+
const rawImgNew = PNG.sync.read(imgBuffer);
37+
if (cfg.scaleFactor === 1) return rawImgNew;
38+
39+
const newImageWidth = Math.ceil(rawImgNew.width * cfg.scaleFactor);
40+
const newImageHeight = Math.ceil(rawImgNew.height * cfg.scaleFactor);
41+
await sharp(imgBuffer).resize(newImageWidth, newImageHeight).toFile(cfg.path);
42+
43+
return PNG.sync.read(fs.readFileSync(cfg.path));
44+
};
45+
46+
export const alignImagesToSameSize = (
47+
firstImage: PNGWithMetadata,
48+
secondImage: PNGWithMetadata
49+
) => {
50+
const firstImageWidth = firstImage.width;
51+
const firstImageHeight = firstImage.height;
52+
const secondImageWidth = secondImage.width;
53+
const secondImageHeight = secondImage.height;
54+
55+
const resizeToSameSize = createImageResizer(
56+
Math.max(firstImageWidth, secondImageWidth),
57+
Math.max(firstImageHeight, secondImageHeight)
58+
);
59+
60+
const resizedFirst = resizeToSameSize(firstImage);
61+
const resizedSecond = resizeToSameSize(secondImage);
62+
63+
return [
64+
fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
65+
fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond),
66+
];
67+
};

src/plugins.ts

Lines changed: 22 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,11 @@
11
import path from "path";
2-
import pixelmatch from "pixelmatch";
32
import fs from "fs";
4-
import { PNG, PNGWithMetadata } from "pngjs";
5-
import { FILE_SUFFIX, IMAGE_SNAPSHOT_PREFIX, TASK } from "./constants";
3+
import { IMAGE_SNAPSHOT_PREFIX } from "./constants";
64
import moveFile from "move-file";
7-
import sharp from "sharp";
8-
import sanitize from "sanitize-filename";
5+
import { initTaskHooks } from "./tasks";
96

107
type NotFalsy<T> = T extends false | null | undefined ? never : T;
118

12-
type CompareImagesCfg = {
13-
scaleFactor: number;
14-
title: string;
15-
imgNew: string;
16-
imgOld: string;
17-
updateImages: boolean;
18-
maxDiffThreshold: number;
19-
diffConfig: Parameters<typeof pixelmatch>[5];
20-
} & Parameters<typeof pixelmatch>[5];
21-
22-
const round = (n: number) => Math.ceil(n * 1000) / 1000;
23-
24-
const createImageResizer = (width: number, height: number) => (source: PNG) => {
25-
const resized = new PNG({ width, height, fill: true });
26-
PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
27-
return resized;
28-
};
29-
30-
const inArea = (x: number, y: number, height: number, width: number) =>
31-
y > height || x > width;
32-
33-
const fillSizeDifference = (width: number, height: number) => (image: PNG) => {
34-
for (let y = 0; y < image.height; y++) {
35-
for (let x = 0; x < image.width; x++) {
36-
if (inArea(x, y, height, width)) {
37-
const idx = (image.width * y + x) << 2;
38-
image.data[idx] = 0;
39-
image.data[idx + 1] = 0;
40-
image.data[idx + 2] = 0;
41-
image.data[idx + 3] = 64;
42-
}
43-
}
44-
}
45-
return image;
46-
};
47-
48-
const importAndScaleImage = async (cfg: {
49-
scaleFactor: number;
50-
path: string;
51-
}) => {
52-
const imgBuffer = fs.readFileSync(cfg.path);
53-
const rawImgNew = PNG.sync.read(imgBuffer);
54-
if (cfg.scaleFactor === 1) return rawImgNew;
55-
56-
const newImageWidth = Math.ceil(rawImgNew.width * cfg.scaleFactor);
57-
const newImageHeight = Math.ceil(rawImgNew.height * cfg.scaleFactor);
58-
await sharp(imgBuffer).resize(newImageWidth, newImageHeight).toFile(cfg.path);
59-
60-
return PNG.sync.read(fs.readFileSync(cfg.path));
61-
};
62-
63-
const alignImagesToSameSize = (
64-
firstImage: PNGWithMetadata,
65-
secondImage: PNGWithMetadata
66-
) => {
67-
const firstImageWidth = firstImage.width;
68-
const firstImageHeight = firstImage.height;
69-
const secondImageWidth = secondImage.width;
70-
const secondImageHeight = secondImage.height;
71-
72-
const resizeToSameSize = createImageResizer(
73-
Math.max(firstImageWidth, secondImageWidth),
74-
Math.max(firstImageHeight, secondImageHeight)
75-
);
76-
77-
const resizedFirst = resizeToSameSize(firstImage);
78-
const resizedSecond = resizeToSameSize(secondImage);
79-
80-
return [
81-
fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
82-
fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond),
83-
];
84-
};
85-
869
const getConfigVariableOrThrow = <K extends keyof Cypress.PluginConfigOptions>(
8710
config: Cypress.PluginConfigOptions,
8811
name: K
@@ -108,124 +31,20 @@ const initForceDeviceScaleFactor = (on: Cypress.PluginEvents) => {
10831
});
10932
};
11033

111-
const initTaskHooks = (on: Cypress.PluginEvents) => {
112-
on("task", {
113-
[TASK.getScreenshotPath]({ title, imagesDir, specPath }) {
114-
return path.join(
115-
IMAGE_SNAPSHOT_PREFIX,
116-
path.dirname(specPath),
117-
...imagesDir.split("/"),
118-
`${sanitize(title)}${FILE_SUFFIX.actual}.png`
119-
);
120-
},
121-
[TASK.doesFileExist]({ path }) {
122-
return fs.existsSync(path);
123-
},
124-
[TASK.approveImage]({ img }) {
125-
const oldImg = img.replace(FILE_SUFFIX.actual, "");
126-
if (fs.existsSync(oldImg)) fs.unlinkSync(oldImg);
127-
128-
const diffImg = img.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff);
129-
if (fs.existsSync(diffImg)) fs.unlinkSync(diffImg);
130-
131-
if (fs.existsSync(img)) moveFile.sync(img, oldImg);
132-
133-
return null;
134-
},
135-
async [TASK.compareImages](cfg: CompareImagesCfg): Promise<null | {
136-
error?: boolean;
137-
message?: string;
138-
imgDiff?: number;
139-
maxDiffThreshold?: number;
140-
}> {
141-
const messages = [] as string[];
142-
let imgDiff: number | undefined;
143-
let error = false;
144-
145-
if (fs.existsSync(cfg.imgOld) && !cfg.updateImages) {
146-
const rawImgNew = await importAndScaleImage({
147-
scaleFactor: cfg.scaleFactor,
148-
path: cfg.imgNew,
149-
});
150-
const rawImgOld = PNG.sync.read(fs.readFileSync(cfg.imgOld));
151-
const isImgSizeDifferent =
152-
rawImgNew.height !== rawImgOld.height ||
153-
rawImgNew.width !== rawImgOld.width;
154-
155-
const [imgNew, imgOld] = isImgSizeDifferent
156-
? alignImagesToSameSize(rawImgNew, rawImgOld)
157-
: [rawImgNew, rawImgOld];
158-
159-
const { width, height } = imgNew;
160-
const diff = new PNG({ width, height });
161-
const diffConfig = Object.assign({ includeAA: true }, cfg.diffConfig);
162-
163-
const diffPixels = pixelmatch(
164-
imgNew.data,
165-
imgOld.data,
166-
diff.data,
167-
width,
168-
height,
169-
diffConfig
170-
);
171-
imgDiff = diffPixels / (width * height);
172-
173-
if (isImgSizeDifferent) {
174-
messages.push(
175-
`Warning: Images size mismatch - new screenshot is ${rawImgNew.width}px by ${rawImgNew.height}px while old one is ${rawImgOld.width}px by ${rawImgOld.height} (width x height).`
176-
);
177-
}
178-
179-
if (imgDiff > cfg.maxDiffThreshold) {
180-
messages.unshift(
181-
`Image diff factor (${round(
182-
imgDiff
183-
)}) is bigger than maximum threshold option ${
184-
cfg.maxDiffThreshold
185-
}.`
186-
);
187-
error = true;
188-
}
189-
190-
if (error) {
191-
fs.writeFileSync(
192-
cfg.imgNew.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff),
193-
PNG.sync.write(diff)
194-
);
195-
return {
196-
error,
197-
message: messages.join("\n"),
198-
imgDiff,
199-
maxDiffThreshold: cfg.maxDiffThreshold,
200-
};
201-
} else {
202-
// don't overwrite file if it's the same (imgDiff < cfg.maxDiffThreshold && !isImgSizeDifferent)
203-
fs.unlinkSync(cfg.imgNew);
204-
}
205-
} else {
206-
// there is no "old screenshot" or screenshots should be immediately updated
207-
imgDiff = 0;
208-
moveFile.sync(cfg.imgNew, cfg.imgOld);
209-
}
210-
211-
if (typeof imgDiff !== "undefined") {
212-
messages.unshift(
213-
`Image diff (${round(
214-
imgDiff
215-
)}%) is within boundaries of maximum threshold option ${
216-
cfg.maxDiffThreshold
217-
}.`
218-
);
219-
return {
220-
message: messages.join("\n"),
221-
imgDiff,
222-
maxDiffThreshold: cfg.maxDiffThreshold,
223-
};
224-
}
34+
const removeScreenshotsDirectory = (
35+
screenshotsFolder: string,
36+
onSuccess: () => void,
37+
onError: (e: Error) => void
38+
) => {
39+
fs.rm(
40+
path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX),
41+
{ recursive: true, force: true },
42+
(err) => {
43+
if (err) return onError(err);
22544

226-
return null;
227-
},
228-
});
45+
onSuccess();
46+
}
47+
);
22948
};
23049

23150
const initAfterScreenshotHook = (
@@ -249,17 +68,13 @@ const initAfterScreenshotHook = (
24968
);
25069

25170
void moveFile(details.path, newAbsolutePath)
252-
.then(() => {
253-
fs.rm(
254-
path.join(screenshotsFolder, IMAGE_SNAPSHOT_PREFIX),
255-
{ recursive: true, force: true },
256-
(err) => {
257-
if (err) return reject(err);
258-
259-
resolve({ path: newAbsolutePath });
260-
}
261-
);
262-
})
71+
.then(() =>
72+
removeScreenshotsDirectory(
73+
screenshotsFolder,
74+
() => resolve({ path: newAbsolutePath }),
75+
reject
76+
)
77+
)
26378
.catch(reject);
26479
});
26580
});

0 commit comments

Comments
 (0)