mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
dc0732bc2c
commit
afb0ee37eb
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
));
|
||||
|
||||
/**
|
||||
|
||||
@@ -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, '/');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
});
|
||||
}
|
||||
@@ -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}`));
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Generated
-11083
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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 +0,0 @@
|
||||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user