@rindo/core 3.0.0 → 3.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.
- package/cli/config-flags.d.ts +122 -122
- package/cli/index.cjs +2301 -2432
- package/cli/index.d.ts +19 -19
- package/cli/index.js +2302 -2433
- package/cli/package.json +1 -1
- package/compiler/package.json +1 -1
- package/compiler/rindo.d.ts +73 -73
- package/compiler/rindo.js +66137 -62812
- package/compiler/rindo.min.js +2 -2
- package/compiler/sys/in-memory-fs.d.ts +218 -218
- package/compiler/transpile.d.ts +32 -32
- package/dev-server/client/app-error.d.ts +18 -18
- package/dev-server/client/events.d.ts +6 -6
- package/dev-server/client/hmr-components.d.ts +1 -1
- package/dev-server/client/hmr-external-styles.d.ts +1 -1
- package/dev-server/client/hmr-images.d.ts +1 -1
- package/dev-server/client/hmr-inline-styles.d.ts +1 -1
- package/dev-server/client/hmr-util.d.ts +9 -9
- package/dev-server/client/hmr-window.d.ts +10 -10
- package/dev-server/client/index.d.ts +6 -6
- package/dev-server/client/index.js +781 -781
- package/dev-server/client/logger.d.ts +5 -5
- package/dev-server/client/package.json +1 -1
- package/dev-server/client/progress.d.ts +3 -3
- package/dev-server/client/status.d.ts +4 -4
- package/dev-server/connector.html +2 -2
- package/dev-server/index.d.ts +3 -3
- package/dev-server/index.js +228 -228
- package/dev-server/open-in-editor-api.js +1 -1
- package/dev-server/package.json +1 -1
- package/dev-server/server-process.js +1301 -1279
- package/dev-server/ws.js +1 -1
- package/dev-server/xdg-open +0 -0
- package/internal/app-data/index.cjs +88 -88
- package/internal/app-data/index.d.ts +4 -4
- package/internal/app-data/index.js +88 -88
- package/internal/app-data/package.json +1 -1
- package/internal/client/css-shim.js +1 -1
- package/internal/client/dom.js +1 -1
- package/internal/client/index.js +3380 -3373
- package/internal/client/package.json +1 -1
- package/internal/client/patch-browser.js +155 -155
- package/internal/client/patch-esm.js +25 -25
- package/internal/client/shadow-css.js +382 -382
- package/internal/hydrate/package.json +1 -1
- package/internal/index.d.ts +2 -2
- package/internal/index.js +1 -1
- package/internal/package.json +1 -1
- package/internal/rindo-private.d.ts +2272 -2268
- package/internal/rindo-public-compiler.d.ts +2377 -2353
- package/internal/rindo-public-docs.d.ts +139 -139
- package/internal/rindo-public-runtime.d.ts +1636 -1636
- package/internal/testing/package.json +1 -1
- package/mock-doc/index.cjs +4766 -4766
- package/mock-doc/index.d.ts +1006 -1006
- package/mock-doc/index.js +4766 -4766
- package/mock-doc/package.json +1 -1
- package/package.json +10 -10
- package/screenshot/connector-base.d.ts +42 -42
- package/screenshot/connector-local.d.ts +7 -7
- package/screenshot/index.d.ts +3 -3
- package/screenshot/index.js +615 -615
- package/screenshot/package.json +1 -1
- package/screenshot/pixel-match.d.ts +1 -1
- package/screenshot/pixel-match.js +14 -14
- package/screenshot/screenshot-compare.d.ts +3 -3
- package/screenshot/screenshot-fs.d.ts +15 -15
- package/sys/node/autoprefixer.js +2 -2
- package/sys/node/glob.js +1 -1
- package/sys/node/graceful-fs.js +1 -1
- package/sys/node/index.d.ts +22 -22
- package/sys/node/index.js +6 -3
- package/sys/node/node-fetch.js +1 -1
- package/sys/node/package.json +1 -1
- package/sys/node/prompts.js +1 -1
- package/sys/node/worker.js +1 -1
- package/testing/index.d.ts +12 -12
- package/testing/index.js +7 -3
- package/testing/jest/jest-config.d.ts +16 -16
- package/testing/jest/jest-environment.d.ts +15 -15
- package/testing/jest/jest-preprocessor.d.ts +59 -59
- package/testing/jest/jest-runner.d.ts +10 -10
- package/testing/jest/jest-screenshot.d.ts +2 -2
- package/testing/jest/jest-serializer.d.ts +4 -4
- package/testing/jest/jest-setup-test-framework.d.ts +1 -1
- package/testing/matchers/attributes.d.ts +14 -14
- package/testing/matchers/class-list.d.ts +12 -12
- package/testing/matchers/events.d.ts +21 -21
- package/testing/matchers/html.d.ts +12 -12
- package/testing/matchers/index.d.ts +23 -23
- package/testing/matchers/screenshot.d.ts +5 -5
- package/testing/matchers/text.d.ts +4 -4
- package/testing/mock-fetch.d.ts +11 -11
- package/testing/mocks.d.ts +56 -56
- package/testing/package.json +1 -1
- package/testing/puppeteer/index.d.ts +2 -2
- package/testing/puppeteer/puppeteer-browser.d.ts +6 -6
- package/testing/puppeteer/puppeteer-declarations.d.ts +403 -403
- package/testing/puppeteer/puppeteer-element.d.ts +67 -67
- package/testing/puppeteer/puppeteer-emulate.d.ts +2 -2
- package/testing/puppeteer/puppeteer-events.d.ts +21 -21
- package/testing/puppeteer/puppeteer-page.d.ts +2 -2
- package/testing/puppeteer/puppeteer-screenshot.d.ts +4 -4
- package/testing/reset-build-conditionals.d.ts +2 -2
- package/testing/spec-page.d.ts +2 -2
- package/testing/test-transpile.d.ts +2 -2
- package/testing/testing-logger.d.ts +25 -25
- package/testing/testing-sys.d.ts +6 -6
- package/testing/testing-utils.d.ts +79 -79
- package/testing/testing.d.ts +2 -2
- package/dependencies.json +0 -120
package/screenshot/index.js
CHANGED
|
@@ -9,626 +9,626 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
|
|
|
9
9
|
const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
10
10
|
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
11
11
|
|
|
12
|
-
function fileExists(filePath) {
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
fs__default['default'].access(filePath, (err) => resolve(!err));
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
function readFile(filePath) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
fs__default['default'].readFile(filePath, 'utf-8', (err, data) => {
|
|
20
|
-
if (err) {
|
|
21
|
-
reject(err);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
resolve(data);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
function readFileBuffer(filePath) {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
fs__default['default'].readFile(filePath, (err, data) => {
|
|
32
|
-
if (err) {
|
|
33
|
-
reject(err);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
resolve(data);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function writeFile(filePath, data) {
|
|
42
|
-
return new Promise((resolve, reject) => {
|
|
43
|
-
fs__default['default'].writeFile(filePath, data, (err) => {
|
|
44
|
-
if (err) {
|
|
45
|
-
reject(err);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
resolve();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function mkDir(filePath) {
|
|
54
|
-
return new Promise((resolve) => {
|
|
55
|
-
fs__default['default'].mkdir(filePath, () => {
|
|
56
|
-
resolve();
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function rmDir(filePath) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
fs__default['default'].rmdir(filePath, () => {
|
|
63
|
-
resolve();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
async function emptyDir(dir) {
|
|
68
|
-
const files = await readDir(dir);
|
|
69
|
-
const promises = files.map(async (fileName) => {
|
|
70
|
-
const filePath = path__default['default'].join(dir, fileName);
|
|
71
|
-
const isDirFile = await isFile(filePath);
|
|
72
|
-
if (isDirFile) {
|
|
73
|
-
await unlink(filePath);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
await Promise.all(promises);
|
|
77
|
-
}
|
|
78
|
-
async function readDir(dir) {
|
|
79
|
-
return new Promise((resolve) => {
|
|
80
|
-
fs__default['default'].readdir(dir, (err, files) => {
|
|
81
|
-
if (err) {
|
|
82
|
-
resolve([]);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
resolve(files);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
async function isFile(itemPath) {
|
|
91
|
-
return new Promise((resolve) => {
|
|
92
|
-
fs__default['default'].stat(itemPath, (err, stat) => {
|
|
93
|
-
if (err) {
|
|
94
|
-
resolve(false);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
resolve(stat.isFile());
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async function unlink(filePath) {
|
|
103
|
-
return new Promise((resolve) => {
|
|
104
|
-
fs__default['default'].unlink(filePath, () => {
|
|
105
|
-
resolve();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
12
|
+
function fileExists(filePath) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
fs__default['default'].access(filePath, (err) => resolve(!err));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function readFile(filePath) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
fs__default['default'].readFile(filePath, 'utf-8', (err, data) => {
|
|
20
|
+
if (err) {
|
|
21
|
+
reject(err);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
resolve(data);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function readFileBuffer(filePath) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
fs__default['default'].readFile(filePath, (err, data) => {
|
|
32
|
+
if (err) {
|
|
33
|
+
reject(err);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
resolve(data);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function writeFile(filePath, data) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
fs__default['default'].writeFile(filePath, data, (err) => {
|
|
44
|
+
if (err) {
|
|
45
|
+
reject(err);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function mkDir(filePath) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
fs__default['default'].mkdir(filePath, () => {
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function rmDir(filePath) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
fs__default['default'].rmdir(filePath, () => {
|
|
63
|
+
resolve();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function emptyDir(dir) {
|
|
68
|
+
const files = await readDir(dir);
|
|
69
|
+
const promises = files.map(async (fileName) => {
|
|
70
|
+
const filePath = path__default['default'].join(dir, fileName);
|
|
71
|
+
const isDirFile = await isFile(filePath);
|
|
72
|
+
if (isDirFile) {
|
|
73
|
+
await unlink(filePath);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
await Promise.all(promises);
|
|
77
|
+
}
|
|
78
|
+
async function readDir(dir) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
fs__default['default'].readdir(dir, (err, files) => {
|
|
81
|
+
if (err) {
|
|
82
|
+
resolve([]);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
resolve(files);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function isFile(itemPath) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
fs__default['default'].stat(itemPath, (err, stat) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
resolve(false);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
resolve(stat.isFile());
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function unlink(filePath) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
fs__default['default'].unlink(filePath, () => {
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
class ScreenshotConnector {
|
|
111
|
-
constructor() {
|
|
112
|
-
this.screenshotDirName = 'screenshot';
|
|
113
|
-
this.imagesDirName = 'images';
|
|
114
|
-
this.buildsDirName = 'builds';
|
|
115
|
-
this.masterBuildFileName = 'master.json';
|
|
116
|
-
this.screenshotCacheFileName = 'screenshot-cache.json';
|
|
117
|
-
}
|
|
118
|
-
async initBuild(opts) {
|
|
119
|
-
this.logger = opts.logger;
|
|
120
|
-
this.buildId = opts.buildId;
|
|
121
|
-
this.buildMessage = opts.buildMessage || '';
|
|
122
|
-
this.buildAuthor = opts.buildAuthor;
|
|
123
|
-
this.buildUrl = opts.buildUrl;
|
|
124
|
-
this.previewUrl = opts.previewUrl;
|
|
125
|
-
(this.buildTimestamp = typeof opts.buildTimestamp === 'number' ? opts.buildTimestamp : Date.now()),
|
|
126
|
-
(this.cacheDir = opts.cacheDir);
|
|
127
|
-
this.packageDir = opts.packageDir;
|
|
128
|
-
this.rootDir = opts.rootDir;
|
|
129
|
-
this.appNamespace = opts.appNamespace;
|
|
130
|
-
this.waitBeforeScreenshot = opts.waitBeforeScreenshot;
|
|
131
|
-
this.pixelmatchModulePath = opts.pixelmatchModulePath;
|
|
132
|
-
if (!opts.logger) {
|
|
133
|
-
throw new Error(`logger option required`);
|
|
134
|
-
}
|
|
135
|
-
if (typeof opts.buildId !== 'string') {
|
|
136
|
-
throw new Error(`buildId option required`);
|
|
137
|
-
}
|
|
138
|
-
if (typeof opts.cacheDir !== 'string') {
|
|
139
|
-
throw new Error(`cacheDir option required`);
|
|
140
|
-
}
|
|
141
|
-
if (typeof opts.packageDir !== 'string') {
|
|
142
|
-
throw new Error(`packageDir option required`);
|
|
143
|
-
}
|
|
144
|
-
if (typeof opts.rootDir !== 'string') {
|
|
145
|
-
throw new Error(`rootDir option required`);
|
|
146
|
-
}
|
|
147
|
-
this.updateMaster = !!opts.updateMaster;
|
|
148
|
-
this.allowableMismatchedPixels = opts.allowableMismatchedPixels;
|
|
149
|
-
this.allowableMismatchedRatio = opts.allowableMismatchedRatio;
|
|
150
|
-
this.pixelmatchThreshold = opts.pixelmatchThreshold;
|
|
151
|
-
this.logger.debug(`screenshot build: ${this.buildId}, ${this.buildMessage}, updateMaster: ${this.updateMaster}`);
|
|
152
|
-
this.logger.debug(`screenshot, allowableMismatchedPixels: ${this.allowableMismatchedPixels}, allowableMismatchedRatio: ${this.allowableMismatchedRatio}, pixelmatchThreshold: ${this.pixelmatchThreshold}`);
|
|
153
|
-
if (typeof opts.screenshotDirName === 'string') {
|
|
154
|
-
this.screenshotDirName = opts.screenshotDirName;
|
|
155
|
-
}
|
|
156
|
-
if (typeof opts.imagesDirName === 'string') {
|
|
157
|
-
this.imagesDirName = opts.imagesDirName;
|
|
158
|
-
}
|
|
159
|
-
if (typeof opts.buildsDirName === 'string') {
|
|
160
|
-
this.buildsDirName = opts.buildsDirName;
|
|
161
|
-
}
|
|
162
|
-
this.screenshotDir = path.join(this.rootDir, this.screenshotDirName);
|
|
163
|
-
this.imagesDir = path.join(this.screenshotDir, this.imagesDirName);
|
|
164
|
-
this.buildsDir = path.join(this.screenshotDir, this.buildsDirName);
|
|
165
|
-
this.masterBuildFilePath = path.join(this.buildsDir, this.masterBuildFileName);
|
|
166
|
-
this.screenshotCacheFilePath = path.join(this.cacheDir, this.screenshotCacheFileName);
|
|
167
|
-
this.currentBuildDir = path.join(os.tmpdir(), 'screenshot-build-' + this.buildId);
|
|
168
|
-
this.logger.debug(`screenshotDirPath: ${this.screenshotDir}`);
|
|
169
|
-
this.logger.debug(`imagesDirPath: ${this.imagesDir}`);
|
|
170
|
-
this.logger.debug(`buildsDirPath: ${this.buildsDir}`);
|
|
171
|
-
this.logger.debug(`currentBuildDir: ${this.currentBuildDir}`);
|
|
172
|
-
this.logger.debug(`cacheDir: ${this.cacheDir}`);
|
|
173
|
-
await mkDir(this.screenshotDir);
|
|
174
|
-
await Promise.all([
|
|
175
|
-
mkDir(this.imagesDir),
|
|
176
|
-
mkDir(this.buildsDir),
|
|
177
|
-
mkDir(this.currentBuildDir),
|
|
178
|
-
mkDir(this.cacheDir),
|
|
179
|
-
]);
|
|
180
|
-
}
|
|
181
|
-
async pullMasterBuild() {
|
|
182
|
-
/**/
|
|
183
|
-
}
|
|
184
|
-
async getMasterBuild() {
|
|
185
|
-
let masterBuild = null;
|
|
186
|
-
try {
|
|
187
|
-
masterBuild = JSON.parse(await readFile(this.masterBuildFilePath));
|
|
188
|
-
}
|
|
189
|
-
catch (e) { }
|
|
190
|
-
return masterBuild;
|
|
191
|
-
}
|
|
192
|
-
async completeBuild(masterBuild) {
|
|
193
|
-
const filePaths = (await readDir(this.currentBuildDir))
|
|
194
|
-
.map((f) => path.join(this.currentBuildDir, f))
|
|
195
|
-
.filter((f) => f.endsWith('.json'));
|
|
196
|
-
const screenshots = await Promise.all(filePaths.map(async (f) => JSON.parse(await readFile(f))));
|
|
197
|
-
this.sortScreenshots(screenshots);
|
|
198
|
-
if (!masterBuild) {
|
|
199
|
-
masterBuild = {
|
|
200
|
-
id: this.buildId,
|
|
201
|
-
message: this.buildMessage,
|
|
202
|
-
author: this.buildAuthor,
|
|
203
|
-
url: this.buildUrl,
|
|
204
|
-
previewUrl: this.previewUrl,
|
|
205
|
-
appNamespace: this.appNamespace,
|
|
206
|
-
timestamp: this.buildTimestamp,
|
|
207
|
-
screenshots: screenshots,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
const results = {
|
|
211
|
-
appNamespace: this.appNamespace,
|
|
212
|
-
masterBuild: masterBuild,
|
|
213
|
-
currentBuild: {
|
|
214
|
-
id: this.buildId,
|
|
215
|
-
message: this.buildMessage,
|
|
216
|
-
author: this.buildAuthor,
|
|
217
|
-
url: this.buildUrl,
|
|
218
|
-
previewUrl: this.previewUrl,
|
|
219
|
-
appNamespace: this.appNamespace,
|
|
220
|
-
timestamp: this.buildTimestamp,
|
|
221
|
-
screenshots: screenshots,
|
|
222
|
-
},
|
|
223
|
-
compare: {
|
|
224
|
-
id: `${masterBuild.id}-${this.buildId}`,
|
|
225
|
-
a: {
|
|
226
|
-
id: masterBuild.id,
|
|
227
|
-
message: masterBuild.message,
|
|
228
|
-
author: masterBuild.author,
|
|
229
|
-
url: masterBuild.url,
|
|
230
|
-
previewUrl: masterBuild.previewUrl,
|
|
231
|
-
},
|
|
232
|
-
b: {
|
|
233
|
-
id: this.buildId,
|
|
234
|
-
message: this.buildMessage,
|
|
235
|
-
author: this.buildAuthor,
|
|
236
|
-
url: this.buildUrl,
|
|
237
|
-
previewUrl: this.previewUrl,
|
|
238
|
-
},
|
|
239
|
-
url: null,
|
|
240
|
-
appNamespace: this.appNamespace,
|
|
241
|
-
timestamp: this.buildTimestamp,
|
|
242
|
-
diffs: [],
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
results.currentBuild.screenshots.forEach((screenshot) => {
|
|
246
|
-
screenshot.diff.device = screenshot.diff.device || screenshot.diff.userAgent;
|
|
247
|
-
results.compare.diffs.push(screenshot.diff);
|
|
248
|
-
delete screenshot.diff;
|
|
249
|
-
});
|
|
250
|
-
this.sortCompares(results.compare.diffs);
|
|
251
|
-
await emptyDir(this.currentBuildDir);
|
|
252
|
-
await rmDir(this.currentBuildDir);
|
|
253
|
-
return results;
|
|
254
|
-
}
|
|
255
|
-
async publishBuild(results) {
|
|
256
|
-
return results;
|
|
257
|
-
}
|
|
258
|
-
async generateJsonpDataUris(build) {
|
|
259
|
-
if (build && Array.isArray(build.screenshots)) {
|
|
260
|
-
for (let i = 0; i < build.screenshots.length; i++) {
|
|
261
|
-
const screenshot = build.screenshots[i];
|
|
262
|
-
const jsonpFileName = `screenshot_${screenshot.image}.js`;
|
|
263
|
-
const jsonFilePath = path.join(this.cacheDir, jsonpFileName);
|
|
264
|
-
const jsonpExists = await fileExists(jsonFilePath);
|
|
265
|
-
if (!jsonpExists) {
|
|
266
|
-
const imageFilePath = path.join(this.imagesDir, screenshot.image);
|
|
267
|
-
const imageBuf = await readFileBuffer(imageFilePath);
|
|
268
|
-
const jsonpContent = `loadScreenshot("${screenshot.image}","data:image/png;base64,${imageBuf.toString('base64')}");`;
|
|
269
|
-
await writeFile(jsonFilePath, jsonpContent);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async getScreenshotCache() {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
async updateScreenshotCache(screenshotCache, buildResults) {
|
|
278
|
-
screenshotCache = screenshotCache || {};
|
|
279
|
-
screenshotCache.timestamp = this.buildTimestamp;
|
|
280
|
-
screenshotCache.lastBuildId = this.buildId;
|
|
281
|
-
screenshotCache.size = 0;
|
|
282
|
-
screenshotCache.items = screenshotCache.items || [];
|
|
283
|
-
if (buildResults && buildResults.compare && Array.isArray(buildResults.compare.diffs)) {
|
|
284
|
-
buildResults.compare.diffs.forEach((diff) => {
|
|
285
|
-
if (typeof diff.cacheKey !== 'string') {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (diff.imageA === diff.imageB) {
|
|
289
|
-
// no need to cache identical matches
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
const existingItem = screenshotCache.items.find((i) => i.key === diff.cacheKey);
|
|
293
|
-
if (existingItem) {
|
|
294
|
-
// already have this cached, but update its timestamp
|
|
295
|
-
existingItem.ts = this.buildTimestamp;
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
// add this item to the cache
|
|
299
|
-
screenshotCache.items.push({
|
|
300
|
-
key: diff.cacheKey,
|
|
301
|
-
ts: this.buildTimestamp,
|
|
302
|
-
mp: diff.mismatchedPixels,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
// sort so the newest items are on top
|
|
308
|
-
screenshotCache.items.sort((a, b) => {
|
|
309
|
-
if (a.ts > b.ts)
|
|
310
|
-
return -1;
|
|
311
|
-
if (a.ts < b.ts)
|
|
312
|
-
return 1;
|
|
313
|
-
if (a.mp > b.mp)
|
|
314
|
-
return -1;
|
|
315
|
-
if (a.mp < b.mp)
|
|
316
|
-
return 1;
|
|
317
|
-
return 0;
|
|
318
|
-
});
|
|
319
|
-
// keep only the most recent items
|
|
320
|
-
screenshotCache.items = screenshotCache.items.slice(0, 1000);
|
|
321
|
-
screenshotCache.size = screenshotCache.items.length;
|
|
322
|
-
return screenshotCache;
|
|
323
|
-
}
|
|
324
|
-
toJson(masterBuild, screenshotCache) {
|
|
325
|
-
const masterScreenshots = {};
|
|
326
|
-
if (masterBuild && Array.isArray(masterBuild.screenshots)) {
|
|
327
|
-
masterBuild.screenshots.forEach((masterScreenshot) => {
|
|
328
|
-
masterScreenshots[masterScreenshot.id] = masterScreenshot.image;
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
const mismatchCache = {};
|
|
332
|
-
if (screenshotCache && Array.isArray(screenshotCache.items)) {
|
|
333
|
-
screenshotCache.items.forEach((cacheItem) => {
|
|
334
|
-
mismatchCache[cacheItem.key] = cacheItem.mp;
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
const screenshotBuild = {
|
|
338
|
-
buildId: this.buildId,
|
|
339
|
-
rootDir: this.rootDir,
|
|
340
|
-
screenshotDir: this.screenshotDir,
|
|
341
|
-
imagesDir: this.imagesDir,
|
|
342
|
-
buildsDir: this.buildsDir,
|
|
343
|
-
masterScreenshots: masterScreenshots,
|
|
344
|
-
cache: mismatchCache,
|
|
345
|
-
currentBuildDir: this.currentBuildDir,
|
|
346
|
-
updateMaster: this.updateMaster,
|
|
347
|
-
allowableMismatchedPixels: this.allowableMismatchedPixels,
|
|
348
|
-
allowableMismatchedRatio: this.allowableMismatchedRatio,
|
|
349
|
-
pixelmatchThreshold: this.pixelmatchThreshold,
|
|
350
|
-
timeoutBeforeScreenshot: this.waitBeforeScreenshot,
|
|
351
|
-
pixelmatchModulePath: this.pixelmatchModulePath,
|
|
352
|
-
};
|
|
353
|
-
return JSON.stringify(screenshotBuild);
|
|
354
|
-
}
|
|
355
|
-
sortScreenshots(screenshots) {
|
|
356
|
-
return screenshots.sort((a, b) => {
|
|
357
|
-
if (a.desc && b.desc) {
|
|
358
|
-
if (a.desc.toLowerCase() < b.desc.toLowerCase())
|
|
359
|
-
return -1;
|
|
360
|
-
if (a.desc.toLowerCase() > b.desc.toLowerCase())
|
|
361
|
-
return 1;
|
|
362
|
-
}
|
|
363
|
-
if (a.device && b.device) {
|
|
364
|
-
if (a.device.toLowerCase() < b.device.toLowerCase())
|
|
365
|
-
return -1;
|
|
366
|
-
if (a.device.toLowerCase() > b.device.toLowerCase())
|
|
367
|
-
return 1;
|
|
368
|
-
}
|
|
369
|
-
if (a.userAgent && b.userAgent) {
|
|
370
|
-
if (a.userAgent.toLowerCase() < b.userAgent.toLowerCase())
|
|
371
|
-
return -1;
|
|
372
|
-
if (a.userAgent.toLowerCase() > b.userAgent.toLowerCase())
|
|
373
|
-
return 1;
|
|
374
|
-
}
|
|
375
|
-
if (a.width < b.width)
|
|
376
|
-
return -1;
|
|
377
|
-
if (a.width > b.width)
|
|
378
|
-
return 1;
|
|
379
|
-
if (a.height < b.height)
|
|
380
|
-
return -1;
|
|
381
|
-
if (a.height > b.height)
|
|
382
|
-
return 1;
|
|
383
|
-
if (a.id < b.id)
|
|
384
|
-
return -1;
|
|
385
|
-
if (a.id > b.id)
|
|
386
|
-
return 1;
|
|
387
|
-
return 0;
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
sortCompares(compares) {
|
|
391
|
-
return compares.sort((a, b) => {
|
|
392
|
-
if (a.allowableMismatchedPixels > b.allowableMismatchedPixels)
|
|
393
|
-
return -1;
|
|
394
|
-
if (a.allowableMismatchedPixels < b.allowableMismatchedPixels)
|
|
395
|
-
return 1;
|
|
396
|
-
if (a.allowableMismatchedRatio > b.allowableMismatchedRatio)
|
|
397
|
-
return -1;
|
|
398
|
-
if (a.allowableMismatchedRatio < b.allowableMismatchedRatio)
|
|
399
|
-
return 1;
|
|
400
|
-
if (a.desc && b.desc) {
|
|
401
|
-
if (a.desc.toLowerCase() < b.desc.toLowerCase())
|
|
402
|
-
return -1;
|
|
403
|
-
if (a.desc.toLowerCase() > b.desc.toLowerCase())
|
|
404
|
-
return 1;
|
|
405
|
-
}
|
|
406
|
-
if (a.device && b.device) {
|
|
407
|
-
if (a.device.toLowerCase() < b.device.toLowerCase())
|
|
408
|
-
return -1;
|
|
409
|
-
if (a.device.toLowerCase() > b.device.toLowerCase())
|
|
410
|
-
return 1;
|
|
411
|
-
}
|
|
412
|
-
if (a.userAgent && b.userAgent) {
|
|
413
|
-
if (a.userAgent.toLowerCase() < b.userAgent.toLowerCase())
|
|
414
|
-
return -1;
|
|
415
|
-
if (a.userAgent.toLowerCase() > b.userAgent.toLowerCase())
|
|
416
|
-
return 1;
|
|
417
|
-
}
|
|
418
|
-
if (a.width < b.width)
|
|
419
|
-
return -1;
|
|
420
|
-
if (a.width > b.width)
|
|
421
|
-
return 1;
|
|
422
|
-
if (a.height < b.height)
|
|
423
|
-
return -1;
|
|
424
|
-
if (a.height > b.height)
|
|
425
|
-
return 1;
|
|
426
|
-
if (a.id < b.id)
|
|
427
|
-
return -1;
|
|
428
|
-
if (a.id > b.id)
|
|
429
|
-
return 1;
|
|
430
|
-
return 0;
|
|
431
|
-
});
|
|
432
|
-
}
|
|
110
|
+
class ScreenshotConnector {
|
|
111
|
+
constructor() {
|
|
112
|
+
this.screenshotDirName = 'screenshot';
|
|
113
|
+
this.imagesDirName = 'images';
|
|
114
|
+
this.buildsDirName = 'builds';
|
|
115
|
+
this.masterBuildFileName = 'master.json';
|
|
116
|
+
this.screenshotCacheFileName = 'screenshot-cache.json';
|
|
117
|
+
}
|
|
118
|
+
async initBuild(opts) {
|
|
119
|
+
this.logger = opts.logger;
|
|
120
|
+
this.buildId = opts.buildId;
|
|
121
|
+
this.buildMessage = opts.buildMessage || '';
|
|
122
|
+
this.buildAuthor = opts.buildAuthor;
|
|
123
|
+
this.buildUrl = opts.buildUrl;
|
|
124
|
+
this.previewUrl = opts.previewUrl;
|
|
125
|
+
(this.buildTimestamp = typeof opts.buildTimestamp === 'number' ? opts.buildTimestamp : Date.now()),
|
|
126
|
+
(this.cacheDir = opts.cacheDir);
|
|
127
|
+
this.packageDir = opts.packageDir;
|
|
128
|
+
this.rootDir = opts.rootDir;
|
|
129
|
+
this.appNamespace = opts.appNamespace;
|
|
130
|
+
this.waitBeforeScreenshot = opts.waitBeforeScreenshot;
|
|
131
|
+
this.pixelmatchModulePath = opts.pixelmatchModulePath;
|
|
132
|
+
if (!opts.logger) {
|
|
133
|
+
throw new Error(`logger option required`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof opts.buildId !== 'string') {
|
|
136
|
+
throw new Error(`buildId option required`);
|
|
137
|
+
}
|
|
138
|
+
if (typeof opts.cacheDir !== 'string') {
|
|
139
|
+
throw new Error(`cacheDir option required`);
|
|
140
|
+
}
|
|
141
|
+
if (typeof opts.packageDir !== 'string') {
|
|
142
|
+
throw new Error(`packageDir option required`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof opts.rootDir !== 'string') {
|
|
145
|
+
throw new Error(`rootDir option required`);
|
|
146
|
+
}
|
|
147
|
+
this.updateMaster = !!opts.updateMaster;
|
|
148
|
+
this.allowableMismatchedPixels = opts.allowableMismatchedPixels;
|
|
149
|
+
this.allowableMismatchedRatio = opts.allowableMismatchedRatio;
|
|
150
|
+
this.pixelmatchThreshold = opts.pixelmatchThreshold;
|
|
151
|
+
this.logger.debug(`screenshot build: ${this.buildId}, ${this.buildMessage}, updateMaster: ${this.updateMaster}`);
|
|
152
|
+
this.logger.debug(`screenshot, allowableMismatchedPixels: ${this.allowableMismatchedPixels}, allowableMismatchedRatio: ${this.allowableMismatchedRatio}, pixelmatchThreshold: ${this.pixelmatchThreshold}`);
|
|
153
|
+
if (typeof opts.screenshotDirName === 'string') {
|
|
154
|
+
this.screenshotDirName = opts.screenshotDirName;
|
|
155
|
+
}
|
|
156
|
+
if (typeof opts.imagesDirName === 'string') {
|
|
157
|
+
this.imagesDirName = opts.imagesDirName;
|
|
158
|
+
}
|
|
159
|
+
if (typeof opts.buildsDirName === 'string') {
|
|
160
|
+
this.buildsDirName = opts.buildsDirName;
|
|
161
|
+
}
|
|
162
|
+
this.screenshotDir = path.join(this.rootDir, this.screenshotDirName);
|
|
163
|
+
this.imagesDir = path.join(this.screenshotDir, this.imagesDirName);
|
|
164
|
+
this.buildsDir = path.join(this.screenshotDir, this.buildsDirName);
|
|
165
|
+
this.masterBuildFilePath = path.join(this.buildsDir, this.masterBuildFileName);
|
|
166
|
+
this.screenshotCacheFilePath = path.join(this.cacheDir, this.screenshotCacheFileName);
|
|
167
|
+
this.currentBuildDir = path.join(os.tmpdir(), 'screenshot-build-' + this.buildId);
|
|
168
|
+
this.logger.debug(`screenshotDirPath: ${this.screenshotDir}`);
|
|
169
|
+
this.logger.debug(`imagesDirPath: ${this.imagesDir}`);
|
|
170
|
+
this.logger.debug(`buildsDirPath: ${this.buildsDir}`);
|
|
171
|
+
this.logger.debug(`currentBuildDir: ${this.currentBuildDir}`);
|
|
172
|
+
this.logger.debug(`cacheDir: ${this.cacheDir}`);
|
|
173
|
+
await mkDir(this.screenshotDir);
|
|
174
|
+
await Promise.all([
|
|
175
|
+
mkDir(this.imagesDir),
|
|
176
|
+
mkDir(this.buildsDir),
|
|
177
|
+
mkDir(this.currentBuildDir),
|
|
178
|
+
mkDir(this.cacheDir),
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
async pullMasterBuild() {
|
|
182
|
+
/**/
|
|
183
|
+
}
|
|
184
|
+
async getMasterBuild() {
|
|
185
|
+
let masterBuild = null;
|
|
186
|
+
try {
|
|
187
|
+
masterBuild = JSON.parse(await readFile(this.masterBuildFilePath));
|
|
188
|
+
}
|
|
189
|
+
catch (e) { }
|
|
190
|
+
return masterBuild;
|
|
191
|
+
}
|
|
192
|
+
async completeBuild(masterBuild) {
|
|
193
|
+
const filePaths = (await readDir(this.currentBuildDir))
|
|
194
|
+
.map((f) => path.join(this.currentBuildDir, f))
|
|
195
|
+
.filter((f) => f.endsWith('.json'));
|
|
196
|
+
const screenshots = await Promise.all(filePaths.map(async (f) => JSON.parse(await readFile(f))));
|
|
197
|
+
this.sortScreenshots(screenshots);
|
|
198
|
+
if (!masterBuild) {
|
|
199
|
+
masterBuild = {
|
|
200
|
+
id: this.buildId,
|
|
201
|
+
message: this.buildMessage,
|
|
202
|
+
author: this.buildAuthor,
|
|
203
|
+
url: this.buildUrl,
|
|
204
|
+
previewUrl: this.previewUrl,
|
|
205
|
+
appNamespace: this.appNamespace,
|
|
206
|
+
timestamp: this.buildTimestamp,
|
|
207
|
+
screenshots: screenshots,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const results = {
|
|
211
|
+
appNamespace: this.appNamespace,
|
|
212
|
+
masterBuild: masterBuild,
|
|
213
|
+
currentBuild: {
|
|
214
|
+
id: this.buildId,
|
|
215
|
+
message: this.buildMessage,
|
|
216
|
+
author: this.buildAuthor,
|
|
217
|
+
url: this.buildUrl,
|
|
218
|
+
previewUrl: this.previewUrl,
|
|
219
|
+
appNamespace: this.appNamespace,
|
|
220
|
+
timestamp: this.buildTimestamp,
|
|
221
|
+
screenshots: screenshots,
|
|
222
|
+
},
|
|
223
|
+
compare: {
|
|
224
|
+
id: `${masterBuild.id}-${this.buildId}`,
|
|
225
|
+
a: {
|
|
226
|
+
id: masterBuild.id,
|
|
227
|
+
message: masterBuild.message,
|
|
228
|
+
author: masterBuild.author,
|
|
229
|
+
url: masterBuild.url,
|
|
230
|
+
previewUrl: masterBuild.previewUrl,
|
|
231
|
+
},
|
|
232
|
+
b: {
|
|
233
|
+
id: this.buildId,
|
|
234
|
+
message: this.buildMessage,
|
|
235
|
+
author: this.buildAuthor,
|
|
236
|
+
url: this.buildUrl,
|
|
237
|
+
previewUrl: this.previewUrl,
|
|
238
|
+
},
|
|
239
|
+
url: null,
|
|
240
|
+
appNamespace: this.appNamespace,
|
|
241
|
+
timestamp: this.buildTimestamp,
|
|
242
|
+
diffs: [],
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
results.currentBuild.screenshots.forEach((screenshot) => {
|
|
246
|
+
screenshot.diff.device = screenshot.diff.device || screenshot.diff.userAgent;
|
|
247
|
+
results.compare.diffs.push(screenshot.diff);
|
|
248
|
+
delete screenshot.diff;
|
|
249
|
+
});
|
|
250
|
+
this.sortCompares(results.compare.diffs);
|
|
251
|
+
await emptyDir(this.currentBuildDir);
|
|
252
|
+
await rmDir(this.currentBuildDir);
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
async publishBuild(results) {
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
async generateJsonpDataUris(build) {
|
|
259
|
+
if (build && Array.isArray(build.screenshots)) {
|
|
260
|
+
for (let i = 0; i < build.screenshots.length; i++) {
|
|
261
|
+
const screenshot = build.screenshots[i];
|
|
262
|
+
const jsonpFileName = `screenshot_${screenshot.image}.js`;
|
|
263
|
+
const jsonFilePath = path.join(this.cacheDir, jsonpFileName);
|
|
264
|
+
const jsonpExists = await fileExists(jsonFilePath);
|
|
265
|
+
if (!jsonpExists) {
|
|
266
|
+
const imageFilePath = path.join(this.imagesDir, screenshot.image);
|
|
267
|
+
const imageBuf = await readFileBuffer(imageFilePath);
|
|
268
|
+
const jsonpContent = `loadScreenshot("${screenshot.image}","data:image/png;base64,${imageBuf.toString('base64')}");`;
|
|
269
|
+
await writeFile(jsonFilePath, jsonpContent);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async getScreenshotCache() {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
async updateScreenshotCache(screenshotCache, buildResults) {
|
|
278
|
+
screenshotCache = screenshotCache || {};
|
|
279
|
+
screenshotCache.timestamp = this.buildTimestamp;
|
|
280
|
+
screenshotCache.lastBuildId = this.buildId;
|
|
281
|
+
screenshotCache.size = 0;
|
|
282
|
+
screenshotCache.items = screenshotCache.items || [];
|
|
283
|
+
if (buildResults && buildResults.compare && Array.isArray(buildResults.compare.diffs)) {
|
|
284
|
+
buildResults.compare.diffs.forEach((diff) => {
|
|
285
|
+
if (typeof diff.cacheKey !== 'string') {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (diff.imageA === diff.imageB) {
|
|
289
|
+
// no need to cache identical matches
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const existingItem = screenshotCache.items.find((i) => i.key === diff.cacheKey);
|
|
293
|
+
if (existingItem) {
|
|
294
|
+
// already have this cached, but update its timestamp
|
|
295
|
+
existingItem.ts = this.buildTimestamp;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// add this item to the cache
|
|
299
|
+
screenshotCache.items.push({
|
|
300
|
+
key: diff.cacheKey,
|
|
301
|
+
ts: this.buildTimestamp,
|
|
302
|
+
mp: diff.mismatchedPixels,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// sort so the newest items are on top
|
|
308
|
+
screenshotCache.items.sort((a, b) => {
|
|
309
|
+
if (a.ts > b.ts)
|
|
310
|
+
return -1;
|
|
311
|
+
if (a.ts < b.ts)
|
|
312
|
+
return 1;
|
|
313
|
+
if (a.mp > b.mp)
|
|
314
|
+
return -1;
|
|
315
|
+
if (a.mp < b.mp)
|
|
316
|
+
return 1;
|
|
317
|
+
return 0;
|
|
318
|
+
});
|
|
319
|
+
// keep only the most recent items
|
|
320
|
+
screenshotCache.items = screenshotCache.items.slice(0, 1000);
|
|
321
|
+
screenshotCache.size = screenshotCache.items.length;
|
|
322
|
+
return screenshotCache;
|
|
323
|
+
}
|
|
324
|
+
toJson(masterBuild, screenshotCache) {
|
|
325
|
+
const masterScreenshots = {};
|
|
326
|
+
if (masterBuild && Array.isArray(masterBuild.screenshots)) {
|
|
327
|
+
masterBuild.screenshots.forEach((masterScreenshot) => {
|
|
328
|
+
masterScreenshots[masterScreenshot.id] = masterScreenshot.image;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const mismatchCache = {};
|
|
332
|
+
if (screenshotCache && Array.isArray(screenshotCache.items)) {
|
|
333
|
+
screenshotCache.items.forEach((cacheItem) => {
|
|
334
|
+
mismatchCache[cacheItem.key] = cacheItem.mp;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const screenshotBuild = {
|
|
338
|
+
buildId: this.buildId,
|
|
339
|
+
rootDir: this.rootDir,
|
|
340
|
+
screenshotDir: this.screenshotDir,
|
|
341
|
+
imagesDir: this.imagesDir,
|
|
342
|
+
buildsDir: this.buildsDir,
|
|
343
|
+
masterScreenshots: masterScreenshots,
|
|
344
|
+
cache: mismatchCache,
|
|
345
|
+
currentBuildDir: this.currentBuildDir,
|
|
346
|
+
updateMaster: this.updateMaster,
|
|
347
|
+
allowableMismatchedPixels: this.allowableMismatchedPixels,
|
|
348
|
+
allowableMismatchedRatio: this.allowableMismatchedRatio,
|
|
349
|
+
pixelmatchThreshold: this.pixelmatchThreshold,
|
|
350
|
+
timeoutBeforeScreenshot: this.waitBeforeScreenshot,
|
|
351
|
+
pixelmatchModulePath: this.pixelmatchModulePath,
|
|
352
|
+
};
|
|
353
|
+
return JSON.stringify(screenshotBuild);
|
|
354
|
+
}
|
|
355
|
+
sortScreenshots(screenshots) {
|
|
356
|
+
return screenshots.sort((a, b) => {
|
|
357
|
+
if (a.desc && b.desc) {
|
|
358
|
+
if (a.desc.toLowerCase() < b.desc.toLowerCase())
|
|
359
|
+
return -1;
|
|
360
|
+
if (a.desc.toLowerCase() > b.desc.toLowerCase())
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
if (a.device && b.device) {
|
|
364
|
+
if (a.device.toLowerCase() < b.device.toLowerCase())
|
|
365
|
+
return -1;
|
|
366
|
+
if (a.device.toLowerCase() > b.device.toLowerCase())
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
if (a.userAgent && b.userAgent) {
|
|
370
|
+
if (a.userAgent.toLowerCase() < b.userAgent.toLowerCase())
|
|
371
|
+
return -1;
|
|
372
|
+
if (a.userAgent.toLowerCase() > b.userAgent.toLowerCase())
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
if (a.width < b.width)
|
|
376
|
+
return -1;
|
|
377
|
+
if (a.width > b.width)
|
|
378
|
+
return 1;
|
|
379
|
+
if (a.height < b.height)
|
|
380
|
+
return -1;
|
|
381
|
+
if (a.height > b.height)
|
|
382
|
+
return 1;
|
|
383
|
+
if (a.id < b.id)
|
|
384
|
+
return -1;
|
|
385
|
+
if (a.id > b.id)
|
|
386
|
+
return 1;
|
|
387
|
+
return 0;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
sortCompares(compares) {
|
|
391
|
+
return compares.sort((a, b) => {
|
|
392
|
+
if (a.allowableMismatchedPixels > b.allowableMismatchedPixels)
|
|
393
|
+
return -1;
|
|
394
|
+
if (a.allowableMismatchedPixels < b.allowableMismatchedPixels)
|
|
395
|
+
return 1;
|
|
396
|
+
if (a.allowableMismatchedRatio > b.allowableMismatchedRatio)
|
|
397
|
+
return -1;
|
|
398
|
+
if (a.allowableMismatchedRatio < b.allowableMismatchedRatio)
|
|
399
|
+
return 1;
|
|
400
|
+
if (a.desc && b.desc) {
|
|
401
|
+
if (a.desc.toLowerCase() < b.desc.toLowerCase())
|
|
402
|
+
return -1;
|
|
403
|
+
if (a.desc.toLowerCase() > b.desc.toLowerCase())
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
if (a.device && b.device) {
|
|
407
|
+
if (a.device.toLowerCase() < b.device.toLowerCase())
|
|
408
|
+
return -1;
|
|
409
|
+
if (a.device.toLowerCase() > b.device.toLowerCase())
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
if (a.userAgent && b.userAgent) {
|
|
413
|
+
if (a.userAgent.toLowerCase() < b.userAgent.toLowerCase())
|
|
414
|
+
return -1;
|
|
415
|
+
if (a.userAgent.toLowerCase() > b.userAgent.toLowerCase())
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
if (a.width < b.width)
|
|
419
|
+
return -1;
|
|
420
|
+
if (a.width > b.width)
|
|
421
|
+
return 1;
|
|
422
|
+
if (a.height < b.height)
|
|
423
|
+
return -1;
|
|
424
|
+
if (a.height > b.height)
|
|
425
|
+
return 1;
|
|
426
|
+
if (a.id < b.id)
|
|
427
|
+
return -1;
|
|
428
|
+
if (a.id > b.id)
|
|
429
|
+
return 1;
|
|
430
|
+
return 0;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
/**
|
|
436
|
-
* Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
|
|
437
|
-
* Forward-slash paths can be used in Windows as long as they're not
|
|
438
|
-
* extended-length paths and don't contain any non-ascii characters.
|
|
439
|
-
* This was created since the path methods in Node.js outputs \\ paths on Windows.
|
|
440
|
-
* @param path the Windows-based path to convert
|
|
441
|
-
* @returns the converted path
|
|
442
|
-
*/
|
|
443
|
-
const normalizePath = (path) => {
|
|
444
|
-
if (typeof path !== 'string') {
|
|
445
|
-
throw new Error(`invalid path to normalize`);
|
|
446
|
-
}
|
|
447
|
-
path = normalizeSlashes(path.trim());
|
|
448
|
-
const components = pathComponents(path, getRootLength(path));
|
|
449
|
-
const reducedComponents = reducePathComponents(components);
|
|
450
|
-
const rootPart = reducedComponents[0];
|
|
451
|
-
const secondPart = reducedComponents[1];
|
|
452
|
-
const normalized = rootPart + reducedComponents.slice(1).join('/');
|
|
453
|
-
if (normalized === '') {
|
|
454
|
-
return '.';
|
|
455
|
-
}
|
|
456
|
-
if (rootPart === '' &&
|
|
457
|
-
secondPart &&
|
|
458
|
-
path.includes('/') &&
|
|
459
|
-
!secondPart.startsWith('.') &&
|
|
460
|
-
!secondPart.startsWith('@')) {
|
|
461
|
-
return './' + normalized;
|
|
462
|
-
}
|
|
463
|
-
return normalized;
|
|
464
|
-
};
|
|
465
|
-
const normalizeSlashes = (path) => path.replace(backslashRegExp, '/');
|
|
466
|
-
const altDirectorySeparator = '\\';
|
|
467
|
-
const urlSchemeSeparator = '://';
|
|
468
|
-
const backslashRegExp = /\\/g;
|
|
469
|
-
const reducePathComponents = (components) => {
|
|
470
|
-
if (!Array.isArray(components) || components.length === 0) {
|
|
471
|
-
return [];
|
|
472
|
-
}
|
|
473
|
-
const reduced = [components[0]];
|
|
474
|
-
for (let i = 1; i < components.length; i++) {
|
|
475
|
-
const component = components[i];
|
|
476
|
-
if (!component)
|
|
477
|
-
continue;
|
|
478
|
-
if (component === '.')
|
|
479
|
-
continue;
|
|
480
|
-
if (component === '..') {
|
|
481
|
-
if (reduced.length > 1) {
|
|
482
|
-
if (reduced[reduced.length - 1] !== '..') {
|
|
483
|
-
reduced.pop();
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
else if (reduced[0])
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
reduced.push(component);
|
|
491
|
-
}
|
|
492
|
-
return reduced;
|
|
493
|
-
};
|
|
494
|
-
const getRootLength = (path) => {
|
|
495
|
-
const rootLength = getEncodedRootLength(path);
|
|
496
|
-
return rootLength < 0 ? ~rootLength : rootLength;
|
|
497
|
-
};
|
|
498
|
-
const getEncodedRootLength = (path) => {
|
|
499
|
-
if (!path)
|
|
500
|
-
return 0;
|
|
501
|
-
const ch0 = path.charCodeAt(0);
|
|
502
|
-
// POSIX or UNC
|
|
503
|
-
if (ch0 === 47 /* CharacterCodes.slash */ || ch0 === 92 /* CharacterCodes.backslash */) {
|
|
504
|
-
if (path.charCodeAt(1) !== ch0)
|
|
505
|
-
return 1; // POSIX: "/" (or non-normalized "\")
|
|
506
|
-
const p1 = path.indexOf(ch0 === 47 /* CharacterCodes.slash */ ? '/' : altDirectorySeparator, 2);
|
|
507
|
-
if (p1 < 0)
|
|
508
|
-
return path.length; // UNC: "//server" or "\\server"
|
|
509
|
-
return p1 + 1; // UNC: "//server/" or "\\server\"
|
|
510
|
-
}
|
|
511
|
-
// DOS
|
|
512
|
-
if (isVolumeCharacter(ch0) && path.charCodeAt(1) === 58 /* CharacterCodes.colon */) {
|
|
513
|
-
const ch2 = path.charCodeAt(2);
|
|
514
|
-
if (ch2 === 47 /* CharacterCodes.slash */ || ch2 === 92 /* CharacterCodes.backslash */)
|
|
515
|
-
return 3; // DOS: "c:/" or "c:\"
|
|
516
|
-
if (path.length === 2)
|
|
517
|
-
return 2; // DOS: "c:" (but not "c:d")
|
|
518
|
-
}
|
|
519
|
-
// URL
|
|
520
|
-
const schemeEnd = path.indexOf(urlSchemeSeparator);
|
|
521
|
-
if (schemeEnd !== -1) {
|
|
522
|
-
const authorityStart = schemeEnd + urlSchemeSeparator.length;
|
|
523
|
-
const authorityEnd = path.indexOf('/', authorityStart);
|
|
524
|
-
if (authorityEnd !== -1) {
|
|
525
|
-
// URL: "file:///", "file://server/", "file://server/path"
|
|
526
|
-
// For local "file" URLs, include the leading DOS volume (if present).
|
|
527
|
-
// Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
|
|
528
|
-
// special case interpreted as "the machine from which the URL is being interpreted".
|
|
529
|
-
const scheme = path.slice(0, schemeEnd);
|
|
530
|
-
const authority = path.slice(authorityStart, authorityEnd);
|
|
531
|
-
if (scheme === 'file' &&
|
|
532
|
-
(authority === '' || authority === 'localhost') &&
|
|
533
|
-
isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {
|
|
534
|
-
const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
|
|
535
|
-
if (volumeSeparatorEnd !== -1) {
|
|
536
|
-
if (path.charCodeAt(volumeSeparatorEnd) === 47 /* CharacterCodes.slash */) {
|
|
537
|
-
// URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
|
|
538
|
-
return ~(volumeSeparatorEnd + 1);
|
|
539
|
-
}
|
|
540
|
-
if (volumeSeparatorEnd === path.length) {
|
|
541
|
-
// URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
|
|
542
|
-
// but not "file:///c:d" or "file:///c%3ad"
|
|
543
|
-
return ~volumeSeparatorEnd;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"
|
|
548
|
-
}
|
|
549
|
-
return ~path.length; // URL: "file://server", "http://server"
|
|
550
|
-
}
|
|
551
|
-
// relative
|
|
552
|
-
return 0;
|
|
553
|
-
};
|
|
554
|
-
const isVolumeCharacter = (charCode) => (charCode >= 97 /* CharacterCodes.a */ && charCode <= 122 /* CharacterCodes.z */) ||
|
|
555
|
-
(charCode >= 65 /* CharacterCodes.A */ && charCode <= 90 /* CharacterCodes.Z */);
|
|
556
|
-
const getFileUrlVolumeSeparatorEnd = (url, start) => {
|
|
557
|
-
const ch0 = url.charCodeAt(start);
|
|
558
|
-
if (ch0 === 58 /* CharacterCodes.colon */)
|
|
559
|
-
return start + 1;
|
|
560
|
-
if (ch0 === 37 /* CharacterCodes.percent */ && url.charCodeAt(start + 1) === 51 /* CharacterCodes._3 */) {
|
|
561
|
-
const ch2 = url.charCodeAt(start + 2);
|
|
562
|
-
if (ch2 === 97 /* CharacterCodes.a */ || ch2 === 65 /* CharacterCodes.A */)
|
|
563
|
-
return start + 3;
|
|
564
|
-
}
|
|
565
|
-
return -1;
|
|
566
|
-
};
|
|
567
|
-
const pathComponents = (path, rootLength) => {
|
|
568
|
-
const root = path.substring(0, rootLength);
|
|
569
|
-
const rest = path.substring(rootLength).split('/');
|
|
570
|
-
const restLen = rest.length;
|
|
571
|
-
if (restLen > 0 && !rest[restLen - 1]) {
|
|
572
|
-
rest.pop();
|
|
573
|
-
}
|
|
574
|
-
return [root, ...rest];
|
|
435
|
+
/**
|
|
436
|
+
* Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
|
|
437
|
+
* Forward-slash paths can be used in Windows as long as they're not
|
|
438
|
+
* extended-length paths and don't contain any non-ascii characters.
|
|
439
|
+
* This was created since the path methods in Node.js outputs \\ paths on Windows.
|
|
440
|
+
* @param path the Windows-based path to convert
|
|
441
|
+
* @returns the converted path
|
|
442
|
+
*/
|
|
443
|
+
const normalizePath = (path) => {
|
|
444
|
+
if (typeof path !== 'string') {
|
|
445
|
+
throw new Error(`invalid path to normalize`);
|
|
446
|
+
}
|
|
447
|
+
path = normalizeSlashes(path.trim());
|
|
448
|
+
const components = pathComponents(path, getRootLength(path));
|
|
449
|
+
const reducedComponents = reducePathComponents(components);
|
|
450
|
+
const rootPart = reducedComponents[0];
|
|
451
|
+
const secondPart = reducedComponents[1];
|
|
452
|
+
const normalized = rootPart + reducedComponents.slice(1).join('/');
|
|
453
|
+
if (normalized === '') {
|
|
454
|
+
return '.';
|
|
455
|
+
}
|
|
456
|
+
if (rootPart === '' &&
|
|
457
|
+
secondPart &&
|
|
458
|
+
path.includes('/') &&
|
|
459
|
+
!secondPart.startsWith('.') &&
|
|
460
|
+
!secondPart.startsWith('@')) {
|
|
461
|
+
return './' + normalized;
|
|
462
|
+
}
|
|
463
|
+
return normalized;
|
|
464
|
+
};
|
|
465
|
+
const normalizeSlashes = (path) => path.replace(backslashRegExp, '/');
|
|
466
|
+
const altDirectorySeparator = '\\';
|
|
467
|
+
const urlSchemeSeparator = '://';
|
|
468
|
+
const backslashRegExp = /\\/g;
|
|
469
|
+
const reducePathComponents = (components) => {
|
|
470
|
+
if (!Array.isArray(components) || components.length === 0) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
const reduced = [components[0]];
|
|
474
|
+
for (let i = 1; i < components.length; i++) {
|
|
475
|
+
const component = components[i];
|
|
476
|
+
if (!component)
|
|
477
|
+
continue;
|
|
478
|
+
if (component === '.')
|
|
479
|
+
continue;
|
|
480
|
+
if (component === '..') {
|
|
481
|
+
if (reduced.length > 1) {
|
|
482
|
+
if (reduced[reduced.length - 1] !== '..') {
|
|
483
|
+
reduced.pop();
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else if (reduced[0])
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
reduced.push(component);
|
|
491
|
+
}
|
|
492
|
+
return reduced;
|
|
493
|
+
};
|
|
494
|
+
const getRootLength = (path) => {
|
|
495
|
+
const rootLength = getEncodedRootLength(path);
|
|
496
|
+
return rootLength < 0 ? ~rootLength : rootLength;
|
|
497
|
+
};
|
|
498
|
+
const getEncodedRootLength = (path) => {
|
|
499
|
+
if (!path)
|
|
500
|
+
return 0;
|
|
501
|
+
const ch0 = path.charCodeAt(0);
|
|
502
|
+
// POSIX or UNC
|
|
503
|
+
if (ch0 === 47 /* CharacterCodes.slash */ || ch0 === 92 /* CharacterCodes.backslash */) {
|
|
504
|
+
if (path.charCodeAt(1) !== ch0)
|
|
505
|
+
return 1; // POSIX: "/" (or non-normalized "\")
|
|
506
|
+
const p1 = path.indexOf(ch0 === 47 /* CharacterCodes.slash */ ? '/' : altDirectorySeparator, 2);
|
|
507
|
+
if (p1 < 0)
|
|
508
|
+
return path.length; // UNC: "//server" or "\\server"
|
|
509
|
+
return p1 + 1; // UNC: "//server/" or "\\server\"
|
|
510
|
+
}
|
|
511
|
+
// DOS
|
|
512
|
+
if (isVolumeCharacter(ch0) && path.charCodeAt(1) === 58 /* CharacterCodes.colon */) {
|
|
513
|
+
const ch2 = path.charCodeAt(2);
|
|
514
|
+
if (ch2 === 47 /* CharacterCodes.slash */ || ch2 === 92 /* CharacterCodes.backslash */)
|
|
515
|
+
return 3; // DOS: "c:/" or "c:\"
|
|
516
|
+
if (path.length === 2)
|
|
517
|
+
return 2; // DOS: "c:" (but not "c:d")
|
|
518
|
+
}
|
|
519
|
+
// URL
|
|
520
|
+
const schemeEnd = path.indexOf(urlSchemeSeparator);
|
|
521
|
+
if (schemeEnd !== -1) {
|
|
522
|
+
const authorityStart = schemeEnd + urlSchemeSeparator.length;
|
|
523
|
+
const authorityEnd = path.indexOf('/', authorityStart);
|
|
524
|
+
if (authorityEnd !== -1) {
|
|
525
|
+
// URL: "file:///", "file://server/", "file://server/path"
|
|
526
|
+
// For local "file" URLs, include the leading DOS volume (if present).
|
|
527
|
+
// Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
|
|
528
|
+
// special case interpreted as "the machine from which the URL is being interpreted".
|
|
529
|
+
const scheme = path.slice(0, schemeEnd);
|
|
530
|
+
const authority = path.slice(authorityStart, authorityEnd);
|
|
531
|
+
if (scheme === 'file' &&
|
|
532
|
+
(authority === '' || authority === 'localhost') &&
|
|
533
|
+
isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {
|
|
534
|
+
const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
|
|
535
|
+
if (volumeSeparatorEnd !== -1) {
|
|
536
|
+
if (path.charCodeAt(volumeSeparatorEnd) === 47 /* CharacterCodes.slash */) {
|
|
537
|
+
// URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
|
|
538
|
+
return ~(volumeSeparatorEnd + 1);
|
|
539
|
+
}
|
|
540
|
+
if (volumeSeparatorEnd === path.length) {
|
|
541
|
+
// URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
|
|
542
|
+
// but not "file:///c:d" or "file:///c%3ad"
|
|
543
|
+
return ~volumeSeparatorEnd;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"
|
|
548
|
+
}
|
|
549
|
+
return ~path.length; // URL: "file://server", "http://server"
|
|
550
|
+
}
|
|
551
|
+
// relative
|
|
552
|
+
return 0;
|
|
553
|
+
};
|
|
554
|
+
const isVolumeCharacter = (charCode) => (charCode >= 97 /* CharacterCodes.a */ && charCode <= 122 /* CharacterCodes.z */) ||
|
|
555
|
+
(charCode >= 65 /* CharacterCodes.A */ && charCode <= 90 /* CharacterCodes.Z */);
|
|
556
|
+
const getFileUrlVolumeSeparatorEnd = (url, start) => {
|
|
557
|
+
const ch0 = url.charCodeAt(start);
|
|
558
|
+
if (ch0 === 58 /* CharacterCodes.colon */)
|
|
559
|
+
return start + 1;
|
|
560
|
+
if (ch0 === 37 /* CharacterCodes.percent */ && url.charCodeAt(start + 1) === 51 /* CharacterCodes._3 */) {
|
|
561
|
+
const ch2 = url.charCodeAt(start + 2);
|
|
562
|
+
if (ch2 === 97 /* CharacterCodes.a */ || ch2 === 65 /* CharacterCodes.A */)
|
|
563
|
+
return start + 3;
|
|
564
|
+
}
|
|
565
|
+
return -1;
|
|
566
|
+
};
|
|
567
|
+
const pathComponents = (path, rootLength) => {
|
|
568
|
+
const root = path.substring(0, rootLength);
|
|
569
|
+
const rest = path.substring(rootLength).split('/');
|
|
570
|
+
const restLen = rest.length;
|
|
571
|
+
if (restLen > 0 && !rest[restLen - 1]) {
|
|
572
|
+
rest.pop();
|
|
573
|
+
}
|
|
574
|
+
return [root, ...rest];
|
|
575
575
|
};
|
|
576
576
|
|
|
577
|
-
class ScreenshotLocalConnector extends ScreenshotConnector {
|
|
578
|
-
async publishBuild(results) {
|
|
579
|
-
if (this.updateMaster || !results.masterBuild) {
|
|
580
|
-
results.masterBuild = {
|
|
581
|
-
id: 'master',
|
|
582
|
-
message: 'Master',
|
|
583
|
-
appNamespace: this.appNamespace,
|
|
584
|
-
timestamp: Date.now(),
|
|
585
|
-
screenshots: [],
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
results.currentBuild.screenshots.forEach((currentScreenshot) => {
|
|
589
|
-
const masterHasScreenshot = results.masterBuild.screenshots.some((masterScreenshot) => {
|
|
590
|
-
return currentScreenshot.id === masterScreenshot.id;
|
|
591
|
-
});
|
|
592
|
-
if (!masterHasScreenshot) {
|
|
593
|
-
results.masterBuild.screenshots.push(Object.assign({}, currentScreenshot));
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
this.sortScreenshots(results.masterBuild.screenshots);
|
|
597
|
-
await writeFile(this.masterBuildFilePath, JSON.stringify(results.masterBuild, null, 2));
|
|
598
|
-
await this.generateJsonpDataUris(results.currentBuild);
|
|
599
|
-
const compareAppSourceDir = path.join(this.packageDir, 'screenshot', 'compare');
|
|
600
|
-
const appSrcUrl = normalizePath(path.relative(this.screenshotDir, compareAppSourceDir));
|
|
601
|
-
const imagesUrl = normalizePath(path.relative(this.screenshotDir, this.imagesDir));
|
|
602
|
-
const jsonpUrl = normalizePath(path.relative(this.screenshotDir, this.cacheDir));
|
|
603
|
-
const compareAppHtml = createLocalCompareApp(this.appNamespace, appSrcUrl, imagesUrl, jsonpUrl, results.masterBuild, results.currentBuild);
|
|
604
|
-
const compareAppFileName = 'compare.html';
|
|
605
|
-
const compareAppFilePath = path.join(this.screenshotDir, compareAppFileName);
|
|
606
|
-
await writeFile(compareAppFilePath, compareAppHtml);
|
|
607
|
-
const gitIgnorePath = path.join(this.screenshotDir, '.gitignore');
|
|
608
|
-
const gitIgnoreExists = await fileExists(gitIgnorePath);
|
|
609
|
-
if (!gitIgnoreExists) {
|
|
610
|
-
const content = [this.imagesDirName, this.buildsDirName, compareAppFileName];
|
|
611
|
-
await writeFile(gitIgnorePath, content.join('\n'));
|
|
612
|
-
}
|
|
613
|
-
const url = new URL(`file://${compareAppFilePath}`);
|
|
614
|
-
results.compare.url = url.href;
|
|
615
|
-
return results;
|
|
616
|
-
}
|
|
617
|
-
async getScreenshotCache() {
|
|
618
|
-
let screenshotCache = null;
|
|
619
|
-
try {
|
|
620
|
-
screenshotCache = JSON.parse(await readFile(this.screenshotCacheFilePath));
|
|
621
|
-
}
|
|
622
|
-
catch (e) { }
|
|
623
|
-
return screenshotCache;
|
|
624
|
-
}
|
|
625
|
-
async updateScreenshotCache(cache, buildResults) {
|
|
626
|
-
cache = await super.updateScreenshotCache(cache, buildResults);
|
|
627
|
-
await writeFile(this.screenshotCacheFilePath, JSON.stringify(cache, null, 2));
|
|
628
|
-
return cache;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
function createLocalCompareApp(namespace, appSrcUrl, imagesUrl, jsonpUrl, a, b) {
|
|
577
|
+
class ScreenshotLocalConnector extends ScreenshotConnector {
|
|
578
|
+
async publishBuild(results) {
|
|
579
|
+
if (this.updateMaster || !results.masterBuild) {
|
|
580
|
+
results.masterBuild = {
|
|
581
|
+
id: 'master',
|
|
582
|
+
message: 'Master',
|
|
583
|
+
appNamespace: this.appNamespace,
|
|
584
|
+
timestamp: Date.now(),
|
|
585
|
+
screenshots: [],
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
results.currentBuild.screenshots.forEach((currentScreenshot) => {
|
|
589
|
+
const masterHasScreenshot = results.masterBuild.screenshots.some((masterScreenshot) => {
|
|
590
|
+
return currentScreenshot.id === masterScreenshot.id;
|
|
591
|
+
});
|
|
592
|
+
if (!masterHasScreenshot) {
|
|
593
|
+
results.masterBuild.screenshots.push(Object.assign({}, currentScreenshot));
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
this.sortScreenshots(results.masterBuild.screenshots);
|
|
597
|
+
await writeFile(this.masterBuildFilePath, JSON.stringify(results.masterBuild, null, 2));
|
|
598
|
+
await this.generateJsonpDataUris(results.currentBuild);
|
|
599
|
+
const compareAppSourceDir = path.join(this.packageDir, 'screenshot', 'compare');
|
|
600
|
+
const appSrcUrl = normalizePath(path.relative(this.screenshotDir, compareAppSourceDir));
|
|
601
|
+
const imagesUrl = normalizePath(path.relative(this.screenshotDir, this.imagesDir));
|
|
602
|
+
const jsonpUrl = normalizePath(path.relative(this.screenshotDir, this.cacheDir));
|
|
603
|
+
const compareAppHtml = createLocalCompareApp(this.appNamespace, appSrcUrl, imagesUrl, jsonpUrl, results.masterBuild, results.currentBuild);
|
|
604
|
+
const compareAppFileName = 'compare.html';
|
|
605
|
+
const compareAppFilePath = path.join(this.screenshotDir, compareAppFileName);
|
|
606
|
+
await writeFile(compareAppFilePath, compareAppHtml);
|
|
607
|
+
const gitIgnorePath = path.join(this.screenshotDir, '.gitignore');
|
|
608
|
+
const gitIgnoreExists = await fileExists(gitIgnorePath);
|
|
609
|
+
if (!gitIgnoreExists) {
|
|
610
|
+
const content = [this.imagesDirName, this.buildsDirName, compareAppFileName];
|
|
611
|
+
await writeFile(gitIgnorePath, content.join('\n'));
|
|
612
|
+
}
|
|
613
|
+
const url = new URL(`file://${compareAppFilePath}`);
|
|
614
|
+
results.compare.url = url.href;
|
|
615
|
+
return results;
|
|
616
|
+
}
|
|
617
|
+
async getScreenshotCache() {
|
|
618
|
+
let screenshotCache = null;
|
|
619
|
+
try {
|
|
620
|
+
screenshotCache = JSON.parse(await readFile(this.screenshotCacheFilePath));
|
|
621
|
+
}
|
|
622
|
+
catch (e) { }
|
|
623
|
+
return screenshotCache;
|
|
624
|
+
}
|
|
625
|
+
async updateScreenshotCache(cache, buildResults) {
|
|
626
|
+
cache = await super.updateScreenshotCache(cache, buildResults);
|
|
627
|
+
await writeFile(this.screenshotCacheFilePath, JSON.stringify(cache, null, 2));
|
|
628
|
+
return cache;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function createLocalCompareApp(namespace, appSrcUrl, imagesUrl, jsonpUrl, a, b) {
|
|
632
632
|
return `<!doctype html>
|
|
633
633
|
<html dir="ltr" lang="en">
|
|
634
634
|
<head>
|
|
@@ -654,7 +654,7 @@ function createLocalCompareApp(namespace, appSrcUrl, imagesUrl, jsonpUrl, a, b)
|
|
|
654
654
|
})();
|
|
655
655
|
</script>
|
|
656
656
|
</body>
|
|
657
|
-
</html>`;
|
|
657
|
+
</html>`;
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
exports.ScreenshotConnector = ScreenshotConnector;
|