diff --git a/.gitignore b/.gitignore index a982a42..12151a3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ node_modules/ npm-debug.log debug.log tests/reftests.js +*.log diff --git a/karma.conf.js b/karma.conf.js index 97a1c6c..811827b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -10,10 +10,11 @@ module.exports = function(config) { platform: 'Windows 10', version: 'beta' }, - sl_stable_firefox: { + sl_beta_firefox: { base: 'SauceLabs', browserName: 'firefox', - platform: 'Windows 10' + platform: 'Windows 8.1', + version: 'beta' }, sl_ie9: { base: 'SauceLabs', @@ -90,16 +91,6 @@ module.exports = function(config) { } }); - /* - 'sl_opera_12.15': { - base: 'SauceLabs', - browserName: 'opera', - platform: 'Linux', - version: '12.15' - }, - */ - - config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) @@ -173,6 +164,7 @@ module.exports = function(config) { '/node_modules': `http://localhost:${port}/base/node_modules`, '/tests': `http://localhost:${port}/base/tests`, '/assets': `http://localhost:${port}/base/tests/assets`, + '/screenshot': `http://localhost:8081/screenshot`, }, client: { diff --git a/package.json b/package.json index 6ec10be..65b4866 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,14 @@ "babel-preset-es2015": "6.24.1", "babel-preset-flow": "6.23.0", "base64-arraybuffer": "0.1.5", + "body-parser": "1.17.2", "chai": "4.1.1", "chromeless": "^1.2.0", "eslint": "4.2.0", "eslint-plugin-flowtype": "2.35.0", "eslint-plugin-prettier": "2.1.2", "express": "4.15.4", + "filenamify-url": "1.0.0", "flow-bin": "0.50.0", "glob": "7.1.2", "jquery": "3.2.1", @@ -45,6 +47,7 @@ "karma-mocha": "1.3.0", "karma-sauce-launcher": "1.1.0", "mocha": "3.5.0", + "platform": "1.3.4", "prettier": "1.5.3", "promise-polyfill": "6.0.2", "rimraf": "2.6.1", @@ -59,7 +62,7 @@ "flow": "flow", "lint": "eslint src/**", "test": "npm run flow && npm run lint && npm run karma", - "karma": "karma start --single-run", + "karma": "karma start", "watch": "webpack --progress --colors --watch" }, "homepage": "https://html2canvas.hertzen.com", diff --git a/scripts/screenshot-server.js b/scripts/screenshot-server.js new file mode 100644 index 0000000..797c9ad --- /dev/null +++ b/scripts/screenshot-server.js @@ -0,0 +1,69 @@ +const path = require('path'); +const fs = require('fs'); +const express = require('express'); +const bodyParser = require('body-parser'); +const filenamifyUrl = require('filenamify-url'); + +const app = express(); +app.use( + bodyParser.json({ + limit: '15mb', + type: '*/*' + }) +); + +const prefix = 'data:image/png;base64,'; + +const writeScreenshot = (buffer, body) => { + const filename = `${filenamifyUrl( + body.test.replace(/^\/tests\/reftests\//, '').replace(/\.html$/, ''), + {replacement: '-'} + )}!${body.platform.name}-${body.platform.version}.png`; + + fs.writeFileSync(path.resolve(__dirname, '../tests/results/', filename), buffer); +}; + +app.post('/screenshot', (req, res) => { + if (!req.body || !req.body.screenshot) { + return res.sendStatus(400); + } + + const buffer = new Buffer(req.body.screenshot.substring(prefix.length), 'base64'); + writeScreenshot(buffer, req.body); + return res.sendStatus(200); +}); + +const chunks = {}; + +app.post('/screenshot/chunk', (req, res) => { + if (!req.body || !req.body.screenshot) { + return res.sendStatus(400); + } + + const key = `${req.body.platform.name}-${req.body.platform.version}-${req.body.test + .replace(/^\/tests\/reftests\//, '') + .replace(/\.html$/, '')}`; + if (!Array.isArray(chunks[key])) { + chunks[key] = Array.from(Array(req.body.totalCount)); + } + + chunks[key][req.body.part] = req.body.screenshot; + + if (chunks[key].every(s => typeof s === 'string')) { + const str = chunks[key].reduce((acc, s) => acc + s, ''); + const buffer = new Buffer(str.substring(prefix.length), 'base64'); + delete chunks[key]; + writeScreenshot(buffer, req.body); + } + + return res.sendStatus(200); +}); + +app.use((error, req, res, next) => { + console.error(error); + next(); +}); + +const listener = app.listen(8081, () => { + console.log(listener.address().port); +}); diff --git a/src/Feature.js b/src/Feature.js index 91786f3..2e158da 100644 --- a/src/Feature.js +++ b/src/Feature.js @@ -75,8 +75,15 @@ const testSVG = document => { const testForeignObject = document => { const img = new Image(); const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; const ctx = canvas.getContext('2d'); - img.src = `data:image/svg+xml,
`; + ctx.fillStyle = 'red'; + ctx.fillRect(0, 0, 1, 1); + + img.src = `data:image/svg+xml,`; return new Promise(resolve => { const onload = () => { diff --git a/src/index.js b/src/index.js index 1faf848..126e9a1 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ import Color, {TRANSPARENT} from './Color'; export type Options = { async: ?boolean, allowTaint: ?boolean, + backgroundColor: string, canvas: ?HTMLCanvasElement, imageTimeout: number, proxy: ?string, @@ -71,14 +72,18 @@ const html2canvas = (element: HTMLElement, config: Options): Promise<*> => { const documentBackgroundColor = ownerDocument.documentElement ? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor) : TRANSPARENT; + const bodyBackgroundColor = ownerDocument.body + ? new Color(getComputedStyle(ownerDocument.body).backgroundColor) + : TRANSPARENT; + const backgroundColor = element === ownerDocument.documentElement ? documentBackgroundColor.isTransparent() - ? ownerDocument.body - ? new Color(getComputedStyle(ownerDocument.body).backgroundColor) - : null + ? bodyBackgroundColor.isTransparent() + ? options.backgroundColor ? new Color(options.backgroundColor) : null + : bodyBackgroundColor : documentBackgroundColor - : null; + : options.backgroundColor ? new Color(options.backgroundColor) : null; // $FlowFixMe const result = Feature.SUPPORT_FOREIGNOBJECT_DRAWING.then( diff --git a/tests/reftests/ignore.txt b/tests/reftests/ignore.txt index f037e33..e69de29 100644 --- a/tests/reftests/ignore.txt +++ b/tests/reftests/ignore.txt @@ -1,5 +0,0 @@ -/tests/reftests/background/radial-gradient.html -/tests/reftests/text/chinese.html -[Edge]/tests/reftests/acid2.html -[Edge]/tests/reftests/pseudoelements.html -[Edge]/tests/reftests/text/multiple.html diff --git a/tests/sauceconnect.js b/tests/sauceconnect.js index 04b54a4..05e99a2 100644 --- a/tests/sauceconnect.js +++ b/tests/sauceconnect.js @@ -4,7 +4,12 @@ sauceConnectLauncher( { username: process.env.SAUCE_USERNAME, accessKey: process.env.SAUCE_ACCESS_KEY, - logger: console.log + logger: console.log, + // Log output from the `sc` process to stdout? + verbose: true, + + // Enable verbose debugging (optional) + verboseDebugging: true }, err => { if (err) { diff --git a/tests/testrunner.js b/tests/testrunner.js index 2fa3998..0d3a13e 100644 --- a/tests/testrunner.js +++ b/tests/testrunner.js @@ -2,6 +2,8 @@ import {expect} from 'chai'; import parseRefTest from '../scripts/parse-reftest'; import reftests from './reftests'; import querystring from 'querystring'; +import platform from 'platform'; +import Promise from 'promise-polyfill'; const DOWNLOAD_REFTESTS = false; const query = querystring.parse(location.search.replace(/^\?/, '')); @@ -78,7 +80,7 @@ const assertPath = (result, expected, desc) => { }) .forEach(url => { describe(url, function() { - this.timeout(30000); + this.timeout(60000); const windowWidth = 800; const windowHeight = 600; const testContainer = document.createElement('iframe'); @@ -113,7 +115,8 @@ const assertPath = (result, expected, desc) => { it('Should render untainted canvas', () => { return testContainer.contentWindow .html2canvas(testContainer.contentWindow.document.documentElement, { - removeContainer: true + removeContainer: true, + backgroundColor: '#ffffff' }) .then(canvas => { try { @@ -330,6 +333,80 @@ const assertPath = (result, expected, desc) => { result ); } + + // window.__karma__ + if (false) { + const MAX_CHUNK_SIZE = 75000; + + const sendScreenshot = (tries, body, url) => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.onload = () => { + if ( + typeof xhr.status !== 'number' || + xhr.status === 200 + ) { + resolve(); + } else { + reject( + `Failed to send screenshot with status ${xhr.status}` + ); + } + }; + // xhr.onerror = reject; + + xhr.open('POST', url, true); + xhr.send(body); + }).catch(e => { + if (tries > 0) { + // Older edge browsers and some safari browsers have issues sending large xhr through saucetunnel + const data = canvas.toDataURL(); + const totalCount = Math.ceil( + data.length / MAX_CHUNK_SIZE + ); + return Promise.all( + Array.apply( + null, + Array(totalCount) + ).map((x, part) => + sendScreenshot( + 0, + JSON.stringify({ + screenshot: data.substr( + part * MAX_CHUNK_SIZE, + MAX_CHUNK_SIZE + ), + part, + totalCount, + test: url, + platform: { + name: platform.name, + version: platform.version + } + }), + '/screenshot/chunk' + ) + ) + ); + } + + return Promise.reject(e); + }); + }; + return sendScreenshot( + 1, + JSON.stringify({ + screenshot: canvas.toDataURL(), + test: url, + platform: { + name: platform.name, + version: platform.version + } + }), + '/screenshot' + ); + } }); }); });