@uniflowed/test 0.0.0-alpha.9 → 0.1.0

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,102 @@
1
+ // @flow
2
+ import { readFile, writeFile, mkdir, realpath, lstat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { PNG } from "pngjs";
5
+ import pixelmatch from "pixelmatch";
6
+
7
+ export type ScreenshotOptions = {|
8
+ readonly root?: string,
9
+ readonly baselines?: string,
10
+ readonly threshold?: number,
11
+ |};
12
+
13
+ /** Baselines stay under the project's configured directory, including through symlinks. */
14
+ export async function compareScreenshot(
15
+ data: string,
16
+ name: string,
17
+ options: ScreenshotOptions = {},
18
+ ) {
19
+ if (
20
+ typeof name !== "string" ||
21
+ !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/.test(name) ||
22
+ name.includes("..") ||
23
+ /\.(actual|diff)$/.test(name)
24
+ )
25
+ throw new Error("screenshot name must be a simple file name without traversal");
26
+ const config = JSON.parse(process.env.UF_VRT_CONFIG ?? "{}");
27
+ const root = await realpath(options.root ?? process.env.UF_PROJECT_ROOT ?? process.cwd());
28
+ const directory = path.resolve(root, options.baselines ?? config.baselines ?? "__uf_vrt__");
29
+ if (!directory.startsWith(root + path.sep))
30
+ throw new Error("vrt.baselines must stay inside the test project");
31
+ // Check each existing ancestor before creating anything through it.
32
+ let ancestor = root;
33
+ for (const segment of path.relative(root, directory).split(path.sep)) {
34
+ ancestor = path.join(ancestor, segment);
35
+ try {
36
+ if ((await lstat(ancestor)).isSymbolicLink())
37
+ throw new Error("vrt.baselines cannot traverse a symlink");
38
+ } catch (error) {
39
+ if (error.code !== "ENOENT") throw error;
40
+ }
41
+ }
42
+ await mkdir(directory, { recursive: true });
43
+ const baseline = path.join(directory, `${name}.png`);
44
+ try {
45
+ if ((await lstat(baseline)).isSymbolicLink())
46
+ throw new Error("screenshot baseline cannot be a symlink");
47
+ } catch (error) {
48
+ if (error.code !== "ENOENT") throw error;
49
+ }
50
+ const bytes = Buffer.from(data, "base64");
51
+ const actual = PNG.sync.read(bytes);
52
+ const threshold = options.threshold ?? config.threshold ?? 0;
53
+ if (!Number.isInteger(threshold) || threshold < 0)
54
+ throw new Error("vrt.threshold is the maximum number of different pixels");
55
+ if (process.env.UF_UPDATE_SNAPSHOTS === "1") {
56
+ await writeFile(baseline, bytes);
57
+ return { baseline, updated: true, differentPixels: 0 };
58
+ }
59
+ let expected;
60
+ try {
61
+ expected = PNG.sync.read(await readFile(baseline));
62
+ } catch (error) {
63
+ if (error.code === "ENOENT")
64
+ throw new Error(`missing screenshot baseline ${baseline}; record it with uf test -u`);
65
+ throw error;
66
+ }
67
+ const width = Math.max(actual.width, expected.width),
68
+ height = Math.max(actual.height, expected.height);
69
+ const diff = new PNG({ width, height });
70
+ const sameSize = actual.width === expected.width && actual.height === expected.height;
71
+ let differentPixels;
72
+ if (sameSize)
73
+ differentPixels = pixelmatch(actual.data, expected.data, diff.data, width, height, {
74
+ threshold: 0.1,
75
+ includeAA: true,
76
+ });
77
+ else {
78
+ differentPixels = width * height;
79
+ for (let i = 0; i < diff.data.length; i += 4) {
80
+ diff.data[i] = 255;
81
+ diff.data[i + 3] = 255;
82
+ }
83
+ }
84
+ if (!sameSize || differentPixels > threshold) {
85
+ const actualFile = path.join(directory, `${name}.actual.png`);
86
+ const diffFile = path.join(directory, `${name}.diff.png`);
87
+ for (const file of [actualFile, diffFile]) {
88
+ try {
89
+ if ((await lstat(file)).isSymbolicLink())
90
+ throw new Error("screenshot output cannot be a symlink");
91
+ } catch (error) {
92
+ if (error.code !== "ENOENT") throw error;
93
+ }
94
+ }
95
+ await writeFile(actualFile, bytes);
96
+ await writeFile(diffFile, PNG.sync.write(diff));
97
+ throw new Error(
98
+ `screenshot ${name}: ${differentPixels} pixels differ (allowed ${threshold})${sameSize ? "" : "; dimensions changed"}; diff: ${diffFile}`,
99
+ );
100
+ }
101
+ return { baseline, updated: false, differentPixels };
102
+ }