Преглед изворни кода

Add a gzip compression task

Gaël Métais пре 10 година
родитељ
комит
baa145e15c

+ 101 - 0
lib/tools/weightChecker/gzipCompressor.js

@@ -0,0 +1,101 @@
+var debug = require('debug')('ylt:gzipCompressor');
+
+var Q       = require('q');
+var zlib    = require('zlib');
+
+var GzipCompressor = function() {
+
+    function compressFile(entry) {
+        return gzipUncompressedFile(entry)
+
+        .then(gzipOptimizedFile);
+    }
+
+    // Gzip a file if it was not already gziped
+    function gzipUncompressedFile(entry) {
+        var deferred = Q.defer();
+
+        if (isCompressible(entry) && !entry.weightCheck.isCompressed) {
+            debug('Compression missing, trying to gzip file %s', entry.url);
+
+            var uncompressedSize = entry.weightCheck.uncompressedSize;
+
+            zlib.gzip(new Buffer(entry.weightCheck.body, 'utf8'), function(err, buffer) {
+                if (err) {
+                    debug('Could not compress uncompressed file with gzip');
+                    debug(err);
+
+                    deferred.reject(err);
+                } else {
+                    var compressedSize = buffer.length;
+
+                    if (gainIsEnough(uncompressedSize, compressedSize)) {
+                        debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
+
+                        entry.weightCheck.afterCompression = compressedSize;
+                        deferred.resolve(entry);
+                    } else {
+                        debug('Gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
+                    }
+                }
+            });
+        } else {
+            deferred.resolve(entry);
+        }
+
+        return deferred.promise;
+    }
+
+    // Gzip a file after minification or optimization if this step was successful
+    function gzipOptimizedFile(entry) {
+        var deferred = Q.defer();
+
+        if (isCompressible(entry) && !entry.weightCheck.isOptimized && !entry.weightCheck.isMinified) {
+            debug('Trying to gzip file after minification: %s', entry.url);
+
+            var uncompressedSize = entry.weightCheck.optimized || entry.weightCheck.minified;
+
+            zlib.gzip(new Buffer(entry.weightCheck.bodyAfterMinification, 'utf8'), function(err, buffer) {
+                if (err) {
+                    debug('Could not compress minified file with gzip');
+                    debug(err);
+
+                    deferred.reject(err);
+                } else {
+                    var compressedSize = buffer.length;
+
+                    if (gainIsEnough(uncompressedSize, compressedSize)) {
+                        debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
+
+                        entry.weightCheck.afterOptimizationAndCompression = compressedSize;
+                        deferred.resolve(entry);
+                    } else {
+                        debug('Gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
+                    }
+                }
+            });
+        } else {
+            deferred.resolve(entry);
+        }
+
+        return deferred.promise;
+    }
+
+    function isCompressible(entry) {
+        return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
+    }
+
+    // The gain is estimated of enough value if it's over 1KB or over 20%,
+    // but it's ignored if is below 100 bytes
+    function gainIsEnough(oldWeight, newWeight) {
+        var gain = oldWeight - newWeight;
+        var ratio = gain / oldWeight;
+        return (gain > 2048 || (ratio > 0.2 && gain > 100));
+    }
+
+    return {
+        compressFile: compressFile
+    };
+};
+
+module.exports = new GzipCompressor();

+ 3 - 0
lib/tools/weightChecker/weightChecker.js

@@ -14,6 +14,7 @@ var request         = require('request');
 
 
 var imageOptimizer  = require('./imageOptimizer');
 var imageOptimizer  = require('./imageOptimizer');
 var fileMinifier    = require('./fileMinifier');
 var fileMinifier    = require('./fileMinifier');
+var gzipCompressor  = require('./gzipCompressor');
 
 
 
 
 var WeightChecker = function() {
 var WeightChecker = function() {
@@ -42,6 +43,8 @@ var WeightChecker = function() {
 
 
                 .then(fileMinifier.minifyFile)
                 .then(fileMinifier.minifyFile)
 
 
+                .then(gzipCompressor.compress)
+
                 .then(function(newEntry) {
                 .then(function(newEntry) {
                     callback(null, newEntry);
                     callback(null, newEntry);
                 })
                 })

+ 586 - 0
test/core/gzipCompressorTest.js

@@ -0,0 +1,586 @@
+var should = require('chai').should();
+var gzipCompressor = require('../../lib/tools/weightChecker/gzipCompressor');
+var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
+var fs = require('fs');
+var path = require('path');
+
+describe('gzipCompressor', function() {
+
+    var minifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/minified-script.js'));
+    var notMinifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
+    var someTextFileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
+
+    
+    it('should gzip a JS file that was not gziped but was minified', function(done) {
+        var fileContent = minifiedJSContent;
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/minified-script.js',
+            status: 200,
+            isJS: true,
+            type: 'js',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip a JS file that was not gziped and not minified', function(done) {
+        /*jshint expr: true*/
+
+        var fileContent = notMinifiedJSContent;
+        var minifiedContent = minifiedJSContent;
+
+        var fileSize = fileContent.length;
+        var minifiedSize = minifiedContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/unminified-script.js',
+            status: 200,
+            isJS: true,
+            type: 'js',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                bodyAfterMinification: minifiedContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: false,
+                minified: minifiedSize
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            delete newEntry.weightCheck.body;
+            delete newEntry.weightCheck.bodyAfterMinification;
+            console.log(newEntry.weightCheck);
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined;
+            newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(newEntry.weightCheck.afterCompression);
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip a JS file that is gziped but not minified', function(done) {
+        /*jshint expr: true*/
+
+        var fileContent = notMinifiedJSContent;
+        var minifiedContent = minifiedJSContent;
+        var fileSize = 6436;
+        var gzipedSize = 2646;
+        var minifiedSize = 1954;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/unminified-script.js',
+            status: 200,
+            isJS: true,
+            type: 'js',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                bodyAfterMinification: minifiedContent.toString('utf8'),
+                totalWeight: gzipedSize + 200,
+                headersSize: 200,
+                bodySize: gzipedSize,
+                isCompressed: true,
+                uncompressedSize: fileSize,
+                isMinified: false,
+                minified: minifiedSize
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined;
+            newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(gzipedSize);
+            newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(minifiedSize);
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should not gzip a JS file that was gziped and minified', function(done) {
+        /*jshint expr: true*/
+
+        var fileContent = notMinifiedJSContent;
+        var fileSize = 6436;
+        var gzipedSize = 2646;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/unminified-script.js',
+            status: 200,
+            isJS: true,
+            type: 'js',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: gzipedSize + 200,
+                headersSize: 200,
+                bodySize: gzipedSize,
+                isCompressed: true,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.not.have.a.property('minified');
+            newEntry.weightCheck.should.not.have.a.property('bodyAfterMinification');
+            newEntry.weightCheck.should.not.have.a.property('afterCompression');
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip a CSS file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/unminified-stylesheet.css',
+            status: 200,
+            isCSS: true,
+            type: 'css',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip an HTML file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/jquery-page.html',
+            status: 200,
+            isHTML: true,
+            type: 'html',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip an SVG file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/svg-image.svg',
+            status: 200,
+            isImage: true,
+            isSVG: true,
+            type: 'image',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip an XML file', function(done) {
+        var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/someTextFile.xml',
+            status: 200,
+            isXML: true,
+            type: 'xml',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip a JSON file', function(done) {
+        var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/someTextFile.json',
+            status: 200,
+            isJSON: true,
+            type: 'json',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should gzip a TTF file', function(done) {
+        var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/someTextFile.ttf',
+            status: 200,
+            isWebFont: true,
+            isTTF: true,
+            type: 'webfont',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+
+    it('should gzip a favicon file', function(done) {
+        var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/someTextFile.ico',
+            status: 200,
+            isFavicon: true,
+            type: 'favicon',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isMinified: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should not gzip a JPEG file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/jpeg-image.jpg',
+            status: 200,
+            isImage: true,
+            type: 'image',
+            contentType: 'image/jpeg',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isOptimized: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.not.have.a.property('afterCompression');
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+
+    it('should not gzip a PNG file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/png-image.png',
+            status: 200,
+            isImage: true,
+            type: 'image',
+            contentType: 'image/png',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isOptimized: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.not.have.a.property('afterCompression');
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should not gzip a GIF file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake gif, don't tell anyone...
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/gif-image.gif',
+            status: 200,
+            isImage: true,
+            type: 'image',
+            contentType: 'image/gif',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isOptimized: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.not.have.a.property('afterCompression');
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+    it('should not gzip a WEBP file', function(done) {
+        var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake webp, don't tell anyone...
+        var fileSize = fileContent.length;
+
+        var entry = {
+            method: 'GET',
+            url: 'http://localhost:8388/webp-image.webp',
+            status: 200,
+            isImage: true,
+            type: 'image',
+            contentType: 'image/webp',
+            contentLength: 999,
+            weightCheck: {
+                body: fileContent.toString('utf8'),
+                totalWeight: fileSize + 200,
+                headersSize: 200,
+                bodySize: fileSize,
+                isCompressed: false,
+                uncompressedSize: fileSize,
+                isOptimized: true
+            }
+        };
+
+        gzipCompressor.compressFile(entry)
+
+        .then(function(newEntry) {
+            newEntry.weightCheck.should.not.have.a.property('afterCompression');
+            newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
+
+            done();
+        })
+
+        .fail(function(err) {
+            done(err);
+        });
+    });
+
+});

+ 14 - 0
test/www/minified-script.js

@@ -0,0 +1,14 @@
+var timelineCtrl=angular.module("timelineCtrl",[])
+timelineCtrl.controller("TimelineCtrl",["$scope","$rootScope","$routeParams","$location","$timeout","Menu","Results","API",function(e,t,n,r,i,a,l,u){function o(){t.loadedResult&&t.loadedResult.runId===n.runId?(e.result=t.loadedResult,c()):l.get({runId:n.runId,exclude:"toolsResults"},function(n){t.loadedResult=n,e.result=n,c()})}function c(){s(),d(),f(),m(),i(p,100)}function s(){var t=r.hash(),n=null
+0===t.indexOf("filter=")&&(n=t.substr(7)),e.warningsFilterOn=null!==n,e.warningsFilters={queryWithoutResults:null===n||"queryWithoutResults"===n,jQueryCallOnEmptyObject:null===n||"jQueryCallOnEmptyObject"===n,eventNotDelegated:null===n||"eventNotDelegated"===n,jsError:null===n||"jsError"===n}}function d(){var t=e.result.rules.jsCount.offendersObj.list
+e.scripts=[],t.forEach(function(t){var n=t.file
+n.length>100&&(n=n.substr(0,98)+"...")
+var r={fullPath:t.file,shortPath:n}
+e.scripts.push(r)})}function f(){var t=e.result.javascriptExecutionTree.children||[],n=t[t.length-1]
+e.endTime=n.data.timestamp+(n.data.time||0),e.executionTree=[],t.forEach(function(t){if(e.selectedScript){if(t.data.backtrace&&-1===t.data.backtrace.indexOf(e.selectedScript.fullPath+":"))return
+if("jQuery loaded"===t.data.type||"jQuery version change"===t.data.type)return}e.executionTree.push(t)})}function m(){var t=199
+e.timelineIntervalDuration=e.endTime/t
+var n=Array.apply(null,Array(e.endTime+1)).map(Number.prototype.valueOf,0)
+e.executionTree.forEach(function(e){if(void 0!==e.data.time)for(var t=Math.min(e.data.time,100)||1,r=e.data.timestamp,i=e.data.timestamp+t;i>r;r++)n[r]|=1}),e.timeline=Array.apply(null,Array(t+1)).map(Number.prototype.valueOf,0),n.forEach(function(t,n){1===t&&(e.timeline[Math.floor(n/e.timelineIntervalDuration)]+=1)}),e.timelineMax=Math.max.apply(Math,e.timeline)}function p(){e.profilerData=e.executionTree}e.runId=n.runId,e.Menu=a.setCurrentPage("timeline",e.runId),e.changeScript=function(){f(),m(),p()},e.findLineIndexByTimestamp=function(t){for(var n=0,r=0;r<e.executionTree.length;r++){var i=e.executionTree[r].data.timestamp-t
+if(i<e.timelineIntervalDuration&&(n=r),i>0)break}return n},e.backToDashboard=function(){r.path("/result/"+e.runId)},e.testAgain=function(){u.relaunchTest(e.result)},o()}]),timelineCtrl.directive("scrollOnClick",["$animate","$timeout",function(e,t){return{restrict:"A",link:function(n,r,i){r.on("click",function(){var r=n.findLineIndexByTimestamp(i.scrollOnClick),a=angular.element(document.getElementById("line_"+r))
+a.addClass("highlight"),t(function(){e.removeClass(a,"highlight"),n.$digest()},50),window.scrollTo(0,a[0].offsetTop)})}}}])