index.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. 'use strict';
  2. const os = require('os');
  3. const path = require('path');
  4. const fs = require('fs');
  5. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  6. const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
  7. const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
  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. * @param path the Windows-based path to convert
  435. * @returns the converted path
  436. */
  437. const normalizePath = (path) => {
  438. if (typeof path !== 'string') {
  439. throw new Error(`invalid path to normalize`);
  440. }
  441. path = normalizeSlashes(path.trim());
  442. const components = pathComponents(path, getRootLength(path));
  443. const reducedComponents = reducePathComponents(components);
  444. const rootPart = reducedComponents[0];
  445. const secondPart = reducedComponents[1];
  446. const normalized = rootPart + reducedComponents.slice(1).join('/');
  447. if (normalized === '') {
  448. return '.';
  449. }
  450. if (rootPart === '' &&
  451. secondPart &&
  452. path.includes('/') &&
  453. !secondPart.startsWith('.') &&
  454. !secondPart.startsWith('@')) {
  455. return './' + normalized;
  456. }
  457. return normalized;
  458. };
  459. const normalizeSlashes = (path) => path.replace(backslashRegExp, '/');
  460. const altDirectorySeparator = '\\';
  461. const urlSchemeSeparator = '://';
  462. const backslashRegExp = /\\/g;
  463. const reducePathComponents = (components) => {
  464. if (!Array.isArray(components) || components.length === 0) {
  465. return [];
  466. }
  467. const reduced = [components[0]];
  468. for (let i = 1; i < components.length; i++) {
  469. const component = components[i];
  470. if (!component)
  471. continue;
  472. if (component === '.')
  473. continue;
  474. if (component === '..') {
  475. if (reduced.length > 1) {
  476. if (reduced[reduced.length - 1] !== '..') {
  477. reduced.pop();
  478. continue;
  479. }
  480. }
  481. else if (reduced[0])
  482. continue;
  483. }
  484. reduced.push(component);
  485. }
  486. return reduced;
  487. };
  488. const getRootLength = (path) => {
  489. const rootLength = getEncodedRootLength(path);
  490. return rootLength < 0 ? ~rootLength : rootLength;
  491. };
  492. const getEncodedRootLength = (path) => {
  493. if (!path)
  494. return 0;
  495. const ch0 = path.charCodeAt(0);
  496. // POSIX or UNC
  497. if (ch0 === 47 /* CharacterCodes.slash */ || ch0 === 92 /* CharacterCodes.backslash */) {
  498. if (path.charCodeAt(1) !== ch0)
  499. return 1; // POSIX: "/" (or non-normalized "\")
  500. const p1 = path.indexOf(ch0 === 47 /* CharacterCodes.slash */ ? '/' : altDirectorySeparator, 2);
  501. if (p1 < 0)
  502. return path.length; // UNC: "//server" or "\\server"
  503. return p1 + 1; // UNC: "//server/" or "\\server\"
  504. }
  505. // DOS
  506. if (isVolumeCharacter(ch0) && path.charCodeAt(1) === 58 /* CharacterCodes.colon */) {
  507. const ch2 = path.charCodeAt(2);
  508. if (ch2 === 47 /* CharacterCodes.slash */ || ch2 === 92 /* CharacterCodes.backslash */)
  509. return 3; // DOS: "c:/" or "c:\"
  510. if (path.length === 2)
  511. return 2; // DOS: "c:" (but not "c:d")
  512. }
  513. // URL
  514. const schemeEnd = path.indexOf(urlSchemeSeparator);
  515. if (schemeEnd !== -1) {
  516. const authorityStart = schemeEnd + urlSchemeSeparator.length;
  517. const authorityEnd = path.indexOf('/', authorityStart);
  518. if (authorityEnd !== -1) {
  519. // URL: "file:///", "file://server/", "file://server/path"
  520. // For local "file" URLs, include the leading DOS volume (if present).
  521. // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
  522. // special case interpreted as "the machine from which the URL is being interpreted".
  523. const scheme = path.slice(0, schemeEnd);
  524. const authority = path.slice(authorityStart, authorityEnd);
  525. if (scheme === 'file' &&
  526. (authority === '' || authority === 'localhost') &&
  527. isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {
  528. const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
  529. if (volumeSeparatorEnd !== -1) {
  530. if (path.charCodeAt(volumeSeparatorEnd) === 47 /* CharacterCodes.slash */) {
  531. // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
  532. return ~(volumeSeparatorEnd + 1);
  533. }
  534. if (volumeSeparatorEnd === path.length) {
  535. // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
  536. // but not "file:///c:d" or "file:///c%3ad"
  537. return ~volumeSeparatorEnd;
  538. }
  539. }
  540. }
  541. return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"
  542. }
  543. return ~path.length; // URL: "file://server", "http://server"
  544. }
  545. // relative
  546. return 0;
  547. };
  548. const isVolumeCharacter = (charCode) => (charCode >= 97 /* CharacterCodes.a */ && charCode <= 122 /* CharacterCodes.z */) ||
  549. (charCode >= 65 /* CharacterCodes.A */ && charCode <= 90 /* CharacterCodes.Z */);
  550. const getFileUrlVolumeSeparatorEnd = (url, start) => {
  551. const ch0 = url.charCodeAt(start);
  552. if (ch0 === 58 /* CharacterCodes.colon */)
  553. return start + 1;
  554. if (ch0 === 37 /* CharacterCodes.percent */ && url.charCodeAt(start + 1) === 51 /* CharacterCodes._3 */) {
  555. const ch2 = url.charCodeAt(start + 2);
  556. if (ch2 === 97 /* CharacterCodes.a */ || ch2 === 65 /* CharacterCodes.A */)
  557. return start + 3;
  558. }
  559. return -1;
  560. };
  561. const pathComponents = (path, rootLength) => {
  562. const root = path.substring(0, rootLength);
  563. const rest = path.substring(rootLength).split('/');
  564. const restLen = rest.length;
  565. if (restLen > 0 && !rest[restLen - 1]) {
  566. rest.pop();
  567. }
  568. return [root, ...rest];
  569. };
  570. class ScreenshotLocalConnector extends ScreenshotConnector {
  571. async publishBuild(results) {
  572. if (this.updateMaster || !results.masterBuild) {
  573. results.masterBuild = {
  574. id: 'master',
  575. message: 'Master',
  576. appNamespace: this.appNamespace,
  577. timestamp: Date.now(),
  578. screenshots: [],
  579. };
  580. }
  581. results.currentBuild.screenshots.forEach((currentScreenshot) => {
  582. const masterHasScreenshot = results.masterBuild.screenshots.some((masterScreenshot) => {
  583. return currentScreenshot.id === masterScreenshot.id;
  584. });
  585. if (!masterHasScreenshot) {
  586. results.masterBuild.screenshots.push(Object.assign({}, currentScreenshot));
  587. }
  588. });
  589. this.sortScreenshots(results.masterBuild.screenshots);
  590. await writeFile(this.masterBuildFilePath, JSON.stringify(results.masterBuild, null, 2));
  591. await this.generateJsonpDataUris(results.currentBuild);
  592. const compareAppSourceDir = path.join(this.packageDir, 'screenshot', 'compare');
  593. const appSrcUrl = normalizePath(path.relative(this.screenshotDir, compareAppSourceDir));
  594. const imagesUrl = normalizePath(path.relative(this.screenshotDir, this.imagesDir));
  595. const jsonpUrl = normalizePath(path.relative(this.screenshotDir, this.cacheDir));
  596. const compareAppHtml = createLocalCompareApp(this.appNamespace, appSrcUrl, imagesUrl, jsonpUrl, results.masterBuild, results.currentBuild);
  597. const compareAppFileName = 'compare.html';
  598. const compareAppFilePath = path.join(this.screenshotDir, compareAppFileName);
  599. await writeFile(compareAppFilePath, compareAppHtml);
  600. const gitIgnorePath = path.join(this.screenshotDir, '.gitignore');
  601. const gitIgnoreExists = await fileExists(gitIgnorePath);
  602. if (!gitIgnoreExists) {
  603. const content = [this.imagesDirName, this.buildsDirName, compareAppFileName];
  604. await writeFile(gitIgnorePath, content.join('\n'));
  605. }
  606. const url = new URL(`file://${compareAppFilePath}`);
  607. results.compare.url = url.href;
  608. return results;
  609. }
  610. async getScreenshotCache() {
  611. let screenshotCache = null;
  612. try {
  613. screenshotCache = JSON.parse(await readFile(this.screenshotCacheFilePath));
  614. }
  615. catch (e) { }
  616. return screenshotCache;
  617. }
  618. async updateScreenshotCache(cache, buildResults) {
  619. cache = await super.updateScreenshotCache(cache, buildResults);
  620. await writeFile(this.screenshotCacheFilePath, JSON.stringify(cache, null, 2));
  621. return cache;
  622. }
  623. }
  624. function createLocalCompareApp(namespace, appSrcUrl, imagesUrl, jsonpUrl, a, b) {
  625. return `<!doctype html>
  626. <html dir="ltr" lang="en">
  627. <head>
  628. <meta charset="utf-8">
  629. <title>Local ${namespace || ''} - Stencil Screenshot Visual Diff</title>
  630. <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">
  631. <meta http-equiv="x-ua-compatible" content="IE=Edge">
  632. <link href="${appSrcUrl}/build/app.css" rel="stylesheet">
  633. <script type="module" src="${appSrcUrl}/build/app.esm.js"></script>
  634. <script nomodule src="${appSrcUrl}/build/app.js"></script>
  635. <link rel="icon" type="image/x-icon" href="${appSrcUrl}/assets/favicon.ico">
  636. </head>
  637. <body>
  638. <script>
  639. (function() {
  640. var app = document.createElement('screenshot-compare');
  641. app.appSrcUrl = '${appSrcUrl}';
  642. app.imagesUrl = '${imagesUrl}/';
  643. app.jsonpUrl = '${jsonpUrl}/';
  644. app.a = ${JSON.stringify(a)};
  645. app.b = ${JSON.stringify(b)};
  646. document.body.appendChild(app);
  647. })();
  648. </script>
  649. </body>
  650. </html>`;
  651. }
  652. exports.ScreenshotConnector = ScreenshotConnector;
  653. exports.ScreenshotLocalConnector = ScreenshotLocalConnector;