chore: remove screenshot testing (#12155)

Removes the screenshot test tool since Angular Material is visually being tested inside of Google.
This commit is contained in:
Paul Gschwendtner
2018-07-11 17:24:21 +02:00
committed by Jeremy Elbourn
parent dc0732bc2c
commit afb0ee37eb
70 changed files with 3 additions and 12924 deletions
-1
View File
@@ -29,7 +29,6 @@ import './tasks/lint';
import './tasks/material-release';
import './tasks/payload';
import './tasks/publish';
import './tasks/screenshots';
import './tasks/unit-test';
import './tasks/universal';
import './tasks/validate-release';
+1 -2
View File
@@ -26,8 +26,7 @@ const assetsGlob = join(appDir, '**/*.+(html|css|json|ts)');
task('e2e', sequenceTask(
[':test:protractor:setup', 'serve:e2eapp'],
':test:protractor',
':serve:e2eapp:stop',
'screenshots',
':serve:e2eapp:stop'
));
/**
-245
View File
@@ -1,245 +0,0 @@
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, '/');
}
-29
View File
@@ -1,9 +1,5 @@
const firebaseAdmin = require('firebase-admin');
const firebase = require('firebase');
const cloudStorage = require('@google-cloud/storage');
// Firebase configuration for the Screenshot project. Use the config from the screenshot functions.
const screenshotFirebaseConfig = require('../../screenshot-test/functions/config.json');
/** Database URL of the dashboard firebase project. */
const dashboardDatabaseUrl = 'https://material2-board.firebaseio.com';
@@ -29,23 +25,6 @@ export function openFirebaseDashboardAppAsGuest() {
return firebase.initializeApp({ databaseURL: dashboardDatabaseUrl });
}
/**
* Open Google Cloud Storage for screenshots.
* The files uploaded to google cloud are also available to firebase storage.
*/
export function openScreenshotsBucket() {
let gcs = cloudStorage({
projectId: 'material2-screenshots',
credentials: {
client_email: 'firebase-adminsdk-t4209@material2-screenshots.iam.gserviceaccount.com',
private_key: decode(process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY']!)
},
});
// Reference the existing appspot bucket.
return gcs.bucket('material2-screenshots.appspot.com');
}
/** Decodes a Travis CI variable that is public in favor for PRs. */
export function decode(str: string): string {
// In Travis CI the private key will be incorrect because the line-breaks are escaped.
@@ -53,11 +32,3 @@ export function decode(str: string): string {
return (str || '').replace(/\\n/g, '\n');
}
/**
* Open firebase connection for screenshot
* This connection is client side connection with no credentials
*/
export function connectFirebaseScreenshots() {
return firebase.initializeApp(screenshotFirebaseConfig.firebase);
}
-66
View File
@@ -1,66 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"screenshot-test": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.json",
"assets": [
"src/favicon.ico"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "screenshot-test:build"
},
"configurations": {}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "screenshot-test:build"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [],
"exclude": []
}
}
}
},
"screenshot-test-e2e": {
"root": "",
"sourceRoot": "",
"projectType": "application"
}
},
"defaultProject": "screenshot-test",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "css"
},
"@schematics/angular:directive": {
"prefix": "app"
}
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"hosting": {
"public": "dist",
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
@@ -1,9 +0,0 @@
{
"firebase": {
"apiKey": "AIzaSyBekh5ZSi1vEhaE2qetH4RU91gHmUmpqgg",
"authDomain": "material2-screenshots.firebaseapp.com",
"databaseURL": "https://material2-screenshots.firebaseio.com",
"storageBucket": "material2-screenshots.appspot.com",
"messagingSenderId": "975527407245"
}
}
@@ -1,40 +0,0 @@
import * as firebaseFunctions from 'firebase-functions';
import {writeFileSync} from 'fs';
import {verifySecureToken} from './jwt-util';
import {isCreateEvent} from './util/util';
const gcs = require('@google-cloud/storage')();
/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */
const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket);
/**
* Writes base-64 encoded test images to png files on the filesystem.
* Image data posted to database will be saved as png files
* and uploaded to screenshot/$prNumber/dataType/$filename
* Convert BufferArray to .png image file
*/
export function writeTestImagesToFiles(event: any) {
// Only edit data when it is first created. Exit when the data is deleted.
if (!isCreateEvent(event)) {
return;
}
let dataType = event.params.dataType;
let prNumber = event.params.prNumber;
let data = event.data.val();
let saveFilename = `${event.params.filename}.screenshot.png`;
// Check it's either diff images generated by screenshot comparison, or the test image results
if (dataType !== 'diff' && dataType !== 'test') {
return;
}
return verifySecureToken(event).then(() => {
let tempPath = `/tmp/${dataType}-${saveFilename}`;
let filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`;
let binaryData = new Buffer(data, 'base64').toString('binary');
writeFileSync(tempPath, binaryData, 'binary');
return bucket.upload(tempPath, {destination: filePath});
});
}
-30
View File
@@ -1,30 +0,0 @@
import * as firebaseFunctions from 'firebase-functions';
import {setGithubStatus} from './util/github';
/** Github status update token */
const token = firebaseFunctions.config().secret.github;
/** The repo slug. This is used to validate the JWT is sent from correct repo. */
const repoSlug = firebaseFunctions.config().repo.slug;
/** Domain to view the screenshots */
const authDomain = firebaseFunctions.config().firebase.authDomain;
/** The same of this screenshot testing tool */
const toolName = firebaseFunctions.config().tool.name;
export function updateGithubStatus(event: firebaseFunctions.Event<any>) {
if (!event.data.exists() || typeof event.data.val() != 'boolean' && event.params) {
return;
}
const result = event.data.val() == true;
const {prNumber, sha} = event.params!;
return setGithubStatus(sha, {
result: result,
name: toolName,
description: `${toolName} ${result ? 'passed' : 'failed'}`,
url: `http://${authDomain}/${prNumber}`
}, repoSlug, token);
}
@@ -1,41 +0,0 @@
import * as firebaseAdmin from 'firebase-admin';
import * as path from 'path';
import {readFileSync} from 'fs';
const gcs = require('@google-cloud/storage')();
/** Folder on Firebase database to store golden images data */
const FIREBASE_DATA_GOLDENS = 'screenshot/goldens';
/**
* Read golden files under /goldens/ and store the image data to
* database /screenshot/goldens/$filename as base-64 encoded string
* Convert png image files to BufferArray data
*/
export function copyGoldImagesToDatabase(name: string, resourceState: string, fileBucket: any) {
// The name should always look like "goldens/xxx.png"
let parsedPath = path.parse(name);
// Get the file name.
if (parsedPath.root != '' ||
parsedPath.dir != 'goldens' ||
parsedPath.ext.toLowerCase() != '.png') {
return;
}
let filenameKey = path.basename(parsedPath.name, '.screenshot');
let databaseRef = firebaseAdmin.database().ref(FIREBASE_DATA_GOLDENS).child(filenameKey);
// When a gold image is deleted, also delete the corresponding record in the firebase database.
if (resourceState === 'not_exists') {
return databaseRef.set(null);
}
let tempFilePath = `/tmp/${parsedPath.base}`;
let bucket = gcs.bucket(fileBucket);
// Download file from bucket.
return bucket.file(name).download({destination: tempFilePath})
.then(() => {
let data = readFileSync(tempFilePath);
return databaseRef.set(data);
}).catch((error: any) => console.error(`${filenameKey} ${error}`));
}
-22
View File
@@ -1,22 +0,0 @@
/**
* Entry point for the Firebase functions of the Screenshot tool. Firebase functions only support
* JavaScript files and therefore the TypeScript files needs to be transpiled. Using ts-node
* seems to be more elegant for now, because Firebase requires the `node_modules` to be copied
* to the output directory and when using TSC the `node_modules` won't be copied to the destination.
*/
'use strict';
const path = require('path');
// Enable TypeScript compilation at runtime using ts-node.
require('ts-node').register({
project: path.join(__dirname, 'tsconfig.json')
});
const functionExports = require('./screenshot-functions');
// Re-export every firebase function from TypeScript
Object.keys(functionExports).forEach(fnName => {
module.exports[fnName] = functionExports[fnName];
});
@@ -1,39 +0,0 @@
import * as firebaseFunctions from 'firebase-functions';
import {verifyJWT} from './util/jwt';
/** The repo slug. This is used to validate the JWT is sent from correct repo. */
const repoSlug = firebaseFunctions.config().repo.slug;
/** The JWT secret. This is used to validate JWT. */
const secret = firebaseFunctions.config().secret.key;
/**
* Extract the Json Web Token from event params.
* In screenshot gulp task the path we use is {jwtHeader}/{jwtPayload}/{jwtSignature}.
* Replace '/' with '.' to get the token.
*/
function getSecureToken(event: firebaseFunctions.Event<any>) {
const {jwtHeader, jwtPayload, jwtSignature} = event.params!;
return `${jwtHeader}.${jwtPayload}.${jwtSignature}`;
}
/**
* Verify that the event has a valid JsonWebToken. If the token is *not* valid,
* the data tied to the event will be deleted and the function will return a rejected promise.
*/
export function verifySecureToken(event: firebaseFunctions.Event<any>) {
return new Promise((resolve, reject) => {
const prNumber = event.params!['prNumber'];
const secureToken = getSecureToken(event);
return verifyJWT(secureToken, prNumber, secret, repoSlug).then(() => {
resolve();
event.data.ref.parent.set(null);
}).catch((error: any) => {
console.error(`Invalid secure token ${secureToken} ${error}`);
event.data.ref.parent.set(null);
reject();
});
});
}
@@ -1,14 +0,0 @@
{
"name": "material2-screenshot-functions",
"description": "Angular Material screenshot firebase functions",
"dependencies": {
"@types/jsonwebtoken": "^7.2.3",
"@google-cloud/storage": "^0.8.0",
"firebase-admin": "~4.1.3",
"firebase-functions": "^0.5.2",
"jsonwebtoken": "^7.4.2",
"request": "^2.81.0",
"typescript": "^2.2.2",
"ts-node": "^3.0.2"
}
}
@@ -1,130 +0,0 @@
'use strict';
import * as firebaseFunctions from 'firebase-functions';
import * as firebaseAdmin from 'firebase-admin';
import {verifyJwtAndTransferResultToTrustedLocation} from './verify-and-copy-report';
import {copyGoldImagesToDatabase} from './image-data';
import {writeTestImagesToFiles} from './data-image';
import {copyTestImagesToGoldens} from './test-goldens';
import {updateGithubStatus} from './github';
/**
* Usage: Firebase functions only accept javascript file index.js
* tsc -p tools/screenshot-test/functions/tsconfig.json
* cd functions
* npm install
* firebase deploy --only functions
*
*
* Data and images handling for Screenshot test.
*
* All users can post data to temporary folder. These Functions will check the data with
* JsonWebToken and move the valid data out of temporary folder.
*
* For valid data posted to database /$temp/screenshot/reports/$prNumber/$secureToken, move it to
* /screenshot/reports/$prNumber.
* These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job
* information.
*
* For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/,
* save the image data to image files and upload to google cloud storage under
* location /screenshots/$prNumber
* These are screenshot test result images, and difference images generated from screenshot
* comparison.
*
* For golden images uploaded to /goldens, read the data from images files and write the data to
* Firebase database under location /screenshot/goldens
* Screenshot tests can only read restricted database data with no credentials, and they cannot
* access.
* Google Cloud Storage. Therefore we copy the image data to database to make it available to
* screenshot tests.
*
* The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to
* database/storage.
* All invalid data will be removed.
* The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path.
*/
// Initialize the admin app
firebaseAdmin.initializeApp(firebaseFunctions.config().firebase);
/** The valid data types database accepts */
const dataTypes = ['result', 'sha', 'travis'];
/** The Json Web Token format. The token is stored in data path. */
const jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}';
/** The temporary folder name for screenshot data that needs to be validated via JWT. */
const tempFolder = '/untrustedInbox';
/** Untrusted report data for a PR */
const reportPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/`;
/** Untrusted image data for a PR */
const imagePath = `${tempFolder}/screenshot/images/{prNumber}/${jwtFormat}/`;
/** Trusted report data for a PR */
const trustedReportPath = `screenshot/reports/{prNumber}`;
/**
* Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/
* to /screenshot/reports/$prNumber
* Data copied: filenames(image results names), commit(github PR info),
* sha (github PR info), result (true or false for all the tests), travis job number
*/
const testDataPath = `${reportPath}/{dataType}`;
export let testData = firebaseFunctions.database.ref(testDataPath)
.onWrite((event: any) => {
const dataType = event.params.dataType;
if (dataTypes.includes(dataType)) {
return verifyJwtAndTransferResultToTrustedLocation(event, dataType);
}
});
/**
* Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/
* to /screenshot/reports/$prNumber
* Data copied: test result for each file/test with ${filename}. The value should be true or false.
*/
const testResultsPath = `${reportPath}/results/{filename}`;
export let testResults = firebaseFunctions.database.ref(testResultsPath)
.onWrite((event: any) => {
return verifyJwtAndTransferResultToTrustedLocation(event, `results/${event.params.filename}`);
});
/**
* Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/
* to storage /screenshots/$prNumber
* Data copied: test result images. Convert from data to image files in storage.
*/
const imageDataToFilePath = `${imagePath}/{dataType}/{filename}`;
export let imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath)
.onWrite(writeTestImagesToFiles);
/**
* Copy valid goldens from storage /goldens/ to database /screenshot/goldens/
* so we can read the goldens without credentials.
*/
export let goldenImageToData = firebaseFunctions.storage.bucket(
firebaseFunctions.config().firebase.storageBucket).object().onChange((event: any) => {
return copyGoldImagesToDatabase(event.data.name, event.data.resourceState, event.data.bucket);
});
/**
* Copy test result images for PR to Goldens.
* Copy images from /screenshot/$prNumber/test/ to /goldens/
*/
const approveImagesPath = `${trustedReportPath}/approved`;
export let approveImages = firebaseFunctions.database.ref(approveImagesPath)
.onWrite((event: any) => {
return copyTestImagesToGoldens(event.params.prNumber);
});
/**
* Update github status. When the result is true, update github commit status to `success`,
* otherwise update github status to `failure`.
* The Github Status Token is set in config.secret.github
*/
const githubStatusPath = `${trustedReportPath}/result/{sha}`;
export let githubStatus = firebaseFunctions.database.ref(githubStatusPath)
.onWrite(updateGithubStatus);
@@ -1,39 +0,0 @@
import * as firebaseFunctions from 'firebase-functions';
import * as firebaseAdmin from 'firebase-admin';
import * as path from 'path';
const gcs = require('@google-cloud/storage')();
/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */
const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket);
/**
* Copy files from /screenshot/$prNumber/test/ to goldens/
* Only copy the files that test result is failure. Passed test images should be the same as
* goldens.
*/
export function copyTestImagesToGoldens(prNumber: string) {
return firebaseAdmin.database().ref(`screenshot/reports/${prNumber}/results`).once('value')
.then((snapshot: firebaseAdmin.database.DataSnapshot) => {
const failedFilenames: string[] = [];
let counter = 0;
snapshot.forEach(childSnapshot => {
if (childSnapshot.key && childSnapshot.val() === false) {
failedFilenames.push(childSnapshot.key);
}
counter++;
return counter === snapshot.numChildren();
});
return failedFilenames;
}).then((failedFilenames: string[]) => {
return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then((data: any) => {
return Promise.all(data[0]
.filter((file: any) => failedFilenames.includes(
path.basename(file.name, '.screenshot.png')))
.map((file: any) => file.copy(`goldens/${path.basename(file.name)}`)));
});
});
}
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"lib": ["es2015", "es2016", "dom"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noImplicitAny": true,
"strictNullChecks": true,
"sourceMap": true,
"target": "es5",
"baseUrl": "",
"outDir": "../../../dist/screenshot-functions/"
},
"files": [
"./screenshot-functions.ts"
]
}
@@ -1,40 +0,0 @@
const request = require('request');
/** Data that must be specified to set a Github PR status. */
export type GithubStatusData = {
result: boolean;
name: string;
description: string;
url: string;
};
/** Function that sets a Github commit status */
export function setGithubStatus(commitSHA: string,
statusData: GithubStatusData,
repoSlug: string,
token: string) {
let state = statusData.result ? 'success' : 'failure';
let data = JSON.stringify({
state: state,
target_url: statusData.url,
context: statusData.name,
description: statusData.description
});
let headers = {
'Authorization': `token ${token}`,
'User-Agent': `${statusData.name}/1.0`,
'Content-Type': 'application/json'
};
return new Promise((resolve, reject) => {
request({
url: `https://api.github.com/repos/${repoSlug}/statuses/${commitSHA}`,
method: 'POST',
form: data,
headers: headers
}, (error: any, response: any) => error ? reject(error) : resolve(response.statusCode));
});
}
@@ -1,17 +0,0 @@
import * as jwt from 'jsonwebtoken';
export function verifyJWT(token: string, prNumber: string, secret: string, repoSlug: string) {
return new Promise((resolve, reject) => {
jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err: any, payload: any) => {
if (err) {
reject(err.message || err);
} else if (payload.slug !== repoSlug) {
reject(`jwt slug invalid. expected: ${repoSlug}`);
} else if (payload['pull-request'].toString() !== prNumber) {
reject(`jwt pull-request invalid. expected: ${prNumber}`);
} else {
resolve(payload);
}
});
});
}
@@ -1,11 +0,0 @@
export function isEditEvent(event: any) {
return event.data.previous.exists() && event.data.exists();
}
export function isDeleteEvent(event: any) {
return event.data.previous.exists() && !event.data.exists();
}
export function isCreateEvent(event: any) {
return !event.data.previous.exists() && event.data.exists();
}
@@ -1,22 +0,0 @@
import * as firebaseAdmin from 'firebase-admin';
import {verifySecureToken} from './jwt-util';
import {isCreateEvent} from './util/util';
/**
* Verifies that a screenshot report is valid (trusted via JWT) and, if so, copies it from the
* temporary, unauthenticated location to the more permanent, trusted location.
*/
export function verifyJwtAndTransferResultToTrustedLocation(event: any, path: string) {
// Only edit data when it is first created. Exit when the data is deleted.
if (!isCreateEvent(event)) {
return;
}
let prNumber = event.params.prNumber;
let data = event.data.val();
return verifySecureToken(event).then(() => {
return firebaseAdmin.database().ref().child('screenshot/reports')
.child(prNumber).child(path).set(data);
});
}
File diff suppressed because it is too large Load Diff
-36
View File
@@ -1,36 +0,0 @@
{
"name": "screenshot-test",
"version": "0.0.0",
"license": "MIT",
"angular-cli": {},
"scripts": {
"ng": "ng",
"start": "ng serve"
},
"private": true,
"dependencies": {
"@angular/animations": "6.0.0",
"@angular/cdk": "github:angular/cdk-builds#master",
"@angular/common": "6.0.0",
"@angular/compiler": "6.0.0",
"@angular/core": "6.0.0",
"@angular/forms": "6.0.0",
"@angular/material": "github:angular/material2-builds#master",
"@angular/platform-browser": "6.0.0",
"@angular/platform-browser-dynamic": "6.0.0",
"@angular/router": "6.0.0",
"core-js": "^2.4.1",
"firebase": "5.0.2",
"rxjs": "6.0.0",
"ts-helpers": "^1.1.1",
"zone.js": "^0.8.12"
},
"devDependencies": {
"@angular/cli": "6.0.0",
"@angular/compiler-cli": "6.0.0",
"@types/node": "^6.0.42",
"ts-node": "1.2.1",
"typescript": "2.7.x",
"@angular-devkit/build-angular": "~0.6.0"
}
}
@@ -1,173 +0,0 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import * as firebase from 'firebase';
const config = require('../config.json');
import {ScreenshotResultSummary} from './screenshot-result';
/** The service to fetch data from firebase database */
@Injectable()
export class FirebaseService {
/** The current user */
user: firebase.User;
/** The screenshot results */
screenshotResultSummary: ScreenshotResultSummary;
constructor(private _httpClient: HttpClient) {
// Initialize Firebase
firebase.initializeApp(config.firebase);
firebase.auth().onAuthStateChanged((user: firebase.User) => {
this.user = user;
});
}
/** Get the firebase storage test image folder url */
getTestScreenshotImageUrl(filename: string): Promise<string> {
return this._storageRef().child('test').child(filename).getDownloadURL();
}
/** Get the firebase storage diff image folder url */
getDiffScreenshotImageUrl(filename: string): Promise<string> {
return this._storageRef().child('diff').child(filename).getDownloadURL();
}
/** Get the firebase storage golden image folder url */
getGoldScreenshotImageUrl(filename: string): Promise<string> {
return firebase.storage().ref('goldens').child(filename).getDownloadURL();
}
/**
* Set pull request number. All test information and pull request information will be retrived
* from database
*/
set prNumber(prNumber: string) {
this.screenshotResultSummary = new ScreenshotResultSummary();
this.screenshotResultSummary.prNumber = prNumber;
this._readPullRequestScreenshotReport();
}
_readPullRequestScreenshotReport() {
if (!this.screenshotResultSummary.prNumber) {
return;
}
this._databaseRef().on('value', (snapshot: firebase.database.DataSnapshot) => {
let counter = 0;
snapshot.forEach((childSnapshot: firebase.database.DataSnapshot) => {
let childValue = childSnapshot.val();
switch (childSnapshot.key) {
case 'sha':
this._readSha(childValue);
break;
case 'travis':
this.screenshotResultSummary.travis = childValue;
break;
case 'results':
this._readResults(childSnapshot);
break;
}
counter++;
return counter === snapshot.numChildren();
});
});
}
signIntoGithub(): Promise<void> {
return firebase.auth().signInWithRedirect(new firebase.auth.GithubAuthProvider());
}
signOutFromGithub() {
firebase.auth().signOut();
}
/** Change the PR status to approved to let Firebase Functions copy test images to goldens */
approvePullRequest() {
return this._databaseRef().child('approved').child(this.screenshotResultSummary.sha)
.set(Date.now());
}
/**
* Change the commit's screenshot test status on GitHub
* The value in result/$sha/$result will trigger GitHub status update:
* true - The GitHub status for this SHA will become `success` for Screenshot Test
* false - The GitHub status for this SHA will become `failure` for Screenshot Test
* In this dashboard, we only approve pull requests, so the value is always `true`
*/
updatePullRequestResult() {
return this._databaseRef().child('result').child(this.screenshotResultSummary.sha)
.set(true);
}
/** Reference to the firebase database where the JSON test results and meatadate is stored. */
_databaseRef(): firebase.database.Reference {
return firebase.database().ref('screenshot').child('reports')
.child(this.screenshotResultSummary.prNumber);
}
/** Reference to the firebase storage bucket where the screenshot PNG files are stored. */
_storageRef(): firebase.storage.Reference {
return firebase.storage().ref('screenshots').child(this.screenshotResultSummary.prNumber);
}
/** Read the results from database adn put the results in screenshotReusltSummary */
_readResults(childSnapshot: firebase.database.DataSnapshot) {
let childCounter = 0;
this.screenshotResultSummary.collapse = [];
this.screenshotResultSummary.testNames = [];
this.screenshotResultSummary.testResultsByName.clear();
childSnapshot.forEach((resultSnapshot: firebase.database.DataSnapshot) => {
if (resultSnapshot.key) {
this._addTestResults(resultSnapshot.key, resultSnapshot.val());
}
childCounter++;
return childCounter === childSnapshot.numChildren();
});
}
_addTestResults(name: string, value: boolean) {
this.screenshotResultSummary.testResultsByName.set(name, value);
this.screenshotResultSummary.testNames.push(name);
this.screenshotResultSummary.collapse.push(value);
}
/** Read SHA and approved status from database and put them in ScreenshotResultSummary */
_readSha(childValue) {
this.screenshotResultSummary.sha = childValue;
// Get github status
this.getGithubStatus();
// Get test allTestsPassedOrApproved
this._databaseRef().child(`result/${childValue}`).once('value')
.then((dataSnapshot: firebase.database.DataSnapshot) => {
this.screenshotResultSummary.allTestsPassedOrApproved = dataSnapshot.val();
});
// Get the approved SHA and date time
this._databaseRef().child(`approved/${childValue}`).once('value')
.then((dataSnapshot: firebase.database.DataSnapshot) => {
this.screenshotResultSummary.approvedTime = dataSnapshot.val();
});
}
getGithubStatus() {
let url =
`https://api.github.com/repos/${config.repoSlug}/commits/` +
`${this.screenshotResultSummary.sha}/status`;
return this._httpClient.get<{statuses: any[]}>(url).toPromise().then(response => {
const screenshotStatus = response.statuses.find(status =>
status.context === 'Screenshot Tests');
switch (screenshotStatus && screenshotStatus.state) {
case 'success':
this.screenshotResultSummary.githubStatus = true;
break;
case 'failure':
this.screenshotResultSummary.githubStatus = false;
return;
}
});
}
}
@@ -1,6 +0,0 @@
<mat-toolbar color="primary">
<div *ngIf="user">{{user.email}}</div>
<button mat-button (click)="signIntoGithub()" *ngIf="!user">Sign In with Github</button>
<button mat-button (click)="signOutFromGithub()" *ngIf="user">Sign out</button>
</mat-toolbar>
@@ -1,24 +0,0 @@
import {Component} from '@angular/core';
import {FirebaseService} from '../firebase.service';
/** Nav bar with ability to sign in or sign out Github account */
@Component({
selector: 'app-nav',
templateUrl: './nav.component.html'
})
export class NavComponent {
get user() {
return this._service.user;
}
constructor(private _service: FirebaseService) { }
signIntoGithub() {
this._service.signIntoGithub();
}
signOutFromGithub() {
this._service.signOutFromGithub();
}
}
@@ -1,10 +0,0 @@
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-nav></app-nav>
<router-outlet></router-outlet>
`
})
export class PixactoDashboardComponent {}
@@ -1,49 +0,0 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpClientModule} from '@angular/common/http';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FirebaseService} from './firebase.service';
import {routing} from './routes';
import {
MatToolbarModule, MatButtonModule, MatCardModule, MatButtonToggleModule, MatIconModule,
MatSnackBarModule, MatTooltipModule
} from '@angular/material';
import {PixactoDashboardComponent} from './pixacto.dashboard.component';
import {ViewerComponent} from './viewer/viewer.component';
import {ResultComponent} from './result/result.component';
import {NavComponent} from './nav/nav.component';
@NgModule({
exports: [
MatToolbarModule,
MatButtonModule,
MatCardModule,
MatButtonToggleModule,
MatIconModule,
MatTooltipModule,
MatSnackBarModule
]
})
export class PixactoMaterialModule {}
@NgModule({
declarations: [
PixactoDashboardComponent,
ViewerComponent,
ResultComponent,
NavComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
PixactoMaterialModule,
routing,
],
providers: [FirebaseService],
bootstrap: [PixactoDashboardComponent]
})
export class PixactoDashboardModule { }
@@ -1,58 +0,0 @@
img {
border: 1px solid #666;
width: 100%;
}
.images-section {
display:flex;
}
.image-title {
text-transform: uppercase;
}
.image-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.side-by-side .diff-section {
display: none;
}
.flip .diff-section {
visibility: hidden;
}
.flip .test-section,
.flip .gold-section {
position: absolute;
top: 80px;
left: 0;
}
.flip .test-section {
visibility: visible;
}
.flip .gold-section {
visibility: hidden;
}
.flip.flipping .test-section {
visibility: hidden;
}
.flip.flipping .gold-section {
visibility: visible;
}
.failed {
color: red;
}
.passed {
color: forestgreen;
}
@@ -1,39 +0,0 @@
<mat-card>
<mat-card-header>
<mat-card-title>
<mat-icon (click)="toggleCollapse()">{{collapseIcon}}</mat-icon>
{{testName}}
</mat-card-title>
<mat-card-subtitle [ngClass]="resultText.toLowerCase()">{{resultText}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content *ngIf="!collapse">
<div class="images-section" [ngClass]="mode" [class.flipping]="flipping">
<div class="image-section test-section" >
<div class="image-title">Test image</div>
<div><img [src]="testImageUrl"/></div>
</div>
<div class="image-section diff-section" >
<div class="image-title">Diff image</div>
<div><img [src]="diffImageUrl"/></div>
</div>
<div class="image-section gold-section">
<div class="image-title">Reference image</div>
<div><img [src]="goldImageUrl"/></div>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<mat-button-toggle-group [(ngModel)]="mode" class="mode-section">
<mat-button-toggle value="diff">
Diff
</mat-button-toggle>
<mat-button-toggle value="flip">
Flip
</mat-button-toggle>
<mat-button-toggle value="side">
Side by side
</mat-button-toggle>
</mat-button-toggle-group>
<button mat-button color="primary" (click)="flip()" *ngIf="mode === 'flip'">Flip</button>
</mat-card-actions>
</mat-card>
@@ -1,119 +0,0 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
Output,
EventEmitter
} from '@angular/core';
import {FirebaseService} from '../firebase.service';
/**
* Component to display test result, test name, test image, reference image, and diff image.
* User can change the view mode, or collapse the panel to hide the images.
*/
@Component({
selector: 'app-result',
templateUrl: './result.component.html',
styleUrls: ['./result.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResultComponent {
/** Test name, display on top of the result card */
testName: string;
/** Test, diff and golden image urls */
testImageUrl: string;
diffImageUrl: string;
goldImageUrl: string;
/**
* Test result, auto set collapse to be the same as the result value
*/
@Input()
get result() { return this._result; }
set result(value: boolean) {
this._result = value;
this.collapse = value;
}
_result: boolean;
get resultText() {
return this._result ? 'Passed' : 'Failed';
}
/** Collapse: whether collapse or expand the card to show images */
@Input() collapse: boolean = true;
/** Mode: the result card has three modes, flip, side by side, and diff */
@Input()
get mode() { return this._mode; }
set mode(value: 'flip' | 'side-by-side' | 'diff') {
this._mode = value;
this.modeEvent.emit(value);
this._changeDetectorRef.markForCheck();
}
_mode: 'flip' | 'side-by-side' | 'diff' = 'diff';
/** When mode is "flip" whether we show the test image or the golden image */
@Input()
get flipping() { return this._flipping; }
set flipping(value: boolean) {
this._flipping = value;
this.flippingEvent.emit(value);
this._changeDetectorRef.markForCheck();
}
_flipping: boolean = false;
get collapseIcon() {
return this.collapse ? 'keyboard_arrow_right' : 'keyboard_arrow_down';
}
@Input()
set filename(filename: string) {
if (this._filename != filename) {
this._filename = filename;
this.setFilenameAndFetchImages();
}
}
_filename: string;
setFilenameAndFetchImages() {
this.testName = this._filename.replace(/[_]/g, ' ');
let imageFilename = `${this._filename}.screenshot.png`;
this.service.getTestScreenshotImageUrl(imageFilename)
.then((url) => {
this.testImageUrl = url;
this._changeDetectorRef.markForCheck();
});
this.service.getDiffScreenshotImageUrl(imageFilename)
.then((url) => {
this.diffImageUrl = url;
this._changeDetectorRef.markForCheck();
});
this.service.getGoldScreenshotImageUrl(imageFilename)
.then((url) => {
this.goldImageUrl = url;
this._changeDetectorRef.markForCheck();
});
}
@Output('flippingChange') flippingEvent = new EventEmitter<boolean>();
@Output('modeChange') modeEvent = new EventEmitter<'flip' | 'side-by-side' | 'diff'>();
@Output('collapseChange') collapseEvent = new EventEmitter<boolean>();
constructor(public service: FirebaseService, private _changeDetectorRef: ChangeDetectorRef) { }
flip() {
this.service.screenshotResultSummary.isFlipped =
!this.service.screenshotResultSummary.isFlipped;
}
toggleCollapse() {
this.collapse = !this.collapse;
this.collapseEvent.emit(this.collapse);
}
}
-10
View File
@@ -1,10 +0,0 @@
import {ModuleWithProviders} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {ViewerComponent} from './viewer/viewer.component';
const SCREENSHOT_ROUTES: Routes = [
{path: '', component: ViewerComponent, pathMatch: 'full'},
{path: ':id', component: ViewerComponent}
];
export const routing: ModuleWithProviders = RouterModule.forRoot(SCREENSHOT_ROUTES);
@@ -1,58 +0,0 @@
const config = require('../config.json');
/**
* Results for a screenshot test
* Has all related information including Travis Job ID, SHA, pull request number.
* And the test results for each test.
*/
export class ScreenshotResultSummary {
/** PR information: the pull request number */
prNumber: string;
/** PR information: the sha of the pull request commit */
sha: string;
/** PR information: The travis job ID */
travis: string;
/** The names of the tests that have screenshot results */
testNames: string[];
/**
* Test result: passed or failed. The value can be true if test failed but
* PR approved by user
*/
allTestsPassedOrApproved: boolean;
/** Test results: passed or failed for each test */
testResultsByName: Map<string, boolean> = new Map<string, boolean>();
/**
* Test approved: whether the test images are copied to goldens.
* The value is the date/time of approval of current commit. Null if never approved.
*/
approvedTime: number;
githubStatus: boolean;
/** Viewing mode, can be flip, diff, side */
mode: 'diff' | 'side' | 'flip' = 'diff';
/** When in "Flip" mode, whether the test result or the gold screenshot is being shown. */
isFlipped: boolean = false;
/** Viewing collapsed: whether the result should be collapsed/expanded */
collapse: boolean[];
setCollapse(value: boolean) {
if (this.collapse) {
for (let i = 0; i < this.collapse.length; i++) {
this.collapse[i] = value;
}
}
}
get prUrl() {
return `https://github.com/${config.repoSlug}/pull/${this.prNumber}`;
}
get commitLink() {
return `https://github.com/${config.repoSlug}/commit/${this.sha}`;
}
get travisLink() {
return `https://travis-ci.org/${config.repoSlug}/jobs/${this.travis}`;
}
}
@@ -1,19 +0,0 @@
.mode-section {
display: flex;
flex-direction: column;
margin: 10px 40px 10px 10px;
}
.description {
font-size: 11px;
color: grey;
padding-left: 20px;
}
.failed {
color: red;
}
.passed {
color: forestgreen;
}
@@ -1,65 +0,0 @@
<div *ngIf="screenshotResultSummary.prNumber">
<mat-card>
<p>
<a mat-button [href]="screenshotResultSummary.prUrl" target="_blank" color="primary">
Pull Request: {{screenshotResultSummary.prNumber}}
</a>
<a mat-button [href]="screenshotResultSummary.commitLink" target="_blank" color="primary">
Commit: {{screenshotResultSummary.sha}}
</a>
<a mat-button [href]="screenshotResultSummary.travisLink" target="_blank" color="primary">
Travis: {{screenshotResultSummary.travis}}
</a>
</p>
<p *ngIf="!isApproved">
<button mat-button (click)="approve()" color="primary" >
Copy To Goldens
</button>
<span class="description">
This will copy all failed test result images to goldens.
Please go to Travis to restart the job manually after approval.
</span>
</p>
<p *ngIf="isApproved">
<button mat-button disabled>Approved.</button>
<button mat-button (click)="approve()">Copy to Goldens (Again)</button>
<span class="description">
Test images should be the same as golden images. Expand all to verify.
</span>
</p>
<p>
<button mat-button
[disabled]="screenshotResultSummary.allTestsPassedOrApproved" color="primary"
(click)="updateGithubStatus()">
Set Github to Success
</button>
<a mat-icon-button *ngIf="githubStatus" [ngClass]="githubClass">
<mat-icon>{{githubIcon}}</mat-icon>
</a>
<a matTooltip="Refresh Github Status" (click)="refreshGithubStatus()" mat-icon-button>
<mat-icon>refresh</mat-icon>
</a>
<br>
<span class="description">
This will not update golds, but only change the screenshot test status on current commit.
Once the PR checked in, the golds will be updated automatically.
</span>
</p>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="screenshotResultSummary.setCollapse(true)">
Collapse All
</button>
<button mat-raised-button color="primary" (click)="screenshotResultSummary.setCollapse(false)">
Expand All
</button>
</mat-card-actions>
</mat-card>
<div *ngFor="let testname of screenshotResultSummary.testNames; let i = index">
<app-result [filename]="testname"
[result]="screenshotResultSummary.testResultsByName.get(testname)"
[(mode)]="screenshotResultSummary.mode"
[(flipping)]="screenshotResultSummary.isFlipped"
[(collapse)]="screenshotResultSummary.collapse[i]"></app-result>
</div>
</div>
@@ -1,67 +0,0 @@
import {Component, Input} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {MatSnackBar} from '@angular/material';
import {FirebaseService} from '../firebase.service';
/**
* Component to show test results for one commit. The github status, pull request and travis job
* information, test result and commit SHA are visible.
* User can approve the change by updating the github status, or by updating the goldens.
*/
@Component({
selector: 'app-viewer',
templateUrl: './viewer.component.html',
styleUrls: ['./viewer.component.css']
})
export class ViewerComponent {
messageDuration = {duration: 10000};
get isApproved() {
return !!this._service.screenshotResultSummary.approvedTime;
}
get screenshotResultSummary() {
return this._service.screenshotResultSummary;
}
get githubIcon(): string {
return this.githubStatus ? 'check' : 'close';
}
@Input()
get githubStatus() {
return this.screenshotResultSummary.githubStatus;
}
get githubClass(): string {
return this.githubStatus ? 'passed' : 'failed';
}
constructor(private _service: FirebaseService,
public snackBar: MatSnackBar,
activatedRoute: ActivatedRoute) {
activatedRoute.params.subscribe(p => {
this._service.prNumber = p['id'];
});
}
approve() {
this._service.approvePullRequest().then(() => {
this.snackBar.open(`Approved`, '', this.messageDuration);
}).catch((error) => {
this.snackBar.open(`Error ${error}`, '', this.messageDuration);
});
}
updateGithubStatus() {
this._service.updatePullRequestResult().then(() => {
this.snackBar.open(`Approved`, '', this.messageDuration);
}).catch((error) => {
this.snackBar.open(error.message, '', this.messageDuration);
});
}
refreshGithubStatus() {
this._service.getGithubStatus();
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"firebase": {
"apiKey": "AIzaSyBekh5ZSi1vEhaE2qetH4RU91gHmUmpqgg",
"authDomain": "material2-screenshots.firebaseapp.com",
"databaseURL": "https://material2-screenshots.firebaseio.com",
"storageBucket": "material2-screenshots.appspot.com",
"messagingSenderId": "975527407245"
},
"repoSlug": "angular/material2"
}
@@ -1,3 +0,0 @@
export const environment = {
production: true
};
@@ -1,8 +0,0 @@
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `angular-cli.json`.
export const environment = {
production: false
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

-16
View File
@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ScreenshotTest</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
-12
View File
@@ -1,12 +0,0 @@
import './polyfills.ts';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {enableProdMode} from '@angular/core';
import {environment} from './environments/environment';
import {PixactoDashboardModule} from './app/pixacto.dashboard.module';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(PixactoDashboardModule);
-13
View File
@@ -1,13 +0,0 @@
// This file includes polyfills needed by Angular and is loaded before
// the app. You can add your own extra polyfills to this file.
import 'core-js/es6/symbol';
import 'core-js/es6/object';
import 'core-js/es6/function';
import 'core-js/es6/number';
import 'core-js/es6/string';
import 'core-js/es6/array';
import 'core-js/es6/map';
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
-1
View File
@@ -1 +0,0 @@
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
-19
View File
@@ -1,19 +0,0 @@
{
"compilerOptions": {
"baseUrl": "",
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"lib": ["es6", "dom"],
"mapRoot": "./",
"module": "es6",
"moduleResolution": "node",
"outDir": "../dist/out-tsc",
"sourceMap": true,
"target": "es5",
"typeRoots": [
"../node_modules/@types"
]
}
}