@coder/pixel-storybook 0.1.0 → 0.1.3-canary.20260616.2a423fe
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.
- package/README.md +0 -4
- package/{dist → build}/api.d.ts +3 -3
- package/{dist → build}/api.js +53 -52
- package/build/bin.js +103 -0
- package/{dist → build}/checkDifferences.d.ts +3 -3
- package/{dist → build}/checkDifferences.js +16 -15
- package/{dist → build}/compare/compare.d.ts +2 -2
- package/{dist → build}/compare/compare.js +17 -16
- package/{dist → build}/compare/pixelmatch.js +12 -6
- package/{dist → build}/compare/utils.d.ts +1 -1
- package/{dist → build}/compare/utils.js +2 -2
- package/{dist → build}/compare/worker.js +3 -2
- package/{dist → build}/concurrency.d.ts +6 -6
- package/{dist → build}/concurrency.js +13 -10
- package/{dist → build}/config.d.ts +289 -307
- package/{dist → build}/config.js +122 -130
- package/{dist → build}/configHelper.js +6 -5
- package/build/constants.js +3 -0
- package/{dist → build}/crawler/storybook.d.ts +2 -2
- package/{dist → build}/crawler/storybook.js +37 -36
- package/{dist → build}/crawler/utils.d.ts +1 -1
- package/{dist → build}/crawler/utils.js +5 -4
- package/{dist → build}/createShots.js +16 -15
- package/build/index.d.ts +2 -0
- package/build/index.js +2 -0
- package/{dist → build}/log.d.ts +4 -4
- package/{dist → build}/log.js +10 -9
- package/{dist → build}/runner.d.ts +1 -1
- package/{dist → build}/runner.js +42 -41
- package/{dist → build}/schemas.d.ts +28 -32
- package/{dist → build}/schemas.js +25 -26
- package/{dist → build}/shard.d.ts +2 -2
- package/{dist → build}/shard.js +3 -2
- package/{dist → build}/shots/shots.d.ts +2 -2
- package/{dist → build}/shots/shots.js +35 -38
- package/{dist → build}/shots/utils.d.ts +7 -7
- package/{dist → build}/shots/utils.js +28 -27
- package/{dist → build}/types.d.ts +2 -2
- package/build/types.js +2 -0
- package/{dist → build}/upload.d.ts +2 -2
- package/{dist → build}/upload.js +8 -7
- package/{dist → build}/uploadStorybook.d.ts +1 -1
- package/{dist → build}/uploadStorybook.js +20 -19
- package/{dist → build}/utils.d.ts +3 -3
- package/{dist → build}/utils.js +35 -35
- package/package.json +68 -63
- package/dist/bin.js +0 -102
- package/dist/constants.js +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/types.js +0 -1
- package/{dist → build}/bin.d.ts +0 -0
- package/{dist → build}/compare/pixelmatch.d.ts +0 -0
- package/{dist → build}/compare/worker.d.ts +0 -0
- package/{dist → build}/configHelper.d.ts +0 -0
- package/{dist → build}/constants.d.ts +0 -0
- package/{dist → build}/createShots.d.ts +1 -1
package/README.md
CHANGED
package/{dist → build}/api.d.ts
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { PlatformModeConfig } from
|
|
2
|
-
import { type LogMemory, log } from
|
|
3
|
-
import type { StabilizationResult } from
|
|
1
|
+
import type { PlatformModeConfig } from "./config.js";
|
|
2
|
+
import { type LogMemory, log } from "./log.js";
|
|
3
|
+
import type { StabilizationResult } from "./types.js";
|
|
4
4
|
export type ShotConfig = {
|
|
5
5
|
name: string;
|
|
6
6
|
threshold?: number;
|
package/{dist → build}/api.js
RENAMED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import { retry } from
|
|
3
|
-
import { log, logMemory } from
|
|
4
|
-
import { getVersion } from
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { retry } from "./concurrency.js";
|
|
3
|
+
import { log, logMemory } from "./log.js";
|
|
4
|
+
import { getVersion } from "./utils.js";
|
|
5
5
|
const version = getVersion();
|
|
6
6
|
const defaultHeaders = {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
"x-api-version": "3",
|
|
8
|
+
"x-client-version": version ?? "unknown",
|
|
9
9
|
};
|
|
10
10
|
const apiRoutes = {
|
|
11
|
-
getApiToken:
|
|
12
|
-
init:
|
|
13
|
-
finalize:
|
|
14
|
-
prepareUpload:
|
|
15
|
-
uploadShot:
|
|
16
|
-
processShots:
|
|
17
|
-
recordLogs:
|
|
18
|
-
uploadStorybookBuild:
|
|
11
|
+
getApiToken: "/auth/get-api-token",
|
|
12
|
+
init: "/app/init",
|
|
13
|
+
finalize: "/app/finalize",
|
|
14
|
+
prepareUpload: "/file/prepare-upload",
|
|
15
|
+
uploadShot: "/file/upload-shot",
|
|
16
|
+
processShots: "/app/process-shots",
|
|
17
|
+
recordLogs: "/app/record-logs",
|
|
18
|
+
uploadStorybookBuild: "/app/upload-storybook",
|
|
19
19
|
};
|
|
20
20
|
class ApiError extends Error {
|
|
21
21
|
status;
|
|
22
22
|
responseData;
|
|
23
23
|
constructor(message, status, responseData) {
|
|
24
24
|
super(message);
|
|
25
|
-
this.name =
|
|
25
|
+
this.name = "ApiError";
|
|
26
26
|
this.status = status;
|
|
27
27
|
this.responseData = responseData;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
const sendToAPI = async (config, parameters, fileKey, customLogger) => {
|
|
31
31
|
const logger = customLogger?.process ?? log.process;
|
|
32
|
-
logger(
|
|
33
|
-
logger(
|
|
32
|
+
logger("info", "api", `⚡️ Sending to API [${parameters.action}]`);
|
|
33
|
+
logger("info", "api", `Endpoint: ${config.lostPixelPlatform}${apiRoutes[parameters.action]}`);
|
|
34
34
|
try {
|
|
35
35
|
const apiCall = async () => {
|
|
36
36
|
const url = `${config.lostPixelPlatform}${apiRoutes[parameters.action]}`;
|
|
37
37
|
let body;
|
|
38
38
|
const headers = {
|
|
39
39
|
...defaultHeaders,
|
|
40
|
-
Authorization: `Bearer ${parameters.apiToken ??
|
|
41
|
-
|
|
40
|
+
"Authorization": `Bearer ${parameters.apiToken ?? ""}`,
|
|
41
|
+
"x-api-key": config.apiKey ?? "undefined",
|
|
42
42
|
};
|
|
43
43
|
if (fileKey) {
|
|
44
44
|
const form = new FormData();
|
|
45
45
|
for (const [key, element] of Object.entries(parameters.payload)) {
|
|
46
46
|
if (key === fileKey) {
|
|
47
47
|
const fileBuffer = await readFile(element);
|
|
48
|
-
const blob = new Blob([fileBuffer], { type:
|
|
49
|
-
form.append(key, blob, element.split(
|
|
48
|
+
const blob = new Blob([fileBuffer], { type: "image/png" });
|
|
49
|
+
form.append(key, blob, element.split("/").pop());
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
form.append(key, element);
|
|
@@ -57,10 +57,10 @@ const sendToAPI = async (config, parameters, fileKey, customLogger) => {
|
|
|
57
57
|
}
|
|
58
58
|
else {
|
|
59
59
|
body = JSON.stringify(parameters.payload);
|
|
60
|
-
headers[
|
|
60
|
+
headers["Content-type"] = "application/json";
|
|
61
61
|
}
|
|
62
62
|
const response = await fetch(url, {
|
|
63
|
-
method:
|
|
63
|
+
method: "POST",
|
|
64
64
|
headers,
|
|
65
65
|
body,
|
|
66
66
|
});
|
|
@@ -80,8 +80,8 @@ const sendToAPI = async (config, parameters, fileKey, customLogger) => {
|
|
|
80
80
|
times: 3,
|
|
81
81
|
interval(retryCount) {
|
|
82
82
|
const delay = Math.round(2 ** retryCount * 3000 * Math.random());
|
|
83
|
-
logger(
|
|
84
|
-
logger(
|
|
83
|
+
logger("info", "api", `🔄 Retry attempt ${retryCount} in ${delay}ms [${parameters.action}]`);
|
|
84
|
+
logger("info", "api", `${config.lostPixelPlatform}${apiRoutes[parameters.action]}`);
|
|
85
85
|
return delay;
|
|
86
86
|
},
|
|
87
87
|
errorFilter(error) {
|
|
@@ -93,33 +93,33 @@ const sendToAPI = async (config, parameters, fileKey, customLogger) => {
|
|
|
93
93
|
},
|
|
94
94
|
}, apiCall);
|
|
95
95
|
if (!response.ok) {
|
|
96
|
-
logger(
|
|
96
|
+
logger("error", "api", `Error: Failed to send to API [${parameters.action}]. Status: ${response.status}`);
|
|
97
97
|
process.exit(1);
|
|
98
98
|
}
|
|
99
|
-
const outdatedApiRequest = response?.headers?.[
|
|
99
|
+
const outdatedApiRequest = response?.headers?.["x-api-version-warning"];
|
|
100
100
|
if (outdatedApiRequest &&
|
|
101
|
-
(parameters.action ===
|
|
102
|
-
parameters.action ===
|
|
103
|
-
logger(
|
|
104
|
-
|
|
101
|
+
(parameters.action === "prepareUpload" ||
|
|
102
|
+
parameters.action === "finalize")) {
|
|
103
|
+
logger("info", "api", [
|
|
104
|
+
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
|
|
105
105
|
`~~ ⚠️ ${outdatedApiRequest}`,
|
|
106
|
-
|
|
107
|
-
].join(
|
|
106
|
+
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
|
|
107
|
+
].join("\n"));
|
|
108
108
|
}
|
|
109
|
-
logger(
|
|
109
|
+
logger("info", "api", `🤘 Successfully sent to API [${parameters.action}]`);
|
|
110
110
|
return response.data;
|
|
111
111
|
}
|
|
112
112
|
catch (error) {
|
|
113
113
|
if (error instanceof ApiError) {
|
|
114
|
-
logger(
|
|
114
|
+
logger("error", "api", "API response: ", error.responseData || error.message);
|
|
115
115
|
}
|
|
116
116
|
else if (error instanceof Error) {
|
|
117
|
-
logger(
|
|
117
|
+
logger("error", "api", error.message);
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
120
|
-
logger(
|
|
120
|
+
logger("error", "api", error);
|
|
121
121
|
}
|
|
122
|
-
if (parameters.action ===
|
|
122
|
+
if (parameters.action === "getApiToken") {
|
|
123
123
|
process.exit(1);
|
|
124
124
|
}
|
|
125
125
|
throw error;
|
|
@@ -127,7 +127,7 @@ const sendToAPI = async (config, parameters, fileKey, customLogger) => {
|
|
|
127
127
|
};
|
|
128
128
|
export const getApiToken = async (config) => {
|
|
129
129
|
return sendToAPI(config, {
|
|
130
|
-
action:
|
|
130
|
+
action: "getApiToken",
|
|
131
131
|
payload: {
|
|
132
132
|
projectId: config.lostPixelProjectId,
|
|
133
133
|
},
|
|
@@ -135,7 +135,7 @@ export const getApiToken = async (config) => {
|
|
|
135
135
|
};
|
|
136
136
|
export const sendInitToAPI = async (config, apiToken) => {
|
|
137
137
|
return sendToAPI(config, {
|
|
138
|
-
action:
|
|
138
|
+
action: "init",
|
|
139
139
|
apiToken,
|
|
140
140
|
payload: {
|
|
141
141
|
commit: config.commitHash,
|
|
@@ -146,7 +146,7 @@ export const sendInitToAPI = async (config, apiToken) => {
|
|
|
146
146
|
};
|
|
147
147
|
export const sendFinalizeToAPI = async (config, apiToken) => {
|
|
148
148
|
return sendToAPI(config, {
|
|
149
|
-
action:
|
|
149
|
+
action: "finalize",
|
|
150
150
|
apiToken,
|
|
151
151
|
payload: {
|
|
152
152
|
projectId: config.lostPixelProjectId,
|
|
@@ -158,7 +158,7 @@ export const sendFinalizeToAPI = async (config, apiToken) => {
|
|
|
158
158
|
};
|
|
159
159
|
export const prepareUpload = async (config, apiToken, shotNamesWithHashes) => {
|
|
160
160
|
return sendToAPI(config, {
|
|
161
|
-
action:
|
|
161
|
+
action: "prepareUpload",
|
|
162
162
|
apiToken,
|
|
163
163
|
payload: {
|
|
164
164
|
branchName: config.commitRefName,
|
|
@@ -173,18 +173,18 @@ export const uploadShot = async ({ config, apiToken, uploadToken, uploadUrl, nam
|
|
|
173
173
|
...config,
|
|
174
174
|
lostPixelPlatform: uploadUrl,
|
|
175
175
|
}, {
|
|
176
|
-
action:
|
|
176
|
+
action: "uploadShot",
|
|
177
177
|
apiToken,
|
|
178
178
|
payload: {
|
|
179
179
|
uploadToken,
|
|
180
180
|
name,
|
|
181
181
|
file,
|
|
182
182
|
},
|
|
183
|
-
},
|
|
183
|
+
}, "file", logger);
|
|
184
184
|
};
|
|
185
185
|
export const processShots = async (config, apiToken, uploadToken, shotsConfig) => {
|
|
186
186
|
return sendToAPI(config, {
|
|
187
|
-
action:
|
|
187
|
+
action: "processShots",
|
|
188
188
|
apiToken,
|
|
189
189
|
payload: {
|
|
190
190
|
uploadToken,
|
|
@@ -198,7 +198,7 @@ export const processShots = async (config, apiToken, uploadToken, shotsConfig) =
|
|
|
198
198
|
};
|
|
199
199
|
export const uploadStorybookBuild = async (config, apiToken, filePath) => {
|
|
200
200
|
return sendToAPI(config, {
|
|
201
|
-
action:
|
|
201
|
+
action: "uploadStorybookBuild",
|
|
202
202
|
apiToken,
|
|
203
203
|
payload: {
|
|
204
204
|
projectId: config.lostPixelProjectId,
|
|
@@ -208,12 +208,12 @@ export const uploadStorybookBuild = async (config, apiToken, filePath) => {
|
|
|
208
208
|
commit: config.commitHash,
|
|
209
209
|
file: filePath,
|
|
210
210
|
},
|
|
211
|
-
},
|
|
211
|
+
}, "file");
|
|
212
212
|
};
|
|
213
213
|
export const sendRecordLogsToAPI = async (config, apiToken) => {
|
|
214
214
|
try {
|
|
215
215
|
await sendToAPI(config, {
|
|
216
|
-
action:
|
|
216
|
+
action: "recordLogs",
|
|
217
217
|
apiToken,
|
|
218
218
|
payload: {
|
|
219
219
|
branchName: config.commitRefName,
|
|
@@ -225,14 +225,15 @@ export const sendRecordLogsToAPI = async (config, apiToken) => {
|
|
|
225
225
|
}
|
|
226
226
|
catch (error) {
|
|
227
227
|
if (error instanceof ApiError) {
|
|
228
|
-
log.process(
|
|
228
|
+
log.process("error", "api", "API response: ", error.responseData || error.message);
|
|
229
229
|
}
|
|
230
230
|
else if (error instanceof Error) {
|
|
231
|
-
log.process(
|
|
231
|
+
log.process("error", "api", error.message);
|
|
232
232
|
}
|
|
233
233
|
else {
|
|
234
|
-
log.process(
|
|
234
|
+
log.process("error", "api", error);
|
|
235
235
|
}
|
|
236
|
-
log.process(
|
|
236
|
+
log.process("error", "api", "Error: Failed to send logs to API");
|
|
237
237
|
}
|
|
238
238
|
};
|
|
239
|
+
//# sourceMappingURL=api.js.map
|
package/build/bin.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { cp } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { sendFinalizeToAPI } from "./api.js";
|
|
8
|
+
import { config, configure, isPlatformModeConfig } from "./config.js";
|
|
9
|
+
import { log } from "./log.js";
|
|
10
|
+
import { getPlatformApiToken, platformRunner, runner } from "./runner.js";
|
|
11
|
+
import { uploadStorybook } from "./uploadStorybook.js";
|
|
12
|
+
import { getVersion, isLocalDebugMode } from "./utils.js";
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const { positionals: commandArgs, values: optionValues } = parseArgs({
|
|
16
|
+
args: process.argv.slice(2),
|
|
17
|
+
options: {
|
|
18
|
+
help: { type: "boolean", short: "h" },
|
|
19
|
+
version: { type: "boolean", short: "v" },
|
|
20
|
+
},
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
strict: false,
|
|
23
|
+
});
|
|
24
|
+
const version = getVersion();
|
|
25
|
+
if (optionValues.version) {
|
|
26
|
+
log.process("info", "general", version ?? "unknown");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
if (optionValues.help) {
|
|
30
|
+
printHelp();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
if (version) {
|
|
34
|
+
log.process("info", "general", `Version: ${version}`);
|
|
35
|
+
}
|
|
36
|
+
function printHelp() {
|
|
37
|
+
log.process("info", "general", [
|
|
38
|
+
"Usage: pixel-storybook [command] [options]",
|
|
39
|
+
"",
|
|
40
|
+
"Visual regression testing tool for Storybook. Captures screenshots",
|
|
41
|
+
"via Playwright and compares them against baselines.",
|
|
42
|
+
"",
|
|
43
|
+
"Commands:",
|
|
44
|
+
" init-js Create a lostpixel.config.js in the current directory",
|
|
45
|
+
" init-ts Create a lostpixel.config.ts in the current directory",
|
|
46
|
+
" update Update baseline images with the current shots",
|
|
47
|
+
" local Enable local debug mode",
|
|
48
|
+
" meta Generate metadata for each shot",
|
|
49
|
+
" finalize Finalize a platform run (platform mode)",
|
|
50
|
+
" upload-storybook Upload a Storybook build to the platform",
|
|
51
|
+
"",
|
|
52
|
+
"Options:",
|
|
53
|
+
" -h, --help Print this help message and exit",
|
|
54
|
+
" -v, --version Print the version and exit",
|
|
55
|
+
" -m <mode> Set the mode (e.g. update)",
|
|
56
|
+
" --shard <n/total> Run a specific shard of the suite (e.g. 2/4)",
|
|
57
|
+
"",
|
|
58
|
+
"Environment variables:",
|
|
59
|
+
" LOST_PIXEL_MODE Same as -m",
|
|
60
|
+
' LOST_PIXEL_LOCAL Set to "true" for local debug mode',
|
|
61
|
+
' LOST_PIXEL_GENERATE_META Set to "true" to generate metadata',
|
|
62
|
+
" LOST_PIXEL_SHARD Same as --shard",
|
|
63
|
+
].join("\n"));
|
|
64
|
+
}
|
|
65
|
+
(async () => {
|
|
66
|
+
if (commandArgs.includes("init-js")) {
|
|
67
|
+
log.process("info", "general", "Initializing javascript lost-pixel config");
|
|
68
|
+
await cp(path.join(__dirname, "..", "config-templates", "example.lostpixel.config.js"), path.join(process.cwd(), "./lostpixel.config.js"));
|
|
69
|
+
log.process("info", "general", "✅ Config successfully initialized");
|
|
70
|
+
}
|
|
71
|
+
else if (commandArgs.includes("init-ts")) {
|
|
72
|
+
log.process("info", "general", "Initializing typescript lost-pixel config");
|
|
73
|
+
// Replace local type resolution with module resolution
|
|
74
|
+
const file = readFileSync(path.join(__dirname, "..", "config-templates", "example.lostpixel.config.ts"));
|
|
75
|
+
const modifiedFile = file.toString().replace("../src/config", "lost-pixel");
|
|
76
|
+
writeFileSync(path.join(process.cwd(), "./lostpixel.config.ts"), modifiedFile);
|
|
77
|
+
log.process("info", "general", "✅ Config successfully initialized");
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
await configure({
|
|
81
|
+
localDebugMode: isLocalDebugMode(),
|
|
82
|
+
});
|
|
83
|
+
if (isPlatformModeConfig(config)) {
|
|
84
|
+
log.process("info", "general", `🚀 Starting Pixel Storybook in 'platform' mode`);
|
|
85
|
+
const apiToken = await getPlatformApiToken(config);
|
|
86
|
+
if (commandArgs.includes("finalize")) {
|
|
87
|
+
await sendFinalizeToAPI(config, apiToken);
|
|
88
|
+
}
|
|
89
|
+
else if (commandArgs.includes("upload-storybook")) {
|
|
90
|
+
log.process("info", "general", "📤 Starting Pixel storybook build upload");
|
|
91
|
+
await uploadStorybook(config, apiToken);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await platformRunner(config, apiToken);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
log.process("info", "general", `🚀 Starting Pixel Storybook in 'generateOnly' mode`);
|
|
99
|
+
await runner(config);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
//# sourceMappingURL=bin.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ShotItem } from
|
|
1
|
+
import type { ShotItem } from "./types.js";
|
|
2
2
|
export declare const checkDifferences: (shotItems: ShotItem[]) => Promise<{
|
|
3
3
|
aboveThresholdDifferenceItems: {
|
|
4
|
+
url: string;
|
|
4
5
|
shotMode: "storybook";
|
|
5
6
|
id: string;
|
|
6
7
|
shotName: string;
|
|
7
|
-
url: string;
|
|
8
8
|
filePathBaseline: string;
|
|
9
9
|
filePathCurrent: string;
|
|
10
10
|
filePathDifference: string;
|
|
@@ -31,10 +31,10 @@ export declare const checkDifferences: (shotItems: ShotItem[]) => Promise<{
|
|
|
31
31
|
waitForSelector?: string | undefined;
|
|
32
32
|
}[];
|
|
33
33
|
noBaselinesItems: {
|
|
34
|
+
url: string;
|
|
34
35
|
shotMode: "storybook";
|
|
35
36
|
id: string;
|
|
36
37
|
shotName: string;
|
|
37
|
-
url: string;
|
|
38
38
|
filePathBaseline: string;
|
|
39
39
|
filePathCurrent: string;
|
|
40
40
|
filePathDifference: string;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { existsSync, writeFileSync } from
|
|
2
|
-
import path from
|
|
3
|
-
import { compareImages } from
|
|
4
|
-
import { mapLimit } from
|
|
5
|
-
import { config, isPlatformModeConfig } from
|
|
6
|
-
import { log } from
|
|
7
|
-
import { featureNotSupported, shallGenerateMeta } from
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compareImages } from "./compare/compare.js";
|
|
4
|
+
import { mapLimit } from "./concurrency.js";
|
|
5
|
+
import { config, isPlatformModeConfig } from "./config.js";
|
|
6
|
+
import { log } from "./log.js";
|
|
7
|
+
import { featureNotSupported, shallGenerateMeta } from "./utils.js";
|
|
8
8
|
export const checkDifferences = async (shotItems) => {
|
|
9
9
|
if (isPlatformModeConfig(config)) {
|
|
10
|
-
return featureNotSupported(
|
|
10
|
+
return featureNotSupported("checkDifferences()");
|
|
11
11
|
}
|
|
12
|
-
log.process(
|
|
12
|
+
log.process("info", "general", `Comparing ${shotItems.length} screenshots using '${config.compareEngine}' as compare engine`);
|
|
13
13
|
const total = shotItems.length;
|
|
14
14
|
const noBaselinesItems = [];
|
|
15
15
|
const aboveThresholdDifferenceItems = [];
|
|
@@ -23,12 +23,12 @@ export const checkDifferences = async (shotItems) => {
|
|
|
23
23
|
itemIndex: index,
|
|
24
24
|
totalItems: total,
|
|
25
25
|
})
|
|
26
|
-
.process(
|
|
26
|
+
.process("info", "general", message);
|
|
27
27
|
};
|
|
28
28
|
logger(`Comparing '${shotItem.id}'`);
|
|
29
29
|
const baselineImageExists = existsSync(shotItem.filePathBaseline);
|
|
30
30
|
if (!baselineImageExists) {
|
|
31
|
-
logger(
|
|
31
|
+
logger("Baseline image missing. Will be treated as addition.");
|
|
32
32
|
noBaselinesItems.push(shotItem);
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
@@ -55,13 +55,14 @@ export const checkDifferences = async (shotItems) => {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
|
-
logger(
|
|
58
|
+
logger("No difference found.");
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
if (shallGenerateMeta()) {
|
|
62
|
-
log.process(
|
|
63
|
-
writeFileSync(`${path.join(config.imagePathCurrent,
|
|
62
|
+
log.process("info", "general", `Writing meta file with ${Object.entries(comparisonResults).length} items.`);
|
|
63
|
+
writeFileSync(`${path.join(config.imagePathCurrent, "meta")}.json`, JSON.stringify(comparisonResults, null, 2));
|
|
64
64
|
}
|
|
65
|
-
log.process(
|
|
65
|
+
log.process("info", "general", "Comparison done!");
|
|
66
66
|
return { aboveThresholdDifferenceItems, noBaselinesItems };
|
|
67
67
|
};
|
|
68
|
+
//# sourceMappingURL=checkDifferences.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type CompareResult } from
|
|
2
|
-
export { checkThreshold } from
|
|
1
|
+
import { type CompareResult } from "./pixelmatch.js";
|
|
2
|
+
export { checkThreshold } from "./pixelmatch.js";
|
|
3
3
|
export declare const compareImagesViaPixelmatch: (threshold: number, baselineShotPath: string, currentShotPath: string, differenceShotPath?: string) => Promise<CompareResult>;
|
|
4
4
|
export declare const compareImagesViaOdiff: (threshold: number, baselineShotPath: string, currentShotPath: string, differenceShotPath: string) => Promise<CompareResult>;
|
|
5
5
|
export declare const compareImages: (threshold: number, baselineShotPath: string, currentShotPath: string, differenceShotPath: string) => Promise<CompareResult>;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import path from
|
|
3
|
-
import { fileURLToPath } from
|
|
4
|
-
import { Worker } from
|
|
5
|
-
import { compare as odiffCompare } from
|
|
6
|
-
import { config, isPlatformModeConfig } from
|
|
7
|
-
import { featureNotSupported } from
|
|
8
|
-
import { runPixelmatchComparison } from
|
|
9
|
-
export { checkThreshold } from
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
import { compare as odiffCompare } from "odiff-bin";
|
|
6
|
+
import { config, isPlatformModeConfig } from "../config.js";
|
|
7
|
+
import { featureNotSupported } from "../utils.js";
|
|
8
|
+
import { runPixelmatchComparison } from "./pixelmatch.js";
|
|
9
|
+
export { checkThreshold } from "./pixelmatch.js";
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
// Resolve the compiled worker script. Available after `pnpm build`.
|
|
13
13
|
// When running from source (e.g. vitest), the .js file won't exist
|
|
14
14
|
// and we fall back to inline comparison on the main thread.
|
|
15
|
-
const workerScript = path.join(__dirname,
|
|
15
|
+
const workerScript = path.join(__dirname, "worker.js");
|
|
16
16
|
const canUseWorker = existsSync(workerScript);
|
|
17
17
|
const comparePixelmatchWorker = (threshold, baselineShotPath, currentShotPath, differenceShotPath) => {
|
|
18
18
|
return new Promise((resolve, reject) => {
|
|
@@ -24,9 +24,9 @@ const comparePixelmatchWorker = (threshold, baselineShotPath, currentShotPath, d
|
|
|
24
24
|
differenceShotPath,
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
|
-
worker.on(
|
|
28
|
-
worker.on(
|
|
29
|
-
worker.on(
|
|
27
|
+
worker.on("message", resolve);
|
|
28
|
+
worker.on("error", reject);
|
|
29
|
+
worker.on("exit", (code) => {
|
|
30
30
|
if (code !== 0) {
|
|
31
31
|
reject(new Error(`Pixelmatch worker exited with code ${code}`));
|
|
32
32
|
}
|
|
@@ -50,7 +50,7 @@ export const compareImagesViaOdiff = async (threshold, baselineShotPath, current
|
|
|
50
50
|
isWithinThreshold: true,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
if (result.reason ===
|
|
53
|
+
if (result.reason === "pixel-diff") {
|
|
54
54
|
let isWithinThreshold = true;
|
|
55
55
|
// Treat theshold as percentage
|
|
56
56
|
const pixelDifferencePercentage = Number(result.diffPercentage / 100);
|
|
@@ -71,10 +71,11 @@ export const compareImagesViaOdiff = async (threshold, baselineShotPath, current
|
|
|
71
71
|
};
|
|
72
72
|
export const compareImages = async (threshold, baselineShotPath, currentShotPath, differenceShotPath) => {
|
|
73
73
|
if (isPlatformModeConfig(config)) {
|
|
74
|
-
return featureNotSupported(
|
|
74
|
+
return featureNotSupported("compareImages()");
|
|
75
75
|
}
|
|
76
|
-
if (config.compareEngine ===
|
|
76
|
+
if (config.compareEngine === "pixelmatch") {
|
|
77
77
|
return compareImagesViaPixelmatch(threshold, baselineShotPath, currentShotPath, differenceShotPath);
|
|
78
78
|
}
|
|
79
79
|
return compareImagesViaOdiff(threshold, baselineShotPath, currentShotPath, differenceShotPath);
|
|
80
80
|
};
|
|
81
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from
|
|
2
|
-
import pixelmatch from
|
|
3
|
-
import { PNG } from
|
|
4
|
-
import { resizeImage } from
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import pixelmatch from "pixelmatch";
|
|
3
|
+
import { PNG } from "pngjs";
|
|
4
|
+
import { resizeImage } from "./utils.js";
|
|
5
5
|
export const checkThreshold = (threshold, pixelsTotal, pixelDifference) => {
|
|
6
6
|
// Treat theshold as percentage
|
|
7
7
|
if (threshold < 1) {
|
|
@@ -14,13 +14,18 @@ export const runPixelmatchComparison = (threshold, baselineShotPath, currentShot
|
|
|
14
14
|
const baselineImageBuffer = readFileSync(baselineShotPath);
|
|
15
15
|
const currentImageBuffer = readFileSync(currentShotPath);
|
|
16
16
|
if (baselineImageBuffer.equals(currentImageBuffer)) {
|
|
17
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
pixelDifference: 0,
|
|
19
|
+
pixelDifferencePercentage: 0,
|
|
20
|
+
isWithinThreshold: true,
|
|
21
|
+
};
|
|
18
22
|
}
|
|
19
23
|
let baselineImage = PNG.sync.read(baselineImageBuffer);
|
|
20
24
|
let currentImage = PNG.sync.read(currentImageBuffer);
|
|
21
25
|
const maxWidth = Math.max(baselineImage.width || 100, currentImage.width || 100);
|
|
22
26
|
const maxHeight = Math.max(baselineImage.height || 100, currentImage.height || 100);
|
|
23
|
-
if (baselineImage.width !== currentImage.width ||
|
|
27
|
+
if (baselineImage.width !== currentImage.width ||
|
|
28
|
+
baselineImage.height !== currentImage.height) {
|
|
24
29
|
baselineImage = resizeImage(baselineImage, maxWidth, maxHeight);
|
|
25
30
|
currentImage = resizeImage(currentImage, maxWidth, maxHeight);
|
|
26
31
|
}
|
|
@@ -44,3 +49,4 @@ export const runPixelmatchComparison = (threshold, baselineShotPath, currentShot
|
|
|
44
49
|
isWithinThreshold: true,
|
|
45
50
|
};
|
|
46
51
|
};
|
|
52
|
+
//# sourceMappingURL=pixelmatch.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { PNG } from
|
|
1
|
+
import { PNG } from "pngjs";
|
|
2
2
|
export declare const resizeImage: (originalImage: PNG, width: number, height: number) => PNG;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PNG } from
|
|
1
|
+
import { PNG } from "pngjs";
|
|
2
2
|
export const resizeImage = (originalImage, width, height) => {
|
|
3
3
|
const newImage = new PNG({
|
|
4
4
|
width,
|
|
@@ -8,7 +8,6 @@ export const resizeImage = (originalImage, width, height) => {
|
|
|
8
8
|
});
|
|
9
9
|
for (let x = 0; x < width; x++) {
|
|
10
10
|
for (let y = 0; y < height; y++) {
|
|
11
|
-
// eslint-disable-next-line no-bitwise
|
|
12
11
|
const index = ((width * y + x) << 2) + 3;
|
|
13
12
|
newImage.data[index] = 64;
|
|
14
13
|
}
|
|
@@ -16,3 +15,4 @@ export const resizeImage = (originalImage, width, height) => {
|
|
|
16
15
|
PNG.bitblt(originalImage, newImage, 0, 0, originalImage.width, originalImage.height, 0, 0);
|
|
17
16
|
return newImage;
|
|
18
17
|
};
|
|
18
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// This file runs inside a worker_threads Worker.
|
|
2
2
|
// It performs the CPU-bound pixelmatch comparison off the main thread.
|
|
3
|
-
import { parentPort, workerData } from
|
|
4
|
-
import { runPixelmatchComparison } from
|
|
3
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
4
|
+
import { runPixelmatchComparison } from "./pixelmatch.js";
|
|
5
5
|
const result = runPixelmatchComparison(workerData.threshold, workerData.baselineShotPath, workerData.currentShotPath, workerData.differenceShotPath);
|
|
6
6
|
parentPort?.postMessage(result);
|
|
7
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default number of concurrent shots to take, derived from the number of
|
|
3
|
-
*
|
|
2
|
+
* Default number of concurrent shots to take, derived from the number of CPUs
|
|
3
|
+
* the process is allowed to use (respects cgroup CPU quotas).
|
|
4
4
|
*/
|
|
5
5
|
export declare const defaultShotConcurrency: () => number;
|
|
6
6
|
/**
|
|
7
|
-
* Run an array of async tasks with a concurrency limit.
|
|
8
|
-
*
|
|
7
|
+
* Run an array of async tasks with a concurrency limit. Drop-in replacement for
|
|
8
|
+
* `async.mapLimit`.
|
|
9
9
|
*/
|
|
10
10
|
export declare const mapLimit: <T, R>(items: Iterable<T>, limit: number, fn: (item: T) => Promise<R>) => Promise<R[]>;
|
|
11
11
|
/**
|
|
12
|
-
* Retry an async function with exponential backoff.
|
|
13
|
-
*
|
|
12
|
+
* Retry an async function with exponential backoff. Drop-in replacement for
|
|
13
|
+
* `async.retry`.
|
|
14
14
|
*/
|
|
15
15
|
export declare const retry: <T>(options: {
|
|
16
16
|
times: number;
|