diff --git a/.gitignore b/.gitignore index d95eeab..09965e3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ tmp/* yarn-error.log test.js .vscode +test/screenshots +test/*.css \ No newline at end of file diff --git a/README.md b/README.md index df51f2d..b3d88a5 100644 --- a/README.md +++ b/README.md @@ -177,23 +177,24 @@ The CLI usage is not implemented yet :scream:. At the moment there is no need of ## Options -| Property | Values | Description | -| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **urls** | Array | An array containing the urls to check the css against. Has to be at least 1 url. | -| css | String | Optional. Can be a plain css string or path to a css file or empty. If it is a path it has to end with `.css`! Otherwise it is not recognized as a path. If not set, the whole website css will be taken. | -| timeout | Number | Optional. Integer number of milliseconds to wait for a page to navigate to. After timeout is reached the page navigation is aborted. **ATTENTION**: The critical css of the url timed out is not included. Default: 30000 | -| pageLoadTimeout | Number | Optional. After the page load event is fired the pageLoadTimeout is started. After the amount of milliseconds the ongoing loading of assets or xhr requests is stopped and the extraction continues. Default: 2000 | -| outputRemainingCss | Boolean | Optional. If set to false the result obj will not contain any rest css. Only an empty string will be given. Default: true | -| browser | Object | Optional. Configuration object of browser. E.g. userAgent, ... See documentation for [browser object](#browser-options). | -| device | Object | Optional. Configuration object of device. E.g. width, height, ... See documentation for [device object](#device-options). | -| puppeteer | Object | Optional. Configuration object of puppeteer options like an already existing browser instance or a path to a Chrome instead of the used Chromium. See documentation for [puppeteer object](#puppeteer-options). | -| printBrowserConsole | Boolean | Optional. If set to true prints console output of urls to the stdoutput. Defaults: false | -| dropKeyframes | Boolean | Optional. If set to false keeps keyframes as critical css content. Otherwise they are removed. Default: false | -| takeScreenshots | Boolean | Optional. If set a screenshot is taken for every url processed. Default: false | -| screenshotPath | String | Optional. The path the screenshots will be saved to. Default: "." (execution path) | -| keepSelectors | Array | Optional. Every CSS Selector in this array will be kept as part of the critical css even if they are not part of it. You can use wildcards (%) to capture more rules with one entry. [Read more](#wildcards). Default: [] | -| removeSelectors: | Array | Optional. Every CSS Selector in this array will be removed of the critical css even if they are part of it. You can use wildcards (%) to capture more rules with one entry. [Read more](#wildcards). Default: [] | -| blockRequests | Array | Optional. Some of the requests made by pages are an | +| Property | Values | Description | +| ----------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **urls** | Array | An array containing the urls to check the css against. Has to be at least 1 url. | +| css | String | Optional. Can be a plain css string or path to a css file or empty. If it is a path it has to end with `.css`! Otherwise it is not recognized as a path. If not set, the whole website css will be taken. | +| timeout | Number | Optional. Integer number of milliseconds to wait for a page to navigate to. After timeout is reached the page navigation is aborted. **ATTENTION**: The critical css of the url timed out is not included. Default: 30000 | +| pageLoadTimeout | Number | Optional. After the page load event is fired the pageLoadTimeout is started. After the amount of milliseconds the ongoing loading of assets or xhr requests is stopped and the extraction continues. Default: 2000 | +| outputRemainingCss | Boolean | Optional. If set to false the result obj will not contain any rest css. Only an empty string will be given. Default: true | +| browser | Object | Optional. Configuration object of browser. E.g. userAgent, ... See documentation for [browser object](#browser-options). | +| device | Object | Optional. Configuration object of device. E.g. width, height, ... See documentation for [device object](#device-options). | +| puppeteer | Object | Optional. Configuration object of puppeteer options like an already existing browser instance or a path to a Chrome instead of the used Chromium. See documentation for [puppeteer object](#puppeteer-options). | +| printBrowserConsole | Boolean | Optional. If set to true prints console output of urls to the stdoutput. Defaults: false | +| dropKeyframes | Boolean | Optional. If set to false keeps keyframes as critical css content. Otherwise they are removed. Default: false | +| takeScreenshots | Boolean | Optional. If set a screenshot is taken for every url processed. Default: false | +| screenshotPath | String | Optional. The path the screenshots will be saved to. Default: "." (execution path) | +| screenshotNameGenerator | (url: string) => Promise | Optional. Function that receives the processed URL as an argument and returns a `Promise` that resolves with the name to be used for the screenshot. When not provided, the URL's name will be sanitized and used as a filename. | +| keepSelectors | Array | Optional. Every CSS Selector in this array will be kept as part of the critical css even if they are not part of it. You can use wildcards (%) to capture more rules with one entry. [Read more](#wildcards). Default: [] | +| removeSelectors: | Array | Optional. Every CSS Selector in this array will be removed of the critical css even if they are part of it. You can use wildcards (%) to capture more rules with one entry. [Read more](#wildcards). Default: [] | +| blockRequests | Array | Optional. Some of the requests made by pages are an | ### Browser options diff --git a/lib/Constants.js b/lib/Constants.js index 02e4352..365f8c8 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -28,4 +28,6 @@ module.exports = { // CODE BASED RULE_SEPARATOR: '-#-', + + SCREENSHOT_NAME_GENERATOR: null, }; diff --git a/lib/classes/Crittr.class.js b/lib/classes/Crittr.class.js index b472ec1..d666c15 100644 --- a/lib/classes/Crittr.class.js +++ b/lib/classes/Crittr.class.js @@ -80,6 +80,7 @@ class Crittr { dropKeyframes: DEFAULTS.DROP_KEYFRAMES, takeScreenshots: DEFAULTS.PAGE_SCREENSHOT, screenshotPath: path.join('.'), + screenshotNameGenerator: DEFAULTS.SCREENSHOT_NAME_GENERATOR, keepSelectors: [], removeSelectors: [], blockRequests: [ @@ -723,7 +724,14 @@ class Crittr { debug('evaluateUrl - Extracting critical selectors'); await page.waitForTimeout(250); // Needed because puppeteer sometimes isn't able to handle quick tab openings if (this.options.takeScreenshots === true) { - const screenName = url.replace(/[^\w\s]/gi, '_') + '.png'; + let screenName = url.replace(/[^\w\s]/gi, '_') + '.png'; + if (typeof this.options.screenshotNameGenerator === 'function') { + const screenGeneratedName = await this.options.screenshotNameGenerator(url); + screenName = `${screenGeneratedName}.png`; + } + + await fs.mkdirp(this.options.screenshotPath); + await page.screenshot({ path: path.join(this.options.screenshotPath, screenName), type: 'png', diff --git a/test/setup.js b/test/setup.js index 55eda58..373fa64 100644 --- a/test/setup.js +++ b/test/setup.js @@ -2,6 +2,7 @@ const Critter = require('../index'); const fs = require('fs-extra'); const path = require('path'); const merge = require('deepmerge'); +const crypto = require('crypto'); const rootDir = path.join(__dirname, '..'); const staticServer = require('../lib/helper/localFileServer')(rootDir); @@ -39,6 +40,36 @@ const noCssOptions = merge(defaultOptions, { css: null, }); +const screenshotOptions = merge(defaultOptions, { + urls: [ + 'http://localhost:8000/test/data/test.html?1', + 'http://localhost:8000/test/data/test.html?2', + 'http://localhost:8000/test/data/test.html?3', + 'http://localhost:8000/test/data/test.html?4', + ], + css: testData.css, + takeScreenshots: true, + screenshotPath: path.join(__dirname, 'screenshots', 'normal'), +}); + +const screenNameGenerator = async (url) => { + const sha1HashGen = crypto.createHash('sha1'); + sha1HashGen.update(url); + return sha1HashGen.digest('hex'); +} +const screenshotWithFunctionOptions = merge(defaultOptions, { + urls: [ + 'http://localhost:8000/test/data/test.html?1', + 'http://localhost:8000/test/data/test.html?2', + 'http://localhost:8000/test/data/test.html?3', + 'http://localhost:8000/test/data/test.html?4', + ], + css: testData.css, + takeScreenshots: true, + screenshotPath: path.join(__dirname, 'screenshots', 'withFunction'), + screenshotNameGenerator: screenNameGenerator, +}); + module.exports = () => { return new Promise(async (resolve, reject) => { const server = staticServer @@ -57,6 +88,20 @@ module.exports = () => { fs.writeFileSync(path.join(rootDir, './test/test_result_noCss_remaining.css'), noCssRest, 'utf-8'); + // Third Run for URL + let { critical: screenshotCssCritical, rest: screenshotCssRest } = await Critter(screenshotOptions); + + fs.writeFileSync(path.join(rootDir, './test/test_result_screenshotCss.css'), screenshotCssCritical, 'utf-8'); + + fs.writeFileSync(path.join(rootDir, './test/test_result_screenshotCss_remaining.css'), screenshotCssRest, 'utf-8'); + + // Fourth Run for URL + let { critical: screenshotWithFunctionCssCritical, rest: screenshotWithFunctionCssRest } = await Critter(screenshotWithFunctionOptions); + + fs.writeFileSync(path.join(rootDir, './test/test_result_screenshotWithFunctionCss.css'), screenshotWithFunctionCssCritical, 'utf-8'); + + fs.writeFileSync(path.join(rootDir, './test/test_result_screenshotWithFunctionCss_remaining.css'), screenshotWithFunctionCssRest, 'utf-8'); + resolve(); } catch (err) { reject(err); diff --git a/test/teardown.js b/test/teardown.js index cf1becd..df79f63 100644 --- a/test/teardown.js +++ b/test/teardown.js @@ -3,19 +3,11 @@ const path = require('path'); const rootDir = path.join(__dirname, '..'); module.exports = async function () { - fs.unlink(path.join(rootDir, './test/test_result.css'), err => { - if (err) throw err; - }); - - fs.unlink(path.join(rootDir, './test/test_result_remaining.css'), err => { - if (err) throw err; - }); - - fs.unlink(path.join(rootDir, './test/test_result_noCss.css'), err => { - if (err) throw err; - }); - - fs.unlink(path.join(rootDir, './test/test_result_noCss_remaining.css'), err => { - if (err) throw err; - }); + // Cleans up artifacts (`.css` files and `screenshots` folder from `test`) + const files = await fs.readdir(__dirname); + await Promise.all( + files + .filter(fileOrFolder => fileOrFolder.endsWith('.css') || fileOrFolder === 'screenshots') + .map(fileOrFolder => fs.remove(path.join('./test/', fileOrFolder))), + ); }; diff --git a/test/tests/screenshot.test.js b/test/tests/screenshot.test.js new file mode 100644 index 0000000..07bb534 --- /dev/null +++ b/test/tests/screenshot.test.js @@ -0,0 +1,40 @@ +const fs = require('fs-extra'); +const path = require('path'); +const crypto = require('crypto'); + +const rootDir = path.join(__dirname, '..', '..'); +const helpers = require('./../helpers.js'); +const Rule = require(path.join(rootDir, 'lib/classes/Rule.class')); + +const urls = [ + 'http://localhost:8000/test/data/test.html?1', + 'http://localhost:8000/test/data/test.html?2', + 'http://localhost:8000/test/data/test.html?3', + 'http://localhost:8000/test/data/test.html?4', +]; + +describe('Screenshots', () => { + describe('Check screenshot names', () => { + test('Check that the normal screenshots were generated with the name based on the URL', async () => { + const files = await fs.readdir(path.join(__dirname, '..', 'screenshots', 'normal')); + expect( + urls.every(url => { + const expectedScreenName = url.replace(/[^\w\s]/gi, '_') + '.png'; + return files.includes(expectedScreenName); + }) + ).toBeTruthy(); + }); + + test('Check that the screenshots with a name generator function were generated with the name as the URL SHA1 hashed', async () => { + const files = await fs.readdir(path.join(__dirname, '..', 'screenshots', 'withFunction')); + expect( + urls.every(url => { + const sha1 = crypto.createHash('sha1'); + sha1.update(url); + const expectedScreenName = `${sha1.digest('hex')}.png`; + return files.includes(expectedScreenName); + }) + ).toBeTruthy(); + }); + }); +});