index.js 25 KB

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