mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(screenshot): add control panel for pixacto (#4055)
This commit is contained in:
committed by
Jeremy Elbourn
parent
ee1a5a7aa3
commit
b2ca0f5d84
@@ -2,7 +2,7 @@ import gulp = require('gulp');
|
||||
import {execNodeTask} from '../util/task_helpers';
|
||||
import {DIST_MATERIAL} from '../constants';
|
||||
|
||||
gulp.task('lint', ['tslint', 'stylelint', 'madge']);
|
||||
gulp.task('lint', ['tslint', 'stylelint', 'madge', 'dashboardlint']);
|
||||
|
||||
/** Task that runs madge to detect circular dependencies. */
|
||||
gulp.task('madge', ['library:clean-build'], execNodeTask('madge', ['--circular', DIST_MATERIAL]));
|
||||
@@ -12,5 +12,11 @@ gulp.task('stylelint', execNodeTask(
|
||||
'stylelint', ['src/**/*.scss', '--config', 'stylelint-config.json', '--syntax', 'scss']
|
||||
));
|
||||
|
||||
gulp.task('dashboardlint', execNodeTask(
|
||||
'stylelint', ['tools/screenshot-test/**/*.css', '--config', 'stylelint-config.json',
|
||||
'--syntax', 'scss']
|
||||
));
|
||||
|
||||
/** Task to run TSLint against the e2e/ and src/ directories. */
|
||||
gulp.task('tslint', execNodeTask('tslint', ['-c', 'tslint.json', '+(src|e2e|tools)/**/*.ts']));
|
||||
gulp.task('tslint', execNodeTask('tslint', ['-c', 'tslint.json', '+(src|e2e|tools)/**/*.ts',
|
||||
'--exclude', '**/node_modules/**/*']));
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"project": {
|
||||
"version": "1.0.0-beta.26",
|
||||
"name": "screenshot-test"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "app",
|
||||
"mobile": false,
|
||||
"styles": [
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environment": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaults": {
|
||||
"styleExt": "css",
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "screenshot-test",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^4.0.2",
|
||||
"@angular/common": "^4.0.0",
|
||||
"@angular/compiler": "^4.0.0",
|
||||
"@angular/compiler-cli": "^4.0.0",
|
||||
"@angular/core": "^4.0.0",
|
||||
"@angular/forms": "^4.0.0",
|
||||
"@angular/http": "^4.0.0",
|
||||
"@angular/material": "^2.0.0-beta.3",
|
||||
"@angular/platform-browser": "^4.0.0",
|
||||
"@angular/platform-browser-dynamic": "^4.0.0",
|
||||
"@angular/router": "^4.0.0",
|
||||
"core-js": "^2.4.1",
|
||||
"firebase": "^3.7.6",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"zone.js": "^0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.0.0",
|
||||
"@angular/compiler-cli": "^4.0.0",
|
||||
"@types/node": "^6.0.42",
|
||||
"ts-node": "1.2.1",
|
||||
"typescript": "~2.2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Http} from '@angular/http';
|
||||
import * as firebase from 'firebase';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
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 _http: Http) {
|
||||
// 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): firebase.Promise<string> {
|
||||
return this._storageRef().child('test').child(filename).getDownloadURL();
|
||||
}
|
||||
|
||||
/** Get the firebase storage diff image folder url */
|
||||
getDiffScreenshotImageUrl(filename: string): firebase.Promise<string> {
|
||||
return this._storageRef().child('diff').child(filename).getDownloadURL();
|
||||
}
|
||||
|
||||
/** Get the firebase storage golden image folder url */
|
||||
getGoldScreenshotImageUrl(filename: string): firebase.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++;
|
||||
if (counter === snapshot.numChildren()) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
signIntoGithub(): firebase.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) => {
|
||||
this._addTestResults(resultSnapshot.key, resultSnapshot.val());
|
||||
childCounter++;
|
||||
if (childCounter === childSnapshot.numChildren()) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_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._http.get(url).toPromise()
|
||||
.then((response) => {
|
||||
let statusResponse = response.json();
|
||||
let screenshotStatus = statusResponse.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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<md-toolbar color="primary">
|
||||
<div *ngIf="user">{{user.email}}</div>
|
||||
<button md-button (click)="signIntoGithub()" *ngIf="!user">Sign In with Github</button>
|
||||
<button md-button (click)="signOutFromGithub()" *ngIf="user">Sign out</button>
|
||||
</md-toolbar>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<app-nav></app-nav>
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
})
|
||||
export class PixactoDashboardComponent {}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {HttpModule} from '@angular/http';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MaterialModule} from '@angular/material';
|
||||
import {FirebaseService} from './firebase.service';
|
||||
import {routing} from './routes';
|
||||
|
||||
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({
|
||||
declarations: [
|
||||
PixactoDashboardComponent,
|
||||
ViewerComponent,
|
||||
ResultComponent,
|
||||
NavComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
HttpModule,
|
||||
MaterialModule,
|
||||
routing,
|
||||
],
|
||||
providers: [FirebaseService],
|
||||
bootstrap: [PixactoDashboardComponent]
|
||||
})
|
||||
export class PixactoDashboardModule { }
|
||||
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<md-card>
|
||||
<md-card-header>
|
||||
<md-card-title>
|
||||
<md-icon (click)="toggleCollapse()">{{collapseIcon}}</md-icon>
|
||||
{{testName}}
|
||||
</md-card-title>
|
||||
<md-card-subtitle [ngClass]="resultText.toLowerCase()">{{resultText}}</md-card-subtitle>
|
||||
</md-card-header>
|
||||
<md-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>
|
||||
</md-card-content>
|
||||
<md-card-actions>
|
||||
<md-button-toggle-group [(ngModel)]="mode" class="mode-section">
|
||||
<md-button-toggle value="diff">
|
||||
Diff
|
||||
</md-button-toggle>
|
||||
<md-button-toggle value="flip">
|
||||
Flip
|
||||
</md-button-toggle>
|
||||
<md-button-toggle value="side">
|
||||
Side by side
|
||||
</md-button-toggle>
|
||||
</md-button-toggle-group>
|
||||
<button md-button color="primary" (click)="flip()" *ngIf="mode === 'flip'">Flip</button>
|
||||
</md-card-actions>
|
||||
</md-card>
|
||||
@@ -0,0 +1,126 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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);
|
||||
@@ -0,0 +1,58 @@
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<div *ngIf="screenshotResultSummary.prNumber">
|
||||
<md-card>
|
||||
<p>
|
||||
<a md-button [href]="screenshotResultSummary.prUrl" target="_blank" color="primary">
|
||||
Pull Request: {{screenshotResultSummary.prNumber}}
|
||||
</a>
|
||||
<a md-button [href]="screenshotResultSummary.commitLink" target="_blank" color="primary">
|
||||
Commit: {{screenshotResultSummary.sha}}
|
||||
</a>
|
||||
<a md-button [href]="screenshotResultSummary.travisLink" target="_blank" color="primary">
|
||||
Travis: {{screenshotResultSummary.travis}}
|
||||
</a>
|
||||
</p>
|
||||
<p *ngIf="!isApproved">
|
||||
<button md-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 md-button disabled>Approved.</button>
|
||||
<button md-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 md-button
|
||||
[disabled]="screenshotResultSummary.allTestsPassedOrApproved" color="primary"
|
||||
(click)="updateGithubStatus()">
|
||||
Set Github to Success
|
||||
</button>
|
||||
<a md-icon-button *ngIf="githubStatus" [ngClass]="githubClass">
|
||||
<md-icon>{{githubIcon}}</md-icon>
|
||||
</a>
|
||||
<a md-tooltip="Refresh Github Status" (click)="refreshGithubStatus()" md-icon-button>
|
||||
<md-icon>refresh</md-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>
|
||||
<md-card-actions>
|
||||
<button md-raised-button color="primary" (click)="screenshotResultSummary.setCollapse(true)">
|
||||
Collapse All
|
||||
</button>
|
||||
<button md-raised-button color="primary" (click)="screenshotResultSummary.setCollapse(false)">
|
||||
Expand All
|
||||
</button>
|
||||
</md-card-actions>
|
||||
</md-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>
|
||||
@@ -0,0 +1,67 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {MdSnackBar} 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,
|
||||
private _route: ActivatedRoute,
|
||||
public snackBar: MdSnackBar) {
|
||||
_route.params.subscribe(p => {
|
||||
this._service.prNumber = p['id'];
|
||||
});
|
||||
}
|
||||
|
||||
approve() {
|
||||
this._service.approvePullRequest().then((result) => {
|
||||
this.snackBar.open(`Approved`, '', this.messageDuration);
|
||||
}).catch((error) => {
|
||||
this.snackBar.open(`Error ${error}`, '', this.messageDuration);
|
||||
});
|
||||
}
|
||||
|
||||
updateGithubStatus() {
|
||||
this._service.updatePullRequestResult().then((result) => {
|
||||
this.snackBar.open(`Approved`, '', this.messageDuration);
|
||||
}).catch((error) => {
|
||||
this.snackBar.open(error.message, '', this.messageDuration);
|
||||
});
|
||||
}
|
||||
|
||||
refreshGithubStatus() {
|
||||
this._service.getGithubStatus();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"firebase": {
|
||||
"apiKey": "AIzaSyBekh5ZSi1vEhaE2qetH4RU91gHmUmpqgg",
|
||||
"authDomain": "material2-screenshots.firebaseapp.com",
|
||||
"databaseURL": "https://material2-screenshots.firebaseio.com",
|
||||
"storageBucket": "material2-screenshots.appspot.com",
|
||||
"messagingSenderId": "975527407245"
|
||||
},
|
||||
"repoSlug": "angular/material2"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<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>
|
||||
@@ -0,0 +1,12 @@
|
||||
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);
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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';
|
||||
@@ -0,0 +1 @@
|
||||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "",
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": 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