From 12373a43c261a786d8e597fefd835ef036e51982 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 19:59:07 +0200 Subject: [PATCH 01/10] Replace PhantomJS with Puppeteer --- .gitignore | 1 + .npmrc | 1 + .travis.yml | 1 - index.js | 111 ++++++++++++++--------------------------- license | 22 ++------- package.json | 112 ++++++++++++++++++----------------------- readme.md | 39 ++------------- screenshot.js | 97 ++++++++++++++++++++++++++++++++++++ stream.js | 127 ----------------------------------------------- test.js | 134 +++++++------------------------------------------- 10 files changed, 211 insertions(+), 434 deletions(-) create mode 100644 .npmrc create mode 100644 screenshot.js delete mode 100644 stream.js diff --git a/.gitignore b/.gitignore index 3c3629e..239ecff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml index 57505cf..c58cf9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,3 @@ language: node_js node_js: - '8' - '6' - - '4' diff --git a/index.js b/index.js index 0185d6e..2782e55 100644 --- a/index.js +++ b/index.js @@ -1,85 +1,48 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); -const urlMod = require('url'); -const base64Stream = require('base64-stream'); -const parseCookiePhantomjs = require('parse-cookie-phantomjs'); -const phantomBridge = require('phantom-bridge'); -const byline = require('byline'); - -const handleCookies = (cookies, url) => { - const parsedUrl = urlMod.parse(url); - - return (cookies || []).map(x => { - const ret = typeof x === 'string' ? parseCookiePhantomjs(x) : x; - - if (!ret.domain) { - ret.domain = parsedUrl.hostname; - } - - if (!ret.path) { - ret.path = parsedUrl.path; - } - - return ret; - }); -}; +const parseResolution = require('parse-resolution'); +const puppeteer = require('puppeteer'); +const Screenshot = require('./screenshot'); module.exports = (url, size, opts) => { + const {width, height} = parseResolution(size); + opts = Object.assign({ - delay: 0, + cookies: [], + format: 'png', + fullPage: true, + hide: [], scale: 1, - format: 'png' + viewport: {} }, opts); - const args = Object.assign(opts, { - url, - width: size.split(/x/i)[0] * opts.scale, - height: size.split(/x/i)[1] * opts.scale, - cookies: handleCookies(opts.cookies, url), - format: opts.format === 'jpg' ? 'jpeg' : opts.format, - css: /\.css$/.test(opts.css) ? fs.readFileSync(opts.css, 'utf8') : opts.css, - script: /\.js$/.test(opts.script) ? fs.readFileSync(opts.script, 'utf8') : opts.script - }); - - const cp = phantomBridge(path.join(__dirname, 'stream.js'), [ - '--ignore-ssl-errors=true', - '--local-to-remote-url-access=true', - '--ssl-protocol=any', - JSON.stringify(args) - ]); - - const stream = base64Stream.decode(); - - process.stderr.setMaxListeners(0); - - cp.stderr.setEncoding('utf8'); - cp.stdout.pipe(stream); - - byline(cp.stderr).on('data', data => { - data = data.trim(); - - if (/ phantomjs\[/.test(data)) { - return; - } - - if (/http:\/\/requirejs.org\/docs\/errors.html#mismatch/.test(data)) { - return; - } - - if (data.startsWith('WARN: ')) { - stream.emit('warning', data.replace(/^WARN: /, '')); - stream.emit('warn', data.replace(/^WARN: /, '')); // TODO: deprecate this event in v5 - return; - } + opts.type = opts.format === 'jpg' ? 'jpeg' : opts.format; - if (data.length > 0) { - const err = new Error(data); - err.noStack = true; - cp.stdout.unpipe(stream); - stream.emit('error', err); - } + opts.viewport = Object.assign({}, opts.viewport, { + width, + height }); - return stream; + if (opts.crop) { + opts.fullPage = false; + } + + if (opts.scale !== 1) { + opts.viewport.deviceScaleFactor = opts.scale; + } + + if (opts.transparent) { + opts.omitBackground = true; + } + + return puppeteer.launch() + .then(browser => browser.newPage() + .then(page => new Screenshot(browser, page))) + .then(Screenshot => Screenshot.authenticate(opts.username, opts.password) + .then(() => Screenshot.setCookie(opts.cookies)) + .then(() => Screenshot.setHeaders(opts.headers)) + .then(() => Screenshot.setUserAgent(opts.userAgent)) + .then(() => Screenshot.open(url, opts)) + .then(() => Screenshot.hideElements(opts.hide)) + .then(() => Screenshot.getRect(opts.selector)) + .then(clip => Screenshot.screenshot(Object.assign(opts, clip)))); }; diff --git a/license b/license index a8ecbbe..db6bc32 100644 --- a/license +++ b/license @@ -1,21 +1,9 @@ -The MIT License (MIT) +MIT License -Copyright (c) Kevin Mårtensson +Copyright (c) Kevin Mårtensson (github.com/kevva) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json index ab3ea78..0504523 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,49 @@ { - "name": "screenshot-stream", - "version": "4.2.0", - "description": "Capture screenshot of a website and return it as a stream", - "license": "MIT", - "repository": "kevva/screenshot-stream", - "author": { - "name": "Kevin Mårtensson", - "email": "kevinmartensson@gmail.com", - "url": "github.com/kevva" - }, - "engines": { - "node": ">=4" - }, - "scripts": { - "test": "xo && ava" - }, - "files": [ - "index.js", - "stream.js" - ], - "keywords": [ - "image", - "page", - "phantom", - "phantomjs", - "resolution", - "screen", - "screenshot", - "size", - "stream", - "url" - ], - "dependencies": { - "base64-stream": "^0.1.2", - "byline": "^4.2.1", - "object-assign": "^4.0.1", - "parse-cookie-phantomjs": "^1.0.0", - "phantom-bridge": "^2.0.0" - }, - "devDependencies": { - "ava": "*", - "cookie": "^0.3.1", - "get-port": "^3.1.0", - "get-stream": "^3.0.0", - "image-size": "^0.5.4", - "is-jpg": "^1.0.0", - "is-png": "^1.0.0", - "pify": "^3.0.0", - "png-js": "^0.1.1", - "xo": "*" - }, - "xo": { - "esnext": true, - "overrides": [ - { - "files": "stream.js", - "esnext": false, - "rules": { - "import/no-extraneous-dependencies": 0, - "import/no-unresolved": 0, - "no-multi-assign": 0 - } - } - ] - } + "name": "screenshot-stream", + "version": "4.2.0", + "description": "Capture screenshot of a website and return it as a stream", + "license": "MIT", + "repository": "kevva/screenshot-stream", + "author": { + "name": "Kevin Mårtensson", + "email": "kevinmartensson@gmail.com", + "url": "github.com/kevva" + }, + "engines": { + "node": ">=6.4" + }, + "scripts": { + "test": "xo && ava" + }, + "files": [ + "index.js" + ], + "keywords": [ + "image", + "page", + "phantom", + "phantomjs", + "resolution", + "screen", + "screenshot", + "size", + "stream", + "url" + ], + "dependencies": { + "file-url": "^2.0.2", + "is-url-superb": "^2.0.0", + "parse-resolution": "^1.0.0", + "puppeteer": "^0.11.0", + "tough-cookie": "^2.3.3" + }, + "devDependencies": { + "ava": "*", + "image-size": "^0.6.1", + "is-jpg": "^1.0.0", + "is-png": "^1.1.0", + "pify": "^3.0.0", + "png-js": "^0.1.1", + "xo": "*" + } } diff --git a/readme.md b/readme.md index 7e7507e..08910dc 100644 --- a/readme.md +++ b/readme.md @@ -16,9 +16,9 @@ $ npm install --save screenshot-stream const fs = require('fs'); const screenshot = require('screenshot-stream'); -const stream = screenshot('http://google.com', '1024x768', {crop: true}); - -stream.pipe(fs.createWriteStream('google.com-1024x768.png')); +screenshot('http://google.com', '1024x768', {crop: true}).then(data => { + fs.writeFileSync('google.com-1024x768.png'); +}); ``` @@ -51,19 +51,12 @@ Default: `false` Crop to the set height. -##### delay - -Type: `number` *(seconds)*
-Default: `0` - -Delay capturing the screenshot. Useful when the site does things after load that you want to capture. - ##### timeout Type: `number` *(seconds)*
Default: `60` -Number of seconds after which PhantomJS aborts the request. +Number of seconds after which Google Chrome aborts the request. ##### selector @@ -71,18 +64,6 @@ Type: `string` Capture a specific DOM element. -##### css - -Type: `string` - -Apply custom CSS to the webpage. Specify some CSS or the path to a CSS file. - -##### script - -Type: `string` - -Apply custom JavaScript to the webpage. Specify some JavaScript or the path to a file. - ##### hide Type: `Array` @@ -141,18 +122,6 @@ Default: `false` Set background color to `transparent` instead of `white` if no background is set. -#### .on('error', callback) - -Type: `Function` - -PhantomJS errors. - -#### .on('warning', callback) - -Type: `Function` - -Warnings with for example page errors. - ## CLI diff --git a/screenshot.js b/screenshot.js new file mode 100644 index 0000000..e23ecd3 --- /dev/null +++ b/screenshot.js @@ -0,0 +1,97 @@ +const toughCookie = require('tough-cookie'); +const fileUrl = require('file-url'); +const isUrl = require('is-url-superb'); + +module.exports = class Screenshot { + constructor(browser, page) { + this.browser = browser; + this.page = page; + } + + authenticate(username, password) { + if (!username || !password) { + return Promise.resolve(); + } + + return this.page.authenticate({username, password}); + } + + getRect(selector) { + if (!selector) { + return Promise.resolve(); + } + + return this.page.waitForSelector(selector, {visible: true}).then(() => this.page.$eval(selector, el => { + const {x, y, width, height} = el.getBoundingClientRect(); + + return { + fullPage: false, + clip: { + width, + height, + x, + y + } + }; + })); + } + + hideElements(selectors) { + if (!Array.isArray(selectors) || selectors.length === 0) { + return Promise.resolve(); + } + + return Promise.all(selectors.map(x => this.page.$eval(x, el => { + el.style.visibility = 'hidden'; + }))); + } + + open(uri, opts) { + if (!uri) { + return Promise.resolve(); + } + + const url = isUrl(uri) ? uri : fileUrl(uri); + + return Promise.all([ + this.page.setViewport(opts.viewport), + this.page.goto(url, opts) + ]); + } + + screenshot(opts) { + return this.page.screenshot(opts) + .then(buf => this.browser.close() + .then(() => buf)); + } + + setCookie(cookies) { + if (!Array.isArray(cookies) || cookies.length === 0) { + return Promise.resolve(); + } + + return Promise.all(cookies.map(x => { + const cookie = typeof x === 'string' ? toughCookie.parse(x).toJSON() : x; + + return this.page.setCookie(Object.assign(cookie, { + name: cookie.name || cookie.key + })); + })); + } + + setHeaders(headers) { + if (typeof headers !== 'object') { + return Promise.resolve(); + } + + return this.page.setExtraHTTPHeaders(headers); + } + + setUserAgent(userAgent) { + if (!userAgent) { + return Promise.resolve(); + } + + return this.page.setUserAgent(userAgent); + } +}; diff --git a/stream.js b/stream.js deleted file mode 100644 index 11434b1..0000000 --- a/stream.js +++ /dev/null @@ -1,127 +0,0 @@ -/* global phantom, document, window, btoa */ -'use strict'; -var system = require('system'); -var page = require('webpage').create(); -var objectAssign = require('object-assign'); - -var opts = JSON.parse(system.args[1]); -var log = console.log; - -function formatTrace(trace) { - var src = trace.file || trace.sourceURL; - var fn = (trace.function ? ' in function ' + trace.function : ''); - return ' → ' + src + ' on line ' + trace.line + fn; -} - -console.log = console.error = function () { - system.stderr.writeLine([].slice.call(arguments).join(' ')); -}; - -if (opts.username && opts.password) { - opts.headers = objectAssign({}, opts.headers, { - Authorization: 'Basic ' + btoa(opts.username + ':' + opts.password) - }); -} - -if (opts.userAgent) { - page.settings.userAgent = opts.userAgent; -} - -page.settings.resourceTimeout = (opts.timeout || 60) * 1000; - -phantom.cookies = opts.cookies; - -phantom.onError = function (err, trace) { - err = err.replace(/\n/g, ''); - console.error('PHANTOM ERROR: ' + err + formatTrace(trace[0])); - phantom.exit(1); -}; - -page.onError = function (err, trace) { - err = err.replace(/\n/g, ''); - console.error('WARN: ' + err + formatTrace(trace[0])); -}; - -page.onResourceError = function (resourceError) { - console.error('WARN: Unable to load resource #' + resourceError.id + ' (' + resourceError.errorString + ') → ' + resourceError.url); -}; - -page.onResourceTimeout = function (resourceTimeout) { - console.error('Resource timed out #' + resourceTimeout.id + ' (' + resourceTimeout.errorString + ') → ' + resourceTimeout.url); - phantom.exit(1); -}; - -page.viewportSize = { - width: opts.width, - height: opts.height -}; - -page.customHeaders = opts.headers || {}; -page.zoomFactor = opts.scale; - -page.open(opts.url, function (status) { - if (status === 'fail') { - console.error('Couldn\'t load url: ' + opts.url); - phantom.exit(1); - return; - } - - if (opts.crop) { - page.clipRect = { - top: 0, - left: 0, - width: opts.width, - height: opts.height - }; - } - - page.evaluate(function (css, transparent) { - var bgColor = window - .getComputedStyle(document.body) - .getPropertyValue('background-color'); - - if (!bgColor || bgColor === 'rgba(0, 0, 0, 0)') { - document.body.style.backgroundColor = transparent ? 'transparent' : 'white'; - } - - if (css) { - var el = document.createElement('style'); - el.appendChild(document.createTextNode(css)); - document.head.appendChild(el); - } - }, opts.css, opts.transparent); - - window.setTimeout(function () { - if (opts.hide) { - page.evaluate(function (els) { - els.forEach(function (el) { - [].forEach.call(document.querySelectorAll(el), function (e) { - e.style.visibility = 'hidden'; - }); - }); - }, opts.hide); - } - - if (opts.selector) { - var clipRect = page.evaluate(function (el) { - return document - .querySelector(el) - .getBoundingClientRect(); - }, opts.selector); - - clipRect.height *= page.zoomFactor; - clipRect.width *= page.zoomFactor; - clipRect.top *= page.zoomFactor; - clipRect.left *= page.zoomFactor; - - page.clipRect = clipRect; - } - - if (opts.script) { - page.evaluateJavaScript('function () { ' + opts.script + '}'); - } - - log.call(console, page.renderBase64(opts.format)); - phantom.exit(); - }, opts.delay * 1000); -}); diff --git a/test.js b/test.js index 54c5171..dda9b88 100644 --- a/test.js +++ b/test.js @@ -3,190 +3,94 @@ import test from 'ava'; import imageSize from 'image-size'; import isJpg from 'is-jpg'; import isPng from 'is-png'; -import PNG from 'png-js'; -import getStream from 'get-stream'; import pify from 'pify'; +import PNG from 'png-js'; import server from './fixtures/server'; import m from '.'; test('generate screenshot', async t => { - const stream = m('http://yeoman.io', '1024x768'); - t.true(isPng(await getStream.buffer(stream))); + t.true(isPng(await m('http://yeoman.io', '1024x768'))); }); test('crop image using the `crop` option', async t => { - const stream = m('http://yeoman.io', '1024x768', {crop: true}); - const size = imageSize(await getStream.buffer(stream)); + const size = imageSize(await m('http://yeoman.io', '1024x768', {crop: true})); t.is(size.width, 1024); t.is(size.height, 768); }); test('capture a DOM element using the `selector` option', async t => { - const stream = m('http://yeoman.io', '1024x768', { - selector: '.page-header' - }); - - const size = imageSize(await getStream.buffer(stream)); + const size = imageSize(await m('http://yeoman.io', '1024x768', {selector: '.page-header'})); t.is(size.width, 1024); t.is(size.height, 80); }); -test('capture a DOM element using the `selector` option only after delay', async t => { +test('wait for DOM element when using the `selector` option', async t => { const fixture = path.join(__dirname, 'fixtures', 'test-delay-element.html'); - const stream = m(fixture, '1024x768', { - selector: 'div', - delay: 5 - }); - - const size = imageSize(await getStream.buffer(stream)); + const size = imageSize(await m(fixture, '1024x768', {selector: 'div'})); t.is(size.width, 300); t.is(size.height, 200); }); test('hide elements using the `hide` option', async t => { const fixture = path.join(__dirname, 'fixtures', 'test-hide-element.html'); - const stream = m(fixture, '100x100', {hide: ['div']}); - const png = new PNG(await getStream.buffer(stream)); + const png = new PNG(await m(fixture, '100x100', {hide: ['div']})); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 255); }); -test('ignore multiline page errors', async t => { - const fixture = path.join(__dirname, 'fixtures', 'test-error-script.html'); - const stream = m(fixture, '100x100'); - t.true(isPng(await getStream.buffer(stream))); -}); - test('auth using the `username` and `password` options', async t => { - const stream = m('http://httpbin.org/basic-auth/user/passwd', '1024x768', { + t.true(isPng(await m('http://httpbin.org/basic-auth/user/passwd', '1024x768', { username: 'user', password: 'passwd' - }); - - const data = await pify(stream.once.bind(stream), {errorFirst: false})('data'); - t.truthy(data.length); -}); - -test('have a `delay` option', async t => { - const now = new Date(); - const stream = m('http://yeoman.io', '1024x768', {delay: 2}); - await pify(stream.once.bind(stream), {errorFirst: false})('data'); - - t.true((new Date()) - now > 2000); + }))); }); -test('have a `dpi` option', async t => { - const stream = m('http://yeoman.io', '1024x768', { +test('have a `scale` option', async t => { + const size = imageSize(await m('http://yeoman.io', '1024x768', { crop: true, scale: 2 - }); + })); - const size = imageSize(await getStream.buffer(stream)); t.is(size.width, 1024 * 2); t.is(size.height, 768 * 2); }); test('have a `format` option', async t => { - const stream = m('http://yeoman.io', '1024x768', {format: 'jpg'}); - t.true(isJpg(await getStream.buffer(stream))); -}); - -test('have a `css` option', async t => { - const stream = m('http://yeoman.io', '1024x768', {css: '.mobile-bar { background-color: red !important; }'}); - const png = new PNG(await getStream.buffer(stream)); - const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 255); - t.is(pixels[1], 0); - t.is(pixels[2], 0); -}); - -test('have a `css` file', async t => { - const stream = m('http://yeoman.io', '1024x768', {css: 'fixtures/style.css'}); - const png = new PNG(await getStream.buffer(stream)); - const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 0); - t.is(pixels[1], 128); - t.is(pixels[2], 0); -}); - -test('have a `script` option', async t => { - const stream = m('http://yeoman.io', '1024x768', {script: 'document.querySelector(\'.mobile-bar\').style.backgroundColor = \'red\';'}); - const png = new PNG(await getStream.buffer(stream)); - const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 255); - t.is(pixels[1], 0); - t.is(pixels[2], 0); -}); - -test('have a `js` file', async t => { - const stream = m('http://yeoman.io', '1024x768', {script: 'fixtures/script.js'}); - const png = new PNG(await getStream.buffer(stream)); - const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 0); - t.is(pixels[1], 128); - t.is(pixels[2], 0); + t.true(isJpg(await m('http://yeoman.io', '1024x768', {format: 'jpg'}))); }); test('send cookie', async t => { const s = await server(); - const stream = m(`${s.url}/cookies`, '100x100', { + const png = new PNG(await m(`${s.url}/cookies`, '100x100', { cookies: ['color=black; Path=/; Domain=localhost'] - }); + })); - const png = new PNG(await getStream.buffer(stream)); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 0); + await s.close(); }); test('send cookie using an object', async t => { const s = await server(); - const stream = m(`${s.url}/cookies`, '100x100', { + const png = new PNG(await m(`${s.url}/cookies`, '100x100', { cookies: [{ name: 'color', value: 'black', domain: 'localhost' }] - }); + })); - const png = new PNG(await getStream.buffer(stream)); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 0); - await s.close(); -}); - -test('send headers', async t => { - const s = await server(); - m(`${s.url}`, '100x100', { - headers: { - foobar: 'unicorn' - } - }); - - t.is((await pify(s.once.bind(s), {errorFirst: false})('/')).headers.foobar, 'unicorn'); await s.close(); }); test('handle redirects', async t => { const s = await server(); - const stream = m(`${s.url}/redirect`, '100x100'); - const png = new PNG(await getStream.buffer(stream)); + const png = new PNG(await m(`${s.url}/redirect`, '100x100')); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 0); await s.close(); }); - -test('resource timeout', async t => { - const s = await server({delay: 5}); - const stream = m(s.url, '100x100', {timeout: 1}); - - await Promise.race([ - pify(s.once.bind(s), {errorFirst: false})('/').then(() => t.fail('Expected resource timed out error')), - t.throws(getStream(stream), `Resource timed out #1 (Network timeout on resource.) → ${s.url}/`) - ]); - - await s.close(); -}); From 6720fd75ed440561caac9d27cd62e953994ef2ed Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 20:07:40 +0200 Subject: [PATCH 02/10] Add missing `devDependencies` --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 0504523..5daa49b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ }, "devDependencies": { "ava": "*", + "cookie": "^0.3.1", + "get-port": "^3.2.0", "image-size": "^0.6.1", "is-jpg": "^1.0.0", "is-png": "^1.1.0", From 08c161e19e6458aa8b7ca6ebb07256bc1501b9c0 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 20:21:39 +0200 Subject: [PATCH 03/10] Add `launch` method to `Screenshot` class --- index.js | 23 +++++++++++------------ screenshot.js | 14 +++++++++++--- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 2782e55..30fd05a 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ 'use strict'; const parseResolution = require('parse-resolution'); -const puppeteer = require('puppeteer'); const Screenshot = require('./screenshot'); module.exports = (url, size, opts) => { @@ -34,15 +33,15 @@ module.exports = (url, size, opts) => { opts.omitBackground = true; } - return puppeteer.launch() - .then(browser => browser.newPage() - .then(page => new Screenshot(browser, page))) - .then(Screenshot => Screenshot.authenticate(opts.username, opts.password) - .then(() => Screenshot.setCookie(opts.cookies)) - .then(() => Screenshot.setHeaders(opts.headers)) - .then(() => Screenshot.setUserAgent(opts.userAgent)) - .then(() => Screenshot.open(url, opts)) - .then(() => Screenshot.hideElements(opts.hide)) - .then(() => Screenshot.getRect(opts.selector)) - .then(clip => Screenshot.screenshot(Object.assign(opts, clip)))); + const screenshot = new Screenshot(); + + return screenshot.launch() + .then(() => screenshot.authenticate(opts.username, opts.password) + .then(() => screenshot.setCookie(opts.cookies)) + .then(() => screenshot.setHeaders(opts.headers)) + .then(() => screenshot.setUserAgent(opts.userAgent)) + .then(() => screenshot.open(url, opts)) + .then(() => screenshot.hideElements(opts.hide)) + .then(() => screenshot.getRect(opts.selector)) + .then(clip => screenshot.screenshot(Object.assign(opts, clip)))); }; diff --git a/screenshot.js b/screenshot.js index e23ecd3..8ec9e10 100644 --- a/screenshot.js +++ b/screenshot.js @@ -1,11 +1,19 @@ const toughCookie = require('tough-cookie'); const fileUrl = require('file-url'); const isUrl = require('is-url-superb'); +const puppeteer = require('puppeteer'); module.exports = class Screenshot { - constructor(browser, page) { - this.browser = browser; - this.page = page; + launch() { + return puppeteer.launch() + .then(browser => { + this.browser = browser; + return browser.newPage(); + }) + .then(page => { + this.page = page; + return page; + }); } authenticate(username, password) { From 3cfae93a443db436467e2b833410cc0f60766d39 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 20:27:51 +0200 Subject: [PATCH 04/10] Typo --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 08910dc..9940786 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ const fs = require('fs'); const screenshot = require('screenshot-stream'); screenshot('http://google.com', '1024x768', {crop: true}).then(data => { - fs.writeFileSync('google.com-1024x768.png'); + fs.writeFileSync('google.com-1024x768.png', data); }); ``` From 53c5ee4f3b5268fc1dce06430a7fbc14884e5495 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Mon, 25 Sep 2017 01:32:44 +0700 Subject: [PATCH 05/10] Minor readme tweaks --- readme.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index 9940786..b0b9abc 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,12 @@ # screenshot-stream [![Build Status](https://travis-ci.org/kevva/screenshot-stream.svg?branch=master)](https://travis-ci.org/kevva/screenshot-stream) -> Capture screenshot of a website and return it as a stream +> Capture screenshot of a website ## Install ``` -$ npm install --save screenshot-stream +$ npm install screenshot-stream ``` @@ -46,7 +46,7 @@ Define options to be used. ##### crop -Type: `Boolean`
+Type: `boolean`
Default: `false` Crop to the set height. @@ -79,7 +79,7 @@ Set custom headers. ##### cookies -Type: `Array` or `Object` +Type: `Array` `Object` A string with the same format as a [browser cookie](http://en.wikipedia.org/wiki/HTTP_cookie) or an object of what [`phantomjs.addCookie`](http://phantomjs.org/api/phantom/method/add-cookie.html) accepts. @@ -117,7 +117,7 @@ Set a custom user agent. ##### transparent -Type: `Boolean`
+Type: `boolean`
Default: `false` Set background color to `transparent` instead of `white` if no background is set. @@ -125,7 +125,7 @@ Set background color to `transparent` instead of `white` if no background is set ## CLI -See the [pageres](https://github.com/sindresorhus/pageres#usage) CLI. +See the [Pageres CLI](https://github.com/sindresorhus/pageres-cli). ## License From 0fc17a3305a6eaf8a9845ec6513c1b3bb2539e85 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 23:27:20 +0200 Subject: [PATCH 06/10] Support Node.js >=8 --- .travis.yml | 1 - fixtures/server.js | 4 +- index.js | 107 +++++++++++++++++++++++++++++++---------- package.json | 3 +- screenshot.js | 105 ---------------------------------------- test.js | 116 ++++++++++++++++++++++++++++++++++++++++----- 6 files changed, 189 insertions(+), 147 deletions(-) delete mode 100644 screenshot.js diff --git a/.travis.yml b/.travis.yml index c58cf9b..651cec7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,3 @@ sudo: false language: node_js node_js: - '8' - - '6' diff --git a/fixtures/server.js b/fixtures/server.js index d432652..523e9b6 100644 --- a/fixtures/server.js +++ b/fixtures/server.js @@ -1,8 +1,8 @@ 'use strict'; const http = require('http'); -const cookie = require('cookie'); const getPort = require('get-port'); const pify = require('pify'); +const toughCookie = require('tough-cookie'); module.exports = opts => { opts = opts || {}; @@ -24,7 +24,7 @@ module.exports = opts => { }); s.on('/cookies', (req, res) => { - const color = cookie.parse(req.headers.cookie).color || 'white'; + const color = toughCookie.parse(req.headers.cookie).value || 'white'; const style = [ `background-color: ${color}; position: absolute;`, 'top: 0; right: 0; bottom: 0; left: 0;' diff --git a/index.js b/index.js index 30fd05a..e88dcc4 100644 --- a/index.js +++ b/index.js @@ -1,47 +1,102 @@ 'use strict'; -const parseResolution = require('parse-resolution'); -const Screenshot = require('./screenshot'); +const fileUrl = require('file-url'); +const isUrl = require('is-url-superb'); +const puppeteer = require('puppeteer'); +const toughCookie = require('tough-cookie'); -module.exports = (url, size, opts) => { - const {width, height} = parseResolution(size); +const parseCookie = cookie => { + if (typeof cookie === 'object') { + return cookie; + } + + const ret = toughCookie.parse(cookie).toJSON(); + ret.name = ret.key; + return ret; +}; + +const hideElement = element => { + element.style.visibility = 'hidden'; +}; + +const getBoundingClientRect = element => { + const {height, width, x, y} = element.getBoundingClientRect(); + return {height, width, x, y}; +}; +module.exports = async (url, opts) => { opts = Object.assign({ cookies: [], - format: 'png', fullPage: true, hide: [], - scale: 1, - viewport: {} + width: 1920, + height: 1080 }, opts); - opts.type = opts.format === 'jpg' ? 'jpeg' : opts.format; + const uri = isUrl(url) ? url : fileUrl(url); + const { + cookies, crop, format, headers, height, hide, password, scale, + script, selector, timeout, transparent, userAgent, username, width + } = opts; - opts.viewport = Object.assign({}, opts.viewport, { - width, - height - }); + opts.type = format === 'jpg' ? 'jpeg' : format; - if (opts.crop) { + if (crop) { opts.fullPage = false; } - if (opts.scale !== 1) { - opts.viewport.deviceScaleFactor = opts.scale; + if (timeout) { + opts.timeout = timeout * 1000; } - if (opts.transparent) { + if (transparent) { opts.omitBackground = true; } - const screenshot = new Screenshot(); + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + const viewport = { + height, + width, + deviceScaleFactor: typeof scale === 'number' ? scale : null + }; + + if (username && password) { + await page.authenticate({username, password}); + } + + if (cookies.length > 0) { + await Promise.all(cookies.map(x => page.setCookie(parseCookie(x)))); + } + + if (typeof headers === 'object') { + await page.setExtraHTTPHeaders(headers); + } + + if (userAgent) { + await page.setUserAgent(userAgent); + } + + await page.setViewport(viewport); + await page.goto(uri, opts); + + if (script) { + const fn = isUrl(script) ? page.addScriptTag : script.endsWith('.js') ? page.injectFile : page.evaluate; + await fn(script); + } + + if (Array.isArray(hide) && hide.length > 0) { + await Promise.all(hide.map(x => page.$eval(x, hideElement))); + } + + if (selector) { + await page.waitForSelector(selector, {visible: true}); + + opts.clip = await page.$eval(selector, getBoundingClientRect); + opts.fullPage = false; + } + + const buf = await page.screenshot(opts); + await browser.close(); - return screenshot.launch() - .then(() => screenshot.authenticate(opts.username, opts.password) - .then(() => screenshot.setCookie(opts.cookies)) - .then(() => screenshot.setHeaders(opts.headers)) - .then(() => screenshot.setUserAgent(opts.userAgent)) - .then(() => screenshot.open(url, opts)) - .then(() => screenshot.hideElements(opts.hide)) - .then(() => screenshot.getRect(opts.selector)) - .then(clip => screenshot.screenshot(Object.assign(opts, clip)))); + return buf; }; diff --git a/package.json b/package.json index 5daa49b..43f7b9a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "url": "github.com/kevva" }, "engines": { - "node": ">=6.4" + "node": ">=8" }, "scripts": { "test": "xo && ava" @@ -39,7 +39,6 @@ }, "devDependencies": { "ava": "*", - "cookie": "^0.3.1", "get-port": "^3.2.0", "image-size": "^0.6.1", "is-jpg": "^1.0.0", diff --git a/screenshot.js b/screenshot.js deleted file mode 100644 index 8ec9e10..0000000 --- a/screenshot.js +++ /dev/null @@ -1,105 +0,0 @@ -const toughCookie = require('tough-cookie'); -const fileUrl = require('file-url'); -const isUrl = require('is-url-superb'); -const puppeteer = require('puppeteer'); - -module.exports = class Screenshot { - launch() { - return puppeteer.launch() - .then(browser => { - this.browser = browser; - return browser.newPage(); - }) - .then(page => { - this.page = page; - return page; - }); - } - - authenticate(username, password) { - if (!username || !password) { - return Promise.resolve(); - } - - return this.page.authenticate({username, password}); - } - - getRect(selector) { - if (!selector) { - return Promise.resolve(); - } - - return this.page.waitForSelector(selector, {visible: true}).then(() => this.page.$eval(selector, el => { - const {x, y, width, height} = el.getBoundingClientRect(); - - return { - fullPage: false, - clip: { - width, - height, - x, - y - } - }; - })); - } - - hideElements(selectors) { - if (!Array.isArray(selectors) || selectors.length === 0) { - return Promise.resolve(); - } - - return Promise.all(selectors.map(x => this.page.$eval(x, el => { - el.style.visibility = 'hidden'; - }))); - } - - open(uri, opts) { - if (!uri) { - return Promise.resolve(); - } - - const url = isUrl(uri) ? uri : fileUrl(uri); - - return Promise.all([ - this.page.setViewport(opts.viewport), - this.page.goto(url, opts) - ]); - } - - screenshot(opts) { - return this.page.screenshot(opts) - .then(buf => this.browser.close() - .then(() => buf)); - } - - setCookie(cookies) { - if (!Array.isArray(cookies) || cookies.length === 0) { - return Promise.resolve(); - } - - return Promise.all(cookies.map(x => { - const cookie = typeof x === 'string' ? toughCookie.parse(x).toJSON() : x; - - return this.page.setCookie(Object.assign(cookie, { - name: cookie.name || cookie.key - })); - })); - } - - setHeaders(headers) { - if (typeof headers !== 'object') { - return Promise.resolve(); - } - - return this.page.setExtraHTTPHeaders(headers); - } - - setUserAgent(userAgent) { - if (!userAgent) { - return Promise.resolve(); - } - - return this.page.setUserAgent(userAgent); - } -}; diff --git a/test.js b/test.js index dda9b88..2e876d3 100644 --- a/test.js +++ b/test.js @@ -9,44 +9,71 @@ import server from './fixtures/server'; import m from '.'; test('generate screenshot', async t => { - t.true(isPng(await m('http://yeoman.io', '1024x768'))); + t.true(isPng(await m('http://yeoman.io', { + width: 1024, + height: 768 + }))); }); test('crop image using the `crop` option', async t => { - const size = imageSize(await m('http://yeoman.io', '1024x768', {crop: true})); + const size = imageSize(await m('http://yeoman.io', { + width: 1024, + height: 768, + crop: true + })); + t.is(size.width, 1024); t.is(size.height, 768); }); test('capture a DOM element using the `selector` option', async t => { - const size = imageSize(await m('http://yeoman.io', '1024x768', {selector: '.page-header'})); + const size = imageSize(await m('http://yeoman.io', { + width: 1024, + height: 768, + selector: '.page-header' + })); + t.is(size.width, 1024); t.is(size.height, 80); }); test('wait for DOM element when using the `selector` option', async t => { const fixture = path.join(__dirname, 'fixtures', 'test-delay-element.html'); - const size = imageSize(await m(fixture, '1024x768', {selector: 'div'})); + const size = imageSize(await m(fixture, { + width: 1024, + height: 768, + selector: 'div' + })); + t.is(size.width, 300); t.is(size.height, 200); }); test('hide elements using the `hide` option', async t => { const fixture = path.join(__dirname, 'fixtures', 'test-hide-element.html'); - const png = new PNG(await m(fixture, '100x100', {hide: ['div']})); + const png = new PNG(await m(fixture, { + width: 100, + height: 100, + hide: ['div'] + })); + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 255); }); test('auth using the `username` and `password` options', async t => { - t.true(isPng(await m('http://httpbin.org/basic-auth/user/passwd', '1024x768', { + t.true(isPng(await m('http://httpbin.org/basic-auth/user/passwd', { + width: 1024, + height: 768, username: 'user', password: 'passwd' }))); }); test('have a `scale` option', async t => { - const size = imageSize(await m('http://yeoman.io', '1024x768', { + const size = imageSize(await m('http://yeoman.io', { + width: 1024, + height: 768, crop: true, scale: 2 })); @@ -56,12 +83,46 @@ test('have a `scale` option', async t => { }); test('have a `format` option', async t => { - t.true(isJpg(await m('http://yeoman.io', '1024x768', {format: 'jpg'}))); + t.true(isJpg(await m('http://yeoman.io', { + width: 1024, + height: 768, + format: 'jpg' + }))); +}); + +test.failing('have a `script` option', async t => { + const png = new PNG(await m('http://yeoman.io', { + width: 1024, + height: 768, + script: `document.querySelector('.mobile-bar).style.backgroundColor = red;` + })); + + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); + + t.is(pixels[0], 255); + t.is(pixels[1], 0); + t.is(pixels[2], 0); +}); + +test.failing('have a `js` file', async t => { + const png = new PNG(await m('http://yeoman.io', { + width: 1024, + height: 768, + script: 'fixtures/script.js' + })); + + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); + + t.is(pixels[0], 0); + t.is(pixels[1], 128); + t.is(pixels[2], 0); }); test('send cookie', async t => { const s = await server(); - const png = new PNG(await m(`${s.url}/cookies`, '100x100', { + const png = new PNG(await m(`${s.url}/cookies`, { + width: 100, + height: 100, cookies: ['color=black; Path=/; Domain=localhost'] })); @@ -73,7 +134,9 @@ test('send cookie', async t => { test('send cookie using an object', async t => { const s = await server(); - const png = new PNG(await m(`${s.url}/cookies`, '100x100', { + const png = new PNG(await m(`${s.url}/cookies`, { + width: 100, + height: 100, cookies: [{ name: 'color', value: 'black', @@ -87,10 +150,41 @@ test('send cookie using an object', async t => { await s.close(); }); +test.skip('send headers', async t => { // eslint-disable-line ava/no-skip-test + const s = await server(); + + await m(`${s.url}`, { + width: 100, + height: 100, + headers: { + foobar: 'unicorn' + } + }); + + t.is((await pify(s.once.bind(s), {errorFirst: false})('/')).headers.foobar, 'unicorn'); + await s.close(); +}); + test('handle redirects', async t => { const s = await server(); - const png = new PNG(await m(`${s.url}/redirect`, '100x100')); + const png = new PNG(await m(`${s.url}/redirect`, { + width: 100, + height: 100 + })); + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 0); + + await s.close(); +}); + +test('resource timeout', async t => { + const s = await server({delay: 5}); + await t.throws(m(`${s.url}`, { + width: 100, + height: 100, + timeout: 1 + }), /1000ms exceeded/); + await s.close(); }); From 74864f46b57145b3218b30edcf086f58f073f6c6 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 23:30:57 +0200 Subject: [PATCH 07/10] Make sure `cookies` is an `Array` --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index e88dcc4..09cb140 100644 --- a/index.js +++ b/index.js @@ -64,7 +64,7 @@ module.exports = async (url, opts) => { await page.authenticate({username, password}); } - if (cookies.length > 0) { + if (Array.isArray(cookies) && cookies.length > 0) { await Promise.all(cookies.map(x => page.setCookie(parseCookie(x)))); } From f31a09bbf3e343298d12e54fffe73fa66fe9b174 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Sun, 24 Sep 2017 23:59:03 +0200 Subject: [PATCH 08/10] Add `keepAlive` and `browser` options --- index.js | 12 +++++++++--- test.js | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 09cb140..74132e3 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,7 @@ module.exports = async (url, opts) => { const uri = isUrl(url) ? url : fileUrl(url); const { - cookies, crop, format, headers, height, hide, password, scale, + cookies, crop, format, headers, height, hide, keepAlive, password, scale, script, selector, timeout, transparent, userAgent, username, width } = opts; @@ -52,7 +52,7 @@ module.exports = async (url, opts) => { opts.omitBackground = true; } - const browser = await puppeteer.launch(); + const browser = opts.browser || await puppeteer.launch(); const page = await browser.newPage(); const viewport = { height, @@ -96,7 +96,13 @@ module.exports = async (url, opts) => { } const buf = await page.screenshot(opts); - await browser.close(); + await page.close(); + + if (keepAlive !== true) { + await browser.close(); + } return buf; }; + +module.exports.startBrowser = puppeteer.launch; diff --git a/test.js b/test.js index 2e876d3..d4a3ab7 100644 --- a/test.js +++ b/test.js @@ -6,7 +6,23 @@ import isPng from 'is-png'; import pify from 'pify'; import PNG from 'png-js'; import server from './fixtures/server'; -import m from '.'; +import screenshotStream, {startBrowser} from '.'; + +let browser; +let m; + +test.before(async () => { + browser = await startBrowser(); + + m = (url, opts) => screenshotStream(url, Object.assign({}, opts, { + browser, + keepAlive: true + })); +}); + +test.after(async () => { + await browser.close(); +}); test('generate screenshot', async t => { t.true(isPng(await m('http://yeoman.io', { From 3bfddc52c35eb58d398bb326583463256496fc06 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Wed, 11 Oct 2017 19:48:36 +0200 Subject: [PATCH 09/10] Use local fixtures --- fixtures/script.js | 2 +- fixtures/server.js | 88 ++++++++++++++++------------- fixtures/test-delay-element.html | 22 -------- fixtures/test-error-script.html | 17 ------ fixtures/test-hide-element.html | 20 ------- package.json | 2 +- test.js | 97 ++++++++++++++------------------ 7 files changed, 92 insertions(+), 156 deletions(-) delete mode 100644 fixtures/test-delay-element.html delete mode 100644 fixtures/test-error-script.html delete mode 100644 fixtures/test-hide-element.html diff --git a/fixtures/script.js b/fixtures/script.js index 7755959..e8d718a 100644 --- a/fixtures/script.js +++ b/fixtures/script.js @@ -1 +1 @@ -document.querySelector('.mobile-bar').style.backgroundColor = 'green'; +document.querySelector('.mobile-bar').style.backgroundColor = 'red'; diff --git a/fixtures/server.js b/fixtures/server.js index 523e9b6..9e8ec29 100644 --- a/fixtures/server.js +++ b/fixtures/server.js @@ -1,46 +1,56 @@ 'use strict'; -const http = require('http'); -const getPort = require('get-port'); +const createTestServer = require('create-test-server'); const pify = require('pify'); const toughCookie = require('tough-cookie'); -module.exports = opts => { - opts = opts || {}; - - return getPort().then(port => { - const s = http.createServer((req, res) => { - setTimeout(() => { - s.emit(req.url, req, res); - }, (opts.delay || 0) * 1000); - }); - - s.port = port; - s.url = `http://localhost:${port}`; - s.close = pify(s.close); - - s.on('/', (req, res) => { - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end(''); - }); - - s.on('/cookies', (req, res) => { - const color = toughCookie.parse(req.headers.cookie).value || 'white'; - const style = [ - `background-color: ${color}; position: absolute;`, - 'top: 0; right: 0; bottom: 0; left: 0;' - ].join(' '); - - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end(`
`); - }); - - s.on('/redirect', (req, res) => { - res.writeHead(302, {location: `http://localhost:${port}/`}); - res.end(); - }); +module.exports = () => createTestServer().then(server => { + server.get('/', (req, res) => { + const style = `background-color: black; width: 100px; height: 100px;`; + res.send(`
`); + }); + + server.get('/delay', (req, res) => { + const style = `width: 100px; height: 100px;`; + res.send(` + +
+ + + `); + }); - s.listen(port); + server.get('/cookie', (req, res) => { + const color = toughCookie.parse(req.headers.cookie).value || 'white'; + const style = ` + background-color: ${color}; + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + `; + + res.send(`
`); + }); - return s; + server.get('/headers', (req, res) => { + server.emit('headers', req); + res.end(); }); -}; + + server.get('/redirect', (req, res) => { + res.redirect(server.url); + }); + + server.get('/timeout/:delay', (req, res) => { + setTimeout(() => { + res.end(); + }, req.params.delay * 1000); + }); + + return server; +}); diff --git a/fixtures/test-delay-element.html b/fixtures/test-delay-element.html deleted file mode 100644 index b3bd269..0000000 --- a/fixtures/test-delay-element.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - -
- - - diff --git a/fixtures/test-error-script.html b/fixtures/test-error-script.html deleted file mode 100644 index 2d86bc4..0000000 --- a/fixtures/test-error-script.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - -
- - - diff --git a/fixtures/test-hide-element.html b/fixtures/test-hide-element.html deleted file mode 100644 index 62a74e1..0000000 --- a/fixtures/test-hide-element.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - -
- - diff --git a/package.json b/package.json index 43f7b9a..430001c 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "ava": "*", - "get-port": "^3.2.0", + "create-test-server": "^2.1.1", "image-size": "^0.6.1", "is-jpg": "^1.0.0", "is-png": "^1.1.0", diff --git a/test.js b/test.js index d4a3ab7..6a4e28c 100644 --- a/test.js +++ b/test.js @@ -1,18 +1,19 @@ -import path from 'path'; import test from 'ava'; import imageSize from 'image-size'; import isJpg from 'is-jpg'; import isPng from 'is-png'; import pify from 'pify'; import PNG from 'png-js'; -import server from './fixtures/server'; +import createServer from './fixtures/server'; import screenshotStream, {startBrowser} from '.'; let browser; let m; +let server; test.before(async () => { browser = await startBrowser(); + server = await createServer(); m = (url, opts) => screenshotStream(url, Object.assign({}, opts, { browser, @@ -22,17 +23,18 @@ test.before(async () => { test.after(async () => { await browser.close(); + await server.close(); }); test('generate screenshot', async t => { - t.true(isPng(await m('http://yeoman.io', { - width: 1024, - height: 768 + t.true(isPng(await m(server.url, { + width: 100, + height: 100 }))); }); test('crop image using the `crop` option', async t => { - const size = imageSize(await m('http://yeoman.io', { + const size = imageSize(await m(server.url, { width: 1024, height: 768, crop: true @@ -43,31 +45,29 @@ test('crop image using the `crop` option', async t => { }); test('capture a DOM element using the `selector` option', async t => { - const size = imageSize(await m('http://yeoman.io', { + const size = imageSize(await m(server.url, { width: 1024, height: 768, - selector: '.page-header' + selector: 'div' })); - t.is(size.width, 1024); - t.is(size.height, 80); + t.is(size.width, 100); + t.is(size.height, 100); }); test('wait for DOM element when using the `selector` option', async t => { - const fixture = path.join(__dirname, 'fixtures', 'test-delay-element.html'); - const size = imageSize(await m(fixture, { + const size = imageSize(await m(`${server.url}/delay`, { width: 1024, height: 768, selector: 'div' })); - t.is(size.width, 300); - t.is(size.height, 200); + t.is(size.width, 100); + t.is(size.height, 100); }); test('hide elements using the `hide` option', async t => { - const fixture = path.join(__dirname, 'fixtures', 'test-hide-element.html'); - const png = new PNG(await m(fixture, { + const png = new PNG(await m(server.url, { width: 100, height: 100, hide: ['div'] @@ -79,38 +79,38 @@ test('hide elements using the `hide` option', async t => { test('auth using the `username` and `password` options', async t => { t.true(isPng(await m('http://httpbin.org/basic-auth/user/passwd', { - width: 1024, - height: 768, + width: 100, + height: 100, username: 'user', password: 'passwd' }))); }); test('have a `scale` option', async t => { - const size = imageSize(await m('http://yeoman.io', { - width: 1024, - height: 768, + const size = imageSize(await m(server.url, { + width: 100, + height: 100, crop: true, scale: 2 })); - t.is(size.width, 1024 * 2); - t.is(size.height, 768 * 2); + t.is(size.width, 100 * 2); + t.is(size.height, 100 * 2); }); test('have a `format` option', async t => { - t.true(isJpg(await m('http://yeoman.io', { - width: 1024, - height: 768, + t.true(isJpg(await m(server.url, { + width: 100, + height: 100, format: 'jpg' }))); }); test.failing('have a `script` option', async t => { - const png = new PNG(await m('http://yeoman.io', { - width: 1024, - height: 768, - script: `document.querySelector('.mobile-bar).style.backgroundColor = red;` + const png = new PNG(await m(server.url, { + width: 100, + height: 100, + script: `document.querySelector('div').style.backgroundColor = red;` })); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); @@ -121,22 +121,21 @@ test.failing('have a `script` option', async t => { }); test.failing('have a `js` file', async t => { - const png = new PNG(await m('http://yeoman.io', { - width: 1024, - height: 768, + const png = new PNG(await m(server.url, { + width: 100, + height: 100, script: 'fixtures/script.js' })); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); - t.is(pixels[0], 0); - t.is(pixels[1], 128); + t.is(pixels[0], 255); + t.is(pixels[1], 0); t.is(pixels[2], 0); }); test('send cookie', async t => { - const s = await server(); - const png = new PNG(await m(`${s.url}/cookies`, { + const png = new PNG(await m(`${server.url}/cookie`, { width: 100, height: 100, cookies: ['color=black; Path=/; Domain=localhost'] @@ -144,13 +143,10 @@ test('send cookie', async t => { const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 0); - - await s.close(); }); test('send cookie using an object', async t => { - const s = await server(); - const png = new PNG(await m(`${s.url}/cookies`, { + const png = new PNG(await m(`${server.url}/cookie`, { width: 100, height: 100, cookies: [{ @@ -162,14 +158,10 @@ test('send cookie using an object', async t => { const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 0); - - await s.close(); }); test.skip('send headers', async t => { // eslint-disable-line ava/no-skip-test - const s = await server(); - - await m(`${s.url}`, { + await m(`${server.url}`, { width: 100, height: 100, headers: { @@ -177,30 +169,23 @@ test.skip('send headers', async t => { // eslint-disable-line ava/no-skip-test } }); - t.is((await pify(s.once.bind(s), {errorFirst: false})('/')).headers.foobar, 'unicorn'); - await s.close(); + t.is((await pify(server.once.bind(server), {errorFirst: false})('/headers')).headers.foobar, 'unicorn'); }); test('handle redirects', async t => { - const s = await server(); - const png = new PNG(await m(`${s.url}/redirect`, { + const png = new PNG(await m(`${server.url}/redirect`, { width: 100, height: 100 })); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); t.is(pixels[0], 0); - - await s.close(); }); test('resource timeout', async t => { - const s = await server({delay: 5}); - await t.throws(m(`${s.url}`, { + await t.throws(m(`${server.url}/timeout/5`, { width: 100, height: 100, timeout: 1 }), /1000ms exceeded/); - - await s.close(); }); From 14a03545e104cb3c16ddb3a3ee41ff75dc5e4318 Mon Sep 17 00:00:00 2001 From: Kevin Martensson Date: Wed, 18 Oct 2017 18:43:03 +0200 Subject: [PATCH 10/10] Always use `addScriptTag` and `addStyleTag` --- fixtures/script.js | 2 +- fixtures/style.css | 4 +-- index.js | 15 ++++++++-- package.json | 3 +- test.js | 72 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 86 insertions(+), 10 deletions(-) diff --git a/fixtures/script.js b/fixtures/script.js index e8d718a..33d225a 100644 --- a/fixtures/script.js +++ b/fixtures/script.js @@ -1 +1 @@ -document.querySelector('.mobile-bar').style.backgroundColor = 'red'; +document.querySelector('div').style.backgroundColor = 'red'; diff --git a/fixtures/style.css b/fixtures/style.css index 458c5de..cc893dd 100644 --- a/fixtures/style.css +++ b/fixtures/style.css @@ -1,3 +1,3 @@ -.mobile-bar { - background-color: green !important; +div { + background-color: red !important; } diff --git a/index.js b/index.js index 74132e3..c1ec413 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,7 @@ module.exports = async (url, opts) => { const uri = isUrl(url) ? url : fileUrl(url); const { cookies, crop, format, headers, height, hide, keepAlive, password, scale, - script, selector, timeout, transparent, userAgent, username, width + script, selector, style, timeout, transparent, userAgent, username, width } = opts; opts.type = format === 'jpg' ? 'jpeg' : format; @@ -80,8 +80,17 @@ module.exports = async (url, opts) => { await page.goto(uri, opts); if (script) { - const fn = isUrl(script) ? page.addScriptTag : script.endsWith('.js') ? page.injectFile : page.evaluate; - await fn(script); + const key = isUrl(script) ? 'url' : script.endsWith('.js') ? 'path' : 'content'; + await page.addScriptTag({[key]: script}); + } + + if (style) { + const key = isUrl(style) ? 'url' : style.endsWith('.css') ? 'path' : 'content'; + await page.addStyleTag({[key]: style}); + + if (isUrl(style)) { + console.log(key, style); + } } if (Array.isArray(hide) && hide.length > 0) { diff --git a/package.json b/package.json index 430001c..bca249c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "file-url": "^2.0.2", "is-url-superb": "^2.0.0", "parse-resolution": "^1.0.0", - "puppeteer": "^0.11.0", + "puppeteer": "^0.12.0", "tough-cookie": "^2.3.3" }, "devDependencies": { @@ -43,6 +43,7 @@ "image-size": "^0.6.1", "is-jpg": "^1.0.0", "is-png": "^1.1.0", + "nock": "^9.0.22", "pify": "^3.0.0", "png-js": "^0.1.1", "xo": "*" diff --git a/test.js b/test.js index 6a4e28c..562387c 100644 --- a/test.js +++ b/test.js @@ -1,7 +1,9 @@ +import path from 'path'; import test from 'ava'; import imageSize from 'image-size'; import isJpg from 'is-jpg'; import isPng from 'is-png'; +import nock from 'nock'; import pify from 'pify'; import PNG from 'png-js'; import createServer from './fixtures/server'; @@ -19,6 +21,12 @@ test.before(async () => { browser, keepAlive: true })); + + nock('http://foo.bar') + .get('/script.js') + .replyWithFile(200, path.join(__dirname, 'fixtures', 'script.js')) + .get('/style.css') + .replyWithFile(200, path.join(__dirname, 'fixtures', 'style.css')); }); test.after(async () => { @@ -106,11 +114,11 @@ test('have a `format` option', async t => { }))); }); -test.failing('have a `script` option', async t => { +test('`script` option inline', async t => { const png = new PNG(await m(server.url, { width: 100, height: 100, - script: `document.querySelector('div').style.backgroundColor = red;` + script: `document.querySelector('div').style.backgroundColor = 'red';` })); const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); @@ -120,7 +128,7 @@ test.failing('have a `script` option', async t => { t.is(pixels[2], 0); }); -test.failing('have a `js` file', async t => { +test('`script` option file', async t => { const png = new PNG(await m(server.url, { width: 100, height: 100, @@ -134,6 +142,64 @@ test.failing('have a `js` file', async t => { t.is(pixels[2], 0); }); +test.skip('`script` option url', async t => { // eslint-disable-line ava/no-skip-test + const png = new PNG(await m(server.url, { + width: 100, + height: 100, + script: 'http://foo.bar/script.js' + })); + + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); + + t.is(pixels[0], 255); + t.is(pixels[1], 0); + t.is(pixels[2], 0); +}); + +test('`style` option inline', async t => { + const png = new PNG(await m(server.url, { + width: 100, + height: 100, + style: 'div { background-color: red !important; }' + })); + + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); + + t.is(pixels[0], 255); + t.is(pixels[1], 0); + t.is(pixels[2], 0); +}); + +test('`style` option file', async t => { + const png = new PNG(await m(server.url, { + width: 100, + height: 100, + style: 'fixtures/style.css' + })); + + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); + + t.is(pixels[0], 255); + t.is(pixels[1], 0); + t.is(pixels[2], 0); +}); + +test.skip('`style` option url', async t => { // eslint-disable-line ava/no-skip-test + const png = new PNG(await m(server.url, { + width: 100, + height: 100, + style: 'http://foo.bar/style.css' + })); + + console.log(png); + + const pixels = await pify(png.decode.bind(png), {errorFirst: false})(); + + t.is(pixels[0], 255); + t.is(pixels[1], 0); + t.is(pixels[2], 0); +}); + test('send cookie', async t => { const png = new PNG(await m(`${server.url}/cookie`, { width: 100,