diff --git a/karma.conf.js b/karma.conf.js index 811827b..0a650bc 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -163,8 +163,7 @@ module.exports = function(config) { '/dist': `http://localhost:${port}/base/dist`, '/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`, + '/assets': `http://localhost:${port}/base/tests/assets` }, client: { diff --git a/scripts/screenshot-server.js b/karma.js similarity index 72% rename from scripts/screenshot-server.js rename to karma.js index 797c9ad..4da50d3 100644 --- a/scripts/screenshot-server.js +++ b/karma.js @@ -1,10 +1,27 @@ +const Server = require('karma').Server; +const cfg = require('karma').config; const path = require('path'); +const karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js')); +const server = new Server(karmaConfig, (exitCode) => { + console.log('Karma has exited with ' + exitCode); + process.exit(exitCode) +}); + const fs = require('fs'); const express = require('express'); const bodyParser = require('body-parser'); +const cors = require('cors'); const filenamifyUrl = require('filenamify-url'); const app = express(); +app.use(cors()); +app.use(function(req, res, next) { + // IE9 doesn't set headers for cross-domain ajax requests + if(typeof(req.headers['content-type']) === 'undefined'){ + req.headers['content-type'] = "application/json"; + } + next(); +}); app.use( bodyParser.json({ limit: '15mb', @@ -20,7 +37,7 @@ const writeScreenshot = (buffer, body) => { {replacement: '-'} )}!${body.platform.name}-${body.platform.version}.png`; - fs.writeFileSync(path.resolve(__dirname, '../tests/results/', filename), buffer); + fs.writeFileSync(path.resolve(__dirname, './tests/results/', filename), buffer); }; app.post('/screenshot', (req, res) => { @@ -65,5 +82,7 @@ app.use((error, req, res, next) => { }); const listener = app.listen(8081, () => { - console.log(listener.address().port); + server.start(); }); + + diff --git a/package.json b/package.json index 65b4866..41d62e9 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "body-parser": "1.17.2", "chai": "4.1.1", "chromeless": "^1.2.0", + "cors": "2.8.4", "eslint": "4.2.0", "eslint-plugin-flowtype": "2.35.0", "eslint-plugin-prettier": "2.1.2", @@ -62,7 +63,7 @@ "flow": "flow", "lint": "eslint src/**", "test": "npm run flow && npm run lint && npm run karma", - "karma": "karma start", + "karma": "node karma", "watch": "webpack --progress --colors --watch" }, "homepage": "https://html2canvas.hertzen.com", diff --git a/src/Clone.js b/src/Clone.js index c6feb31..77a2644 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -37,7 +37,7 @@ export class DocumentCloner { if (backgroundImage.method === 'url') { return this.imageLoader .inlineImage(backgroundImage.args[0]) - .then(src => (src ? `url("${src}")` : 'none')); + .then(img => (img ? `url("${img.src}")` : 'none')); } return Promise.resolve( `${backgroundImage.prefix}${backgroundImage.method}(${backgroundImage.args.join( @@ -54,9 +54,12 @@ export class DocumentCloner { }); if (node instanceof HTMLImageElement) { - this.imageLoader.inlineImage(node.src).then(src => { - if (src && node instanceof HTMLImageElement) { - node.src = src; + this.imageLoader.inlineImage(node.src).then(img => { + if (img && node instanceof HTMLImageElement && node.parentNode) { + node.parentNode.replaceChild( + copyCSSStyles(node.style, img.cloneNode(false)), + node + ); } }); } diff --git a/src/ImageLoader.js b/src/ImageLoader.js index d33a29f..1bc69df 100644 --- a/src/ImageLoader.js +++ b/src/ImageLoader.js @@ -50,9 +50,9 @@ export default class ImageLoader { } } - inlineImage(src: string): Promise { + inlineImage(src: string): Promise { if (isInlineImage(src)) { - return Promise.resolve(src); + return loadImage(src, this.options.imageTimeout || 0); } if (this.hasImageInCache(src)) { return this.cache[src]; @@ -61,7 +61,7 @@ export default class ImageLoader { return this.xhrImage(src); } - xhrImage(src: string): Promise { + xhrImage(src: string): Promise { this.cache[src] = new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { @@ -87,7 +87,7 @@ export default class ImageLoader { } xhr.open('GET', src, true); xhr.send(); - }); + }).then(src => loadImage(src, this.options.imageTimeout || 0)); return this.cache[src]; } @@ -196,3 +196,24 @@ const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src); const isSVG = (src: string): boolean => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src); + +const loadImage = (src: string, timeout: number) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + if (img.complete === true) { + // Inline XML images may fail to parse, throwing an Error later on + setTimeout(() => { + resolve(img); + }, 500); + } + if (timeout) { + setTimeout( + () => reject(__DEV__ ? `Timed out (${timeout}ms) loading image` : ''), + timeout + ); + } + }); +}; diff --git a/src/Util.js b/src/Util.js index 217a2a1..fd414de 100644 --- a/src/Util.js +++ b/src/Util.js @@ -3,7 +3,7 @@ export const contains = (bit: number, value: number): boolean => (bit & value) !== 0; -export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): void => { +export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => { // Edge does not provide value for cssText for (let i = style.length - 1; i >= 0; i--) { const property = style.item(i); @@ -12,6 +12,7 @@ export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): target.style.setProperty(property, style.getPropertyValue(property)); } } + return target; }; export const SMALL_IMAGE = diff --git a/src/index.js b/src/index.js index f9b6b8b..b2e2476 100644 --- a/src/index.js +++ b/src/index.js @@ -32,11 +32,12 @@ export type Options = { offsetY: number }; -const html2canvas = (element: HTMLElement, config: Options = {}): Promise<*> => { +const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => { if (typeof console === 'object' && typeof console.log === 'function') { console.log(`html2canvas ${__VERSION__}`); } + const config = conf || {}; const logger = new Logger(); const ownerDocument = element.ownerDocument; diff --git a/tests/testrunner.js b/tests/testrunner.js index 0d3a13e..67ff291 100644 --- a/tests/testrunner.js +++ b/tests/testrunner.js @@ -81,6 +81,7 @@ const assertPath = (result, expected, desc) => { .forEach(url => { describe(url, function() { this.timeout(60000); + this.retries(2); const windowWidth = 800; const windowHeight = 600; const testContainer = document.createElement('iframe'); @@ -334,13 +335,15 @@ const assertPath = (result, expected, desc) => { ); } - // window.__karma__ - if (false) { + if (window.__karma__) { const MAX_CHUNK_SIZE = 75000; - const sendScreenshot = (tries, body, url) => { + const sendScreenshot = (tries, body, server) => { return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); + const xhr = + 'withCredentials' in new XMLHttpRequest() + ? new XMLHttpRequest() + : new XDomainRequest(); xhr.onload = () => { if ( @@ -354,9 +357,9 @@ const assertPath = (result, expected, desc) => { ); } }; - // xhr.onerror = reject; + xhr.onerror = reject; - xhr.open('POST', url, true); + xhr.open('POST', server, true); xhr.send(body); }).catch(e => { if (tries > 0) { @@ -385,7 +388,7 @@ const assertPath = (result, expected, desc) => { version: platform.version } }), - '/screenshot/chunk' + 'http://localhost:8081/screenshot/chunk' ) ) ); @@ -404,7 +407,7 @@ const assertPath = (result, expected, desc) => { version: platform.version } }), - '/screenshot' + 'http://localhost:8081/screenshot' ); } });