UNPKG

ipsos-components

Version:

Material Design components for Angular

246 lines (209 loc) 11.2 kB
import {task} from 'gulp'; import {readdirSync, statSync, existsSync, mkdirp, readFileSync, writeFileSync} from 'fs-extra'; import {openScreenshotsBucket, connectFirebaseScreenshots} from '../util/firebase'; import {isTravisMasterBuild} from '../util/travis-ci'; import * as path from 'path'; import * as firebaseAdmin from 'firebase-admin'; // Firebase provides TypeScript definitions that are only accessible from specific namespaces. // This means that those types are really long and it's nearly impossible to write a function that // doesn't exceed the maximum columns. Import the types from the namespace so they are shorter. import Database = firebaseAdmin.database.Database; import DataSnapshot = firebaseAdmin.database.DataSnapshot; // This import lacks of type definitions. const imageDiff = require('image-diff'); /** Travis secure token that will be used by the Screenshot functions to verify the identity. */ const travisSecureToken = getSecureToken(); /** Git SHA of the current Pull Request being checked by Travis. */ const pullRequestSha = process.env['TRAVIS_PULL_REQUEST_SHA']; const SCREENSHOT_DIR = './screenshots'; const LOCAL_GOLDENS = path.join(SCREENSHOT_DIR, `golds`); const LOCAL_DIFFS = path.join(SCREENSHOT_DIR, `diff`); // Directory to which untrusted screenshot results are temporarily written // (without authentication required) before they are verified and copied to // the final storage location. const TEMP_FOLDER = 'untrustedInbox'; const FIREBASE_REPORT = `${TEMP_FOLDER}/screenshot/reports`; const FIREBASE_IMAGE = `${TEMP_FOLDER}/screenshot/images`; const FIREBASE_DATA_GOLDENS = `screenshot/goldens`; const FIREBASE_STORAGE_GOLDENS = 'goldens'; /** Task which upload screenshots generated from e2e test. */ task('screenshots', () => { const prNumber = process.env['TRAVIS_PULL_REQUEST']!; if (isTravisMasterBuild()) { // Only update goldens for master build return uploadGoldenScreenshots(); } else if (prNumber) { const firebaseApp = connectFirebaseScreenshots(); const database = firebaseApp.database(); let lastActionTime = Date.now(); console.log(` Starting screenshots task with results from e2e task...`); return uploadTravisJobInfo(database, prNumber) .then(() => { console.log(` Downloading screenshot golds from Firebase...`); lastActionTime = Date.now(); return downloadGoldScreenshotFiles(database); }) .then(() => { console.log(` Downloading golds done (took ${Date.now() - lastActionTime}ms)`); console.log(` Comparing screenshots golds to test result screenshots...`); lastActionTime = Date.now(); return compareScreenshotFiles(database, prNumber); }) .then(passedAll => { console.log(` Comparison done (took ${Date.now() - lastActionTime}ms)`); console.log(` Uploading screenshot diff results to Firebase and GitHub...`); lastActionTime = Date.now(); return Promise.all([ setPullRequestResult(database, prNumber, passedAll), uploadScreenshotsData(database, 'diff', prNumber), uploadScreenshotsData(database, 'test', prNumber), ]); }) .then(() => { console.log(` Uploading results done (took ${Date.now() - lastActionTime}ms)`); firebaseApp.delete(); }) .catch((err: any) => { console.error(` Screenshot tests encountered an error!`); console.error(err); firebaseApp.delete(); }); } }); /** Sets the screenshot diff result for a given file of a Pull Request. */ function setFileResult(database: Database, prNumber: string, fileName: string, result: boolean) { return getPullRequestRef(database, prNumber).child('results').child(fileName).set(result); } /** Sets the full diff result for the current Pull Request that runs inside of Travis. */ function setPullRequestResult(database: Database, prNumber: string, result: boolean) { return getPullRequestRef(database, prNumber).child('result').child(pullRequestSha).set(result); } /** Returns the Firebase Reference that contains all data related to the specified PR. */ function getPullRequestRef(database: Database, prNumber: string) { return database.ref(FIREBASE_REPORT).child(prNumber).child(travisSecureToken); } /** Uploads necessary Travis CI job variables that will be used in the Screenshot Panel. */ function uploadTravisJobInfo(database: Database, prNumber: string) { return getPullRequestRef(database, prNumber).update({ sha: process.env['TRAVIS_PULL_REQUEST_SHA'], travis: process.env['TRAVIS_JOB_ID'], }); } /** Downloads all golden screenshot files and stores them in the local file system. */ function downloadGoldScreenshotFiles(database: Database) { // Create the directory that will contain all goldens if it's not present yet. mkdirp(LOCAL_GOLDENS); return database.ref(FIREBASE_DATA_GOLDENS).once('value').then(snapshot => { snapshot.forEach((childSnapshot: DataSnapshot) => { const screenshotName = childSnapshot.key; const binaryData = new Buffer(childSnapshot.val(), 'base64').toString('binary'); writeFileSync(`${LOCAL_GOLDENS}/${screenshotName}.screenshot.png`, binaryData, 'binary'); }); }); } /** Extracts the name of a given screenshot file by removing the file extension. */ function extractScreenshotName(fileName: string) { return path.basename(fileName, '.screenshot.png'); } /** Gets a list of files inside of a directory that end with `.screenshot.png`. */ function getLocalScreenshotFiles(directory: string): string[] { return readdirSync(directory) .filter((fileName: string) => !statSync(path.join(directory, fileName)).isDirectory()) .filter((fileName: string) => fileName.endsWith('.screenshot.png')); } /** * Upload screenshots to a Firebase Database path that will then upload the file to a Google * Cloud Storage bucket if the Auth token is valid. * @param database Firebase database instance. * @param prNumber The key used in firebase. Here it is the PR number. * @param mode Upload mode. This can be either 'test' or 'diff'. * - If the images are the test results, mode should be 'test'. * - If the images are the diff images generated, mode should be 'diff'. */ function uploadScreenshotsData(database: Database, mode: 'test' | 'diff', prNumber: string) { const localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR; return Promise.all(getLocalScreenshotFiles(localDir).map(file => { const filePath = path.join(localDir, file); const fileName = extractScreenshotName(filePath); const binaryContent = readFileSync(filePath); // Upload the Buffer of the screenshot image to a Firebase Database reference that will // then upload the screenshot file to a Google Cloud Storage bucket if the JWT token is valid. return database.ref(FIREBASE_IMAGE) .child(prNumber).child(travisSecureToken).child(mode).child(fileName) .set(binaryContent); })); } /** Concurrently compares every golden screenshot with the newly taken screenshots. */ function compareScreenshotFiles(database: Database, prNumber: string) { const fileNames = getLocalScreenshotFiles(LOCAL_GOLDENS); const compares = fileNames.map(fileName => compareScreenshotFile(fileName, database, prNumber)); // Wait for all compares to finish and then return a Promise that resolves with a boolean that // shows whether the tests passed or not. return Promise.all(compares).then((results: boolean[]) => results.every(Boolean)); } /** Compare the specified screenshot file with the golden file from Firebase. */ function compareScreenshotFile(fileName: string, database: Database, prNumber: string) { const goldScreenshotPath = path.join(LOCAL_GOLDENS, fileName); const localScreenshotPath = path.join(SCREENSHOT_DIR, fileName); const diffScreenshotPath = path.join(LOCAL_DIFFS, fileName); const screenshotName = extractScreenshotName(fileName); if (existsSync(goldScreenshotPath) && existsSync(localScreenshotPath)) { return compareImage(localScreenshotPath, goldScreenshotPath, diffScreenshotPath) .then(result => { // Set the screenshot diff result in Firebase and afterwards pass the result boolean // to the Promise chain again. return setFileResult(database, prNumber, screenshotName, result).then(() => result); }); } else { return setFileResult(database, prNumber, screenshotName, false).then(() => false); } } /** Uploads golden screenshots to the Google Cloud Storage bucket for the screenshots. */ async function uploadGoldenScreenshots() { const bucket = openScreenshotsBucket(); const localScreenshots = getLocalScreenshotFiles(SCREENSHOT_DIR); const storageGoldenFiles = (await bucket.getFiles({prefix: FIREBASE_STORAGE_GOLDENS}))[0]; // Only delete golden images that are outdated to avoid collisions with other screenshot diffs. // Deleting every golden screenshot may also work, but will likely cause flakiness if multiple // screenshot tasks run. const deleteOutdatedGoldenFiles = Promise.all(storageGoldenFiles .filter((file: any) => !localScreenshots.includes(path.basename(file.name))) .map((file: any) => file.delete())); const uploadNewGoldenImages = Promise.all(localScreenshots.map(fileName => { const filePath = path.join(SCREENSHOT_DIR, fileName); const storageDestination = `${FIREBASE_STORAGE_GOLDENS}/${fileName}`; return bucket.upload(filePath, { destination: storageDestination }); })); await Promise.all([deleteOutdatedGoldenFiles, uploadNewGoldenImages]); } /** * Compares two images using the Node package image-diff. A difference screenshot will be created. * The returned promise will resolve with a boolean that will be true if the images are equal. */ function compareImage(actualPath: string, goldenPath: string, diffPath: string): Promise<boolean> { return new Promise(resolve => { imageDiff({ actualImage: actualPath, expectedImage: goldenPath, diffImage: diffPath, }, (err: any, imagesAreEqual: boolean) => { if (err) { throw err; } resolve(imagesAreEqual); }); }); } /** * Get processed secure token. The jwt token has 3 parts: header, payload, signature and has format * {jwtHeader}.{jwtPayload}.{jwtSignature} * The three parts is connected by '.', while '.' is not a valid path in firebase database. * Replace all '.' to '/' to make the path valid * Output is {jwtHeader}/{jwtPayload}/{jwtSignature}. * This secure token is used to validate the write access is from our TravisCI under our repo. * All data is written to /$path/$secureToken/$data and after validated the * secure token, the data is moved to /$path/$data in database. */ function getSecureToken() { return (process.env['FIREBASE_ACCESS_TOKEN'] || '').replace(/[.]/g, '/'); }