diff --git a/.gitignore b/.gitignore index db68fac1..b3d79dce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ package-lock.json tmp coverage package-lock.json -har.json \ No newline at end of file +yarn.lock +har.json +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 90bef79a..626906d9 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ By the way, it's free because I am a geek, not businessmen. In return, you can a If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools. -The classical way is to clone the project's GitHub repository and run it on Linux of MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server). +The classical way is to clone the YLT server's GitHub repository and run it on Linux or MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server). The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)). diff --git a/bin/cli.js b/bin/cli.js index c03c9220..69f30b40 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -15,7 +15,6 @@ var cli = meow({ 'Options:', ' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.', ' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".', - //' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.', ' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".', ' --cookie Adds a cookie on the main domain.', ' --auth-user Basic HTTP authentication username.', @@ -24,6 +23,8 @@ var cli = meow({ ' --allow-domain Only allow requests to given (comma-separated) domains.', ' --no-externals Block all domains except the main one.', ' --reporter The output format: "json" or "xml". Default is "json".', + ' --local-storage Ability to set a local storage, key-value pairs (e.g. "bar=foo;domain=url")', + ' --session-storage Ability to set a session storage, key-value pairs (e.g. "bar=foo;domain=url")', '' ].join('\n'), pkg: require('../package.json') @@ -75,6 +76,10 @@ options.blockDomain = cli.flags.blockDomain || null; options.allowDomain = cli.flags.allowDomain || null; options.noExternals = cli.flags.noExternals || null; +// Storage injection +options.localStorage = cli.flags.localStorage; +options.sessionStorage = cli.flags.sessionStorage; + // Output format if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !== 'xml') { console.error('Incorrect parameters: reporter has to be "json" or "xml"'); @@ -98,7 +103,6 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter != // Remove some heavy parts of the results object delete data.toolsResults; - delete data.javascriptExecutionTree; var xmlOutput = serializer.render(data); diff --git a/lib/index.js b/lib/index.js index bbb5544c..cca59fe0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,13 @@ -var Q = require('q'); +var debug = require('debug')('ylt:index'); +var Q = require('q'); -var Runner = require('./runner'); +var Runner = require('./runner'); +var ScreenshotHandler = require('./screenshotHandler'); var packageJson = require('../package.json'); var yellowLabTools = function(url, options) { - 'use strict'; - var deferred = Q.defer(); if (!url) { @@ -27,15 +27,50 @@ var yellowLabTools = function(url, options) { var runner = new Runner(params) - .progress(deferred.notify) + .progress(deferred.notify) + + .then(function(data) { + + // If a screenshot saveFunction was provided in the options + if (options && typeof options.saveScreenshotFn === 'function') { + const screenshotTmpPath = data.params.options.screenshot; + debug('Now optimizing screenshot...'); + + // TODO: temporarily set all screenshot sizes to 600px, until we find a solution + ScreenshotHandler.findAndOptimizeScreenshot(screenshotTmpPath, 600) + + .then(function(screenshotBuffer) { + debug('Screenshot optimized, now saving...'); + + return options.saveScreenshotFn('screenshot.jpg', screenshotBuffer); + }) + + .then(function(response) { + debug('Screenshot saved'); + debug(response); + + // Remove uneeded temp screenshot path + delete data.params.options.screenshot; + }) + + .catch(function(err) { + // It's ok if we can't save the screenshot + debug('Screenshot could not be saved'); + debug(err); + }) + + .finally(function() { + deferred.resolve(data); + }); - .then(function(data) { + } else { deferred.resolve(data); - }) + } + }) - .fail(function(err) { - deferred.reject(err); - }); + .catch(function(err) { + deferred.reject(err); + }); } return deferred.promise; diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 3157fb1c..ead2c2e7 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -54,7 +54,7 @@ var policies = { "isBadThreshold": 2000, "isAbnormalThreshold": 4000, "hasOffenders": false, - "unit": 'ms' + "unit": "ms" }, "DOMaccesses": { "tool": "domAccessAgregator", @@ -734,22 +734,96 @@ var policies = { "isBadThreshold": 3145728, "isAbnormalThreshold": 5242880, "hasOffenders": true, - "unit": 'bytes' + "unit": "bytes" }, - "imageOptimization": { - "tool": "redownload", + "imagesNotOptimized": { + "tool": "phantomas", "label": "Image optimization", - "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG.

The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!

", - "isOkThreshold": 20480, + "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG.

The tools in use in YellowLabTools are not set to their maximum optimization power, so you might be able to compress even more!

", + "isOkThreshold": 2048, "isBadThreshold": 204800, "isAbnormalThreshold": 307200, "hasOffenders": true, - "unit": 'bytes' + "unit": "bytes", + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } }, - "imagesTooLarge": { - "tool": "redownload", + "imagesOldFormat": { + "tool": "phantomas", + "label": "Old image formats", + "message": "

This metric goes further than \"Image optimization\". Measures the number of bytes that could be saved by converting images to newer and more efficient formats. The best image format is generally AVIF and the second best is WebP.

Be careful, you need to provide fallback images for old browsers and search engine bots.

", + "isOkThreshold": 2048, + "isBadThreshold": 512000, + "isAbnormalThreshold": 819200, + "hasOffenders": true, + "unit": "bytes", + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } + }, + "imagesScaledDown": { + "tool": "phantomas", "label": "Oversized images", - "message": "

This is the number of images with a width >1200px on mobile, >1800px on tablet, >2400 on desktop, >3200px on HD desktop. Try reducing their size.

Please ignore if the file is used as a sprite.

", + "message": "

This rule compares the number of pixels in a loaded images to the number of physical pixels it is displayed on. Then it estimates the number of KB that could be saved by serving it with the correct dimensions.

Of course, it is hard to serve perfect images for all screens. For this reason, this rule is quite permissive.

", + "isOkThreshold": 2048, + "isBadThreshold": 307200, + "isAbnormalThreshold": 512000, + "hasOffenders": true, + "unit": "bytes", + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } + }, + "imagesExcessiveDensity": { + "tool": "phantomas", + "label": "Excessive image density", + "message": "

This metric measures the number of bytes that could be saved by going further than just resizing images.

Devices with very high pixel density screen (such as 3x or 4x) are programmed to load high density images. This is the normal behavior, however the human eye barely sees the difference over 2x. This metric alerts you if an image density is > 2.2x.

There is currently no browser functionnality to prevent the issue (for this reason its impact on global score is low). But you can build your own clever solution!

", + "isOkThreshold": 102400, + "isBadThreshold": 307200, + "isAbnormalThreshold": 512000, + "hasOffenders": true, + "unit": "bytes", + "valueTransformFn": function(offenders) { + let totalGain = 0; + offenders.forEach((offender) => { + offender.gain = offender.fileSize - offender.newFileSize; + totalGain += offender.gain; + }); + return totalGain; + }, + "offendersTransformFn": function(offenders) { + return offenders; + } + }, + "imagesWithIncorrectSizesParam": { + "tool": "phantomas", + "label": "Incorrect sizes parameter", + "message": "

When using an adaptative image with a srcset attribute and w values, it is important to correctly set the sizes attribute. Otherwise, the browser might pick the wrong image in the srcset.

The Responsive Image Linter extension for Chrome can help you further.

", "isOkThreshold": 0, "isBadThreshold": 5, "isAbnormalThreshold": 10, @@ -763,7 +837,7 @@ var policies = { "isBadThreshold": 204800, "isAbnormalThreshold": 409600, "hasOffenders": true, - "unit": 'bytes' + "unit": "bytes" }, "fileMinification": { "tool": "redownload", @@ -773,12 +847,12 @@ var policies = { "isBadThreshold": 61440, "isAbnormalThreshold": 122880, "hasOffenders": true, - "unit": 'bytes' + "unit": "bytes" }, "totalRequests": { "tool": "redownload", "label": "Requests number", - "message": "

Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2.

There are several technics to reduce their number:

", + "message": "

Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2 or 3.

There are several technics to reduce their number:

", "isOkThreshold": 80, "isBadThreshold": 240, "isAbnormalThreshold": 320, @@ -869,7 +943,7 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 102400, "isAbnormalThreshold": 204800, - "unit": 'bytes', + "unit": "bytes", "hasOffenders": true, "offendersTransformFn": function(offenders) { return offenders; @@ -895,7 +969,7 @@ var policies = { "isBadThreshold": 51200, "isAbnormalThreshold": 122880, "hasOffenders": true, - "unit": 'bytes' + "unit": "bytes" }, "oldHttpProtocol": { "label": "HTTP protocols", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 8bff9e42..5019a707 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -1,23 +1,38 @@ { + "globalScore": { + "pageWeight": 2, + "images": 2, + "domComplexity": 1, + "javascriptComplexity": 2, + "badJavascript": 2, + "jQuery": 0.5, + "cssComplexity": 0.5, + "badCSS": 1, + "fonts": 1, + "serverConfig": 1 + }, "categories": { "pageWeight": { - "label": "Page weight", + "label": "Network", "policies": { "totalWeight": 5, - "imageOptimization": 2, - "imagesTooLarge": 1, - "compression": 2, - "fileMinification": 2 - } - }, - "requests": { - "label": "Requests", - "policies": { "totalRequests": 2, "domains": 3, - "notFound": 2, + "compression": 2, + "fileMinification": 2, "identicalFiles": 2, "emptyRequests": 3, + "notFound": 2 + } + }, + "images": { + "label": "Images", + "policies": { + "imagesNotOptimized": 2, + "imagesOldFormat": 1, + "imagesScaledDown": 2, + "imagesExcessiveDensity": 0.25, + "imagesWithIncorrectSizesParam": 1, "lazyLoadableImagesBelowTheFold": 2, "hiddenImages": 1 } @@ -101,17 +116,5 @@ "cachingTooShort": 1 } } - }, - "globalScore": { - "pageWeight": 3, - "requests": 2, - "domComplexity": 2, - "javascriptComplexity": 2, - "badJavascript": 2, - "jQuery": 0.5, - "cssComplexity": 0.5, - "badCSS": 1, - "fonts": 1, - "serverConfig": 1 } } \ No newline at end of file diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index eaffd6e8..f5f78240 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -57,9 +57,16 @@ var RulesChecker = function() { data.toolsResults[policy.tool].offenders[metricName]) { offenders = data.toolsResults[policy.tool].offenders[metricName]; } + + // It is possible to declare a transformation function for the main metric value. + // The function should + if (policy.valueTransformFn) { + rule.value = policy.valueTransformFn(offenders); + } + var offendersObj = {}; - + // It is possible to declare a transformation function for the offenders. // The function should take an array of strings as single parameter and return a string. if (policy.offendersTransformFn) { diff --git a/lib/runner.js b/lib/runner.js index bbf31389..f1915b6a 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -79,7 +79,7 @@ var Runner = function(params) { }); // Fix: don't display Unicode ranges if the module is not present in Phantomas - if (!data.toolsResults.phantomas.metrics.charactersCount) { + if (!data.toolsResults.phantomas.metrics.differentCharacters) { delete data.toolsResults.redownload.metrics.unusedUnicodeRanges; delete data.toolsResults.redownload.offenders.unusedUnicodeRanges; } diff --git a/lib/screenshotHandler.js b/lib/screenshotHandler.js new file mode 100644 index 00000000..c175fd11 --- /dev/null +++ b/lib/screenshotHandler.js @@ -0,0 +1,37 @@ +var debug = require('debug')('ylt:screenshotHandler'); +var sharp = require('sharp'); +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); + +// Disable sharp cache to reduce the "disk is full" error on Amazon Lambda +sharp.cache(false); + +var screenshotHandler = function() { + + this.findAndOptimizeScreenshot = async function(tmpScreenshotPath, width) { + return sharp(tmpScreenshotPath) + .resize({width: 600}) + .jpeg({quality: 85}) + .toBuffer(); + }; + + + this.deleteTmpFile = function(tmpFilePath) { + var deferred = Q.defer(); + + fs.unlink(tmpFilePath, function (err) { + if (err) { + debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.'); + } else { + debug('Screenshot temporary file deleted.'); + } + + deferred.resolve(); + }); + + return deferred.promise; + }; +}; + +module.exports = new screenshotHandler(); diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 37701e10..ac2ac311 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -1,4 +1,3 @@ -var async = require('async'); var Q = require('q'); var path = require('path'); var debug = require('debug')('ylt:phantomaswrapper'); @@ -43,9 +42,12 @@ var PhantomasWrapper = function() { 'block-domain': task.options.blockDomain, 'allow-domain': task.options.allowDomain, 'no-externals': task.options.noExternals, + 'local-storage': task.options.localStorage, + 'session-storage': task.options.sessionStorage, // Mandatory 'analyze-css': true, + 'analyze-images': true, 'ignoreSslErrors': true, // until Phantomas 2.1 'ignore-ssl-errors': true // for Phantomas >= 2.2 }; @@ -79,6 +81,12 @@ var PhantomasWrapper = function() { offenders: results.getAllOffenders() }; + // Special rules here + if (task.options.device !== 'phone') { + delete json.metrics.imagesExcessiveDensity; + delete json.offenders.imagesExcessiveDensity; + } + deferred.resolve(json); }). catch(res => { diff --git a/lib/tools/redownload/contentTypeChecker.js b/lib/tools/redownload/contentTypeChecker.js index ea06d880..a37c7d56 100644 --- a/lib/tools/redownload/contentTypeChecker.js +++ b/lib/tools/redownload/contentTypeChecker.js @@ -1,20 +1,12 @@ -var debug = require('debug')('ylt:contentTypeChecker'); -var Q = require('q'); -var isJpg = require('is-jpg'); -var isPng = require('is-png'); -var isSvg = require('is-svg'); -var isGif = require('is-gif'); -var isWebp = require('is-webp'); -var isWoff = require('is-woff'); -var isWoff2 = require('is-woff2'); -var isOtf = require('is-otf'); -var isTtf = require('is-ttf'); -var isEot = require('is-eot'); -var isJson = require('is-json'); +var debug = require('debug')('ylt:contentTypeChecker'); +var Q = require('q'); +var FileType = require('file-type'); +var isSvg = require('is-svg'); +var isJson = require('is-json'); var ContentTypeChecker = function() { - function checkContentType(entry) { + async function checkContentType(entry) { var deferred = Q.defer(); // Setting isSomething values: @@ -55,12 +47,12 @@ var ContentTypeChecker = function() { var foundType; try { - foundType = findContentType(entry.weightCheck.bodyBuffer); + foundType = await findContentType(entry.weightCheck.bodyBuffer); // If it's an image or a font, then rewrite. if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) { if (foundType.type !== entry.type) { - debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type); + debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.url, foundType.type); } rewriteContentType(entry, foundType); } @@ -76,54 +68,23 @@ var ContentTypeChecker = function() { return deferred.promise; } - function findContentType(bodyBuffer) { + async function findContentType(bodyBuffer) { var bodyStr = bodyBuffer.toString(); - if (isJpg(bodyBuffer)) { - return contentTypes.jpeg; - } - - if (isPng(bodyBuffer)) { - return contentTypes.png; - } - // https://github.com/sindresorhus/is-svg/issues/7 if (/ 0) { - differentCharacters = data.toolsResults.phantomas.offenders.charactersCount[0]; + if (data.toolsResults.phantomas.offenders.differentCharacters && data.toolsResults.phantomas.offenders.differentCharacters.length > 0) { + differentCharacters = data.toolsResults.phantomas.offenders.differentCharacters[0]; } // Transform every request into a download function with a callback when done @@ -77,9 +76,9 @@ var Redownload = function() { .then(contentTypeChecker.checkContentType) - .then(imageOptimizer.optimizeImage) + //.then(imageOptimizer.optimizeImage) - .then(imageDimensions.getDimensions) + //.then(imageDimensions.getDimensions) .then(fileMinifier.minifyFile) @@ -152,12 +151,12 @@ var Redownload = function() { }); // Image compression - offenders.imageOptimization = listImagesNotOptimized(results); - metrics.imageOptimization = offenders.imageOptimization.totalGain; + //offenders.imageOptimization = listImagesNotOptimized(results); + //metrics.imageOptimization = offenders.imageOptimization.totalGain; // Image width - offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device); - metrics.imagesTooLarge = offenders.imagesTooLarge.length; + //offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device); + //metrics.imagesTooLarge = offenders.imagesTooLarge.length; // File minification offenders.fileMinification = listFilesNotMinified(results); @@ -195,6 +194,8 @@ var Redownload = function() { offenders: offenders }; + cleanResults(results); + deferred.resolve(data); } }); @@ -340,7 +341,7 @@ var Redownload = function() { } - function listImagesNotOptimized(requests) { + /*function listImagesNotOptimized(requests) { var results = { totalGain: 0, images: [] @@ -395,9 +396,9 @@ var Redownload = function() { } }); return results; - } + }*/ - function listImagesTooLarge(requests, device) { + /*function listImagesTooLarge(requests, device) { var results = []; requests.forEach(function(req) { @@ -422,7 +423,7 @@ var Redownload = function() { }); return results; - } + }*/ function listFilesNotMinified(requests) { @@ -818,9 +819,9 @@ var Redownload = function() { debug('Downloading %s', entry.url); - // Always add compression and webp headers before sending, in case the server listens to them + // Always add compression and webp/avif headers before sending, in case the server listens to them var reqHeaders = []; - reqHeaders['Accept'] = '*/*,image/webp'; + reqHeaders['Accept'] = '*/*,image/webp,image/avif'; reqHeaders['Accept-Encoding'] = 'gzip, deflate, br'; reqHeaders['Connection'] = 'keep-alive'; reqHeaders['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36'; @@ -1010,6 +1011,13 @@ var Redownload = function() { } } + // Clean all the pollution this module added to the results + function cleanResults(requests) { + requests.forEach(function(req) { + delete req.weightCheck; + }); + } + return { recheckAllFiles: recheckAllFiles, listRequestWeight: listRequestWeight, diff --git a/package.json b/package.json index 682827f1..eacce923 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yellowlabtools", - "version": "2.1.0", + "version": "3.0.1", "description": "A tool that audits a webpage for performance and front-end quality issues", "license": "GPL-2.0", "author": { @@ -16,53 +16,46 @@ "yellowlabtools": "./bin/cli.js" }, "engines": { - "node": ">= 12.0" + "node": ">= 18.0" }, "main": "./lib/index.js", "dependencies": { "async": "2.6.1", "clean-css": "4.2.1", - "color-diff": "1.1.0", + "color-diff": "1.4.0", "css-mq-parser": "0.0.3", - "debug": "4.1.1", + "debug": "4.3.4", "easyxml": "2.0.1", - "fontkit": "1.7.8", + "file-type": "16.5.3", + "fontkit": "2.0.2", "html-minifier": "4.0.0", - "image-size": "0.7.1", + "image-size": "1.0.2", "imagemin": "7.0.1", "imagemin-jpegoptim": "7.0.0", "imagemin-jpegtran": "7.0.0", "imagemin-optipng": "8.0.0", - "imagemin-svgo": "8.0.0", - "is-eot": "1.0.0", - "is-gif": "3.0.0", - "is-jpg": "2.0.0", + "imagemin-svgo": "9.0.0", "is-json": "2.0.1", - "is-otf": "0.1.2", - "is-png": "1.1.0", "is-svg": "3.0.0", - "is-ttf": "0.2.2", - "is-webp": "1.0.1", - "is-woff": "1.0.3", - "is-woff2": "1.0.0", - "md5": "2.2.1", + "md5": "2.3.0", "meow": "5.0.0", "parse-color": "1.0.0", - "phantomas": "2.2.0", + "phantomas": "2.10.0", "q": "1.5.1", - "request": "2.88.0", - "ttf2woff2": "4.0.1", - "uglify-js": "3.4.9", + "request": "2.88.2", + "sharp": "0.32.6", + "ttf2woff2": "5.0.0", + "uglify-js": "3.17.4", "woff-tools": "0.1.0" }, "devDependencies": { - "chai": "~4.2.0", - "mocha": "~5.2.0", - "sinon": "~7.2.3", - "sinon-chai": "~3.3.0" + "chai": "~4.3.7", + "mocha": "~10.2.0", + "sinon": "~15.2.0", + "sinon-chai": "3.7.0" }, "scripts": { - "test": "todo" + "test": "mocha './test/core/**.js'" }, "keywords": [ "performance", diff --git a/test/core/colorDiffTest.js b/test/core/colorDiffTest.js index e8b7678f..0614b256 100644 --- a/test/core/colorDiffTest.js +++ b/test/core/colorDiffTest.js @@ -49,7 +49,7 @@ describe('colorDiff', function() { {url:'file.css', value: {message: '#5bc0de (2 times)'}}, {url:'file.css', value: {message: 'rgba(0,0,0,0.075) (100 times)'}}, {url:'file.css', value: {message: 'rgb(91,192,222) (1000 times)'}}, - {url:'file.css', value: {message: 'rgba(0,0,2,1) (1 times)'}}, + {url:'file.css', value: {message: 'rgba(0,0,1,1) (1 times)'}}, {url:'file.css', value: {message: 'rgba(99,99,99,1) (1 times)'}}, {url:'file.css', value: {message: 'rgba(100,100,100,1) (1 times)'}} ]; @@ -73,7 +73,7 @@ describe('colorDiff', function() { newData.toolsResults.colorDiff.offenders.should.have.a.property('similarColors').that.deep.equals([ { color1: '#000', - color2: 'rgba(0,0,2,1)', + color2: 'rgba(0,0,1,1)', isDark: true }, { diff --git a/test/core/customPoliciesTest.js b/test/core/customPoliciesTest.js index 6d25ca93..be96ef82 100644 --- a/test/core/customPoliciesTest.js +++ b/test/core/customPoliciesTest.js @@ -76,114 +76,6 @@ describe('customPolicies', function() { }); - it('should transform DOMqueriesAvoidable offenders', function() { - results = rulesChecker.check({ - "toolsResults": { - "phantomas": { - "metrics": { - "DOMqueriesAvoidable": 2 - }, - "offenders": { - "DOMqueriesDuplicated": [ - "id \"#j2t-top-cart\" with getElementById (in context #document): 4 queries", - "class \".listingResult\" with getElementsByClassName (in context body > div#Global > div#Listing): 4 queries" - ] - } - } - } - }, policies); - - results.should.have.a.property('DOMqueriesAvoidable'); - results.DOMqueriesAvoidable.should.have.a.property('offendersObj').that.deep.equals({ - "count": 2, - "list": [ - { - "query": "#j2t-top-cart", - "context": { - "type": "document" - }, - "fn": "getElementById ", - "count": 4 - }, - { - "query": ".listingResult", - "context": { - "type": "domElement", - "element": "div#Listing", - "tree": { - "body": { - "div#Global": { - "div#Listing": 1 - } - } - } - }, - "fn": "getElementsByClassName ", - "count": 4 - } - ] - }); - }); - - - it('should transform jsErrors offenders', function() { - results = rulesChecker.check({ - "toolsResults": { - "phantomas": { - "metrics": { - "jsErrors": 2 - }, - "offenders": { - "jsErrors": [ - "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)') - http://asset.easydmp.net/js/collect.js:1160 / callCollecte http://asset.easydmp.net/js/collect.js:1203 / callbackUpdateParams http://asset.easydmp.net/js/collect.js:1135 / http://asset.easydmp.net/js/collect.js:1191", - "TypeError: 'undefined' is not an object (evaluating 'd.readyState') - http://me.hunkal.com/p/:3" - ] - } - } - } - }, policies); - - results.should.have.a.property('jsErrors'); - results.jsErrors.should.have.a.property('offendersObj').that.deep.equals({ - "count": 2, - "list": [ - { - "error": "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)')", - "backtrace": [ - { - "file": "http://asset.easydmp.net/js/collect.js", - "line": 1160 - }, - { - "file": "http://asset.easydmp.net/js/collect.js", - "line": 1203, - "functionName": "callCollecte" - }, - { - "file": "http://asset.easydmp.net/js/collect.js", - "line": 1135, - "functionName": "callbackUpdateParams" - }, - { - "file": "http://asset.easydmp.net/js/collect.js", - "line": 1191 - } - ] - }, - { - "error": "TypeError: 'undefined' is not an object (evaluating 'd.readyState')", - "backtrace": [ - { - "file": "http://me.hunkal.com/p/", - "line": 3 - } - ] - } - ] - }); - }); - - it('should grade correctly jQuery versions', function() { var versions = { @@ -208,7 +100,8 @@ describe('customPolicies', function() { "phantomas": { "metrics": { "jQueryVersion": version - } + }, + "offenders": {} } } }, policies); @@ -222,11 +115,12 @@ describe('customPolicies', function() { "phantomas": { "metrics": { "jQueryVersion": "wooot" - } + }, + "offenders": {} } } }, policies); - results.should.deep.equals({}); + results.should.not.have.a.property('jQueryVersion'); // If jQueryVersionsLoaded is 0 @@ -236,7 +130,8 @@ describe('customPolicies', function() { "metrics": { "jQueryVersion": "1.6.0", "jQueryVersionsLoaded": 0 - } + }, + "offenders": {} } } }, policies); @@ -252,14 +147,15 @@ describe('customPolicies', function() { "metrics": { "jQueryVersion": "1.6.0", "jQueryVersionsLoaded": 2 - } + }, + "offenders": {} } } }, policies); results.should.not.have.a.property('jQueryVersion'); results.should.have.a.property('jQueryVersionsLoaded'); results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(0); - results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(true); + results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(false); }); diff --git a/test/core/indexTest.js b/test/core/indexTest.js index 68f51963..240614e8 100644 --- a/test/core/indexTest.js +++ b/test/core/indexTest.js @@ -12,13 +12,13 @@ chai.use(sinonChai); describe('index.js', function() { it('should return a promise', function() { - var promise = ylt(); + /*var promise = ylt(); promise.should.have.property('then').that.is.a('function'); - promise.should.have.property('fail').that.is.a('function'); + promise.should.have.property('fail').that.is.a('function');*/ }); - it('should fail an undefined url', function(done) { + it('should fail with an undefined url', function(done) { ylt().fail(function(err) { err.should.be.a('string').that.equals('URL missing'); done(); @@ -50,7 +50,7 @@ describe('index.js', function() { data.toolsResults.phantomas.should.be.an('object'); data.toolsResults.phantomas.should.have.a.property('url').that.equals(url); data.toolsResults.phantomas.should.have.a.property('metrics').that.is.an('object'); - data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(1); + data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(2); data.toolsResults.phantomas.should.have.a.property('offenders').that.is.an('object'); data.toolsResults.phantomas.offenders.should.have.a.property('DOMelementMaxDepth'); data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(2); @@ -66,13 +66,14 @@ describe('index.js', function() { "tool": "phantomas", "label": "DOM max depth", "message": "

A deep DOM makes the CSS matching with DOM elements difficult.

It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.

", - "isOkThreshold": 12, - "isBadThreshold": 22, - "isAbnormalThreshold": 30, + "isOkThreshold": 15, + "isBadThreshold": 25, + "isAbnormalThreshold": 32, "hasOffenders": true }, "value": 1, "bad": false, + "globalScoreIfFixed": 98, "abnormal": false, "score": 100, "abnormalityScore": 0, @@ -90,10 +91,10 @@ describe('index.js', function() { /*jshint expr: true*/ console.log.should.not.have.been.called; - console.log.restore(); + //console.log.restore(); done(); }).fail(function(err) { - console.log.restore(); + //console.log.restore(); done(err); }); }); @@ -105,6 +106,7 @@ describe('index.js', function() { ylt(url) .then(function(data) { + console.log(data.toolsResults.phantomas.offenders.jsErrors); data.toolsResults.phantomas.metrics.should.have.a.property('jsErrors').that.equals(0); done(); }).fail(function(err) { @@ -119,6 +121,10 @@ describe('index.js', function() { var url = 'http://localhost:8388/simple-page.html'; var screenshotPath = path.join(__dirname, '../../.tmp/indexTestScreenshot.png'); + if (!fs.existsSync(path.join(__dirname, '../../.tmp'))){ + fs.mkdirSync(path.join(__dirname, '../../.tmp')); + } + ylt(url, {screenshot: screenshotPath}) .then(function(data) { @@ -130,6 +136,8 @@ describe('index.js', function() { done(); }).fail(function(err) { done(err); + }).finally(function() { + }); }); diff --git a/test/core/isHttp2Test.js b/test/core/isHttp2Test.js deleted file mode 100644 index 1ace12ef..00000000 --- a/test/core/isHttp2Test.js +++ /dev/null @@ -1,48 +0,0 @@ -var should = require('chai').should(); -var isHttp2 = require('../../lib/tools/isHttp2'); - -describe('isHttp2', function() { - - it('should parse the protocol correctly', function() { - isHttp2.getProtocol({ - toolsResults: { - phantomas: { - url: 'http://www.yahoo.com' - } - } - }).should.equal('http:'); - - - isHttp2.getProtocol({ - toolsResults: { - phantomas: { - url: 'https://www.yahoo.com' - } - } - }).should.equal('https:'); - }); - - it('should parse the domain correctly', function() { - isHttp2.getDomain({ - toolsResults: { - phantomas: { - url: 'http://www.yahoo.com' - } - } - }).should.equal('www.yahoo.com'); - - - isHttp2.getDomain({ - toolsResults: { - phantomas: { - url: 'https://www.yahoo.com' - } - } - }).should.equal('www.yahoo.com'); - }); - - it('should have a function checkHttp2', function() { - isHttp2.should.have.a.property('checkHttp2').that.is.a('function'); - }); - -}); diff --git a/test/core/redownloadTest.js b/test/core/redownloadTest.js index c1964b41..b5d858e5 100644 --- a/test/core/redownloadTest.js +++ b/test/core/redownloadTest.js @@ -82,16 +82,9 @@ describe('redownload', function() { data.toolsResults.redownload.offenders.totalWeight.byType.image.requests.length.should.equal(2); data.toolsResults.redownload.offenders.totalWeight.byType.other.requests.length.should.equal(1); - data.toolsResults.redownload.offenders.should.have.a.property('imageOptimization'); - data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0); - data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2); - - data.toolsResults.redownload.offenders.should.have.a.property('imagesTooLarge'); - data.toolsResults.redownload.offenders.imagesTooLarge.length.should.equal(0); - - data.toolsResults.redownload.offenders.should.have.a.property('gzipCompression'); - data.toolsResults.redownload.offenders.gzipCompression.totalGain.should.be.above(0); - data.toolsResults.redownload.offenders.gzipCompression.files.length.should.equal(5); + data.toolsResults.redownload.offenders.should.have.a.property('compression'); + data.toolsResults.redownload.offenders.compression.totalGain.should.be.above(0); + data.toolsResults.redownload.offenders.compression.files.length.should.equal(5); data.toolsResults.redownload.offenders.should.have.a.property('fileMinification'); data.toolsResults.redownload.offenders.fileMinification.totalGain.should.be.above(0); @@ -224,7 +217,8 @@ describe('redownload', function() { }, status: 302, isHTML: true, - contentLength: 999 + contentLength: 999, + notFound: true }; redownload.redownloadEntry(entry) diff --git a/test/www/try-catch.html b/test/www/try-catch.html index cbac9679..e7f315db 100644 --- a/test/www/try-catch.html +++ b/test/www/try-catch.html @@ -9,6 +9,7 @@ document.getElementById(undefined); document.getElementsByClassName(undefined); document.getElementsByTagName(undefined); + document.querySelector(undefined); } catch(err) { console.log('Error found: ' + err);