diff --git a/examples/existing_canvas.html b/examples/existing_canvas.html index ac7be53..baa743e 100644 --- a/examples/existing_canvas.html +++ b/examples/existing_canvas.html @@ -42,7 +42,7 @@ ctx.stroke(); document.querySelector("button").addEventListener("click", function() { - html2canvas(document.querySelector("#content"), {canvas: canvas}).then(function(canvas) { + html2canvas(document.querySelector("#content"), {canvas: canvas, scale: 1}).then(function(canvas) { console.log('Drew on the existing canvas'); }); }, false); diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts new file mode 100644 index 0000000..1cc7708 --- /dev/null +++ b/src/__tests__/index.ts @@ -0,0 +1,91 @@ +import html2canvas from '../index'; + +import {CanvasRenderer} from '../render/canvas/canvas-renderer'; +import {DocumentCloner} from '../dom/document-cloner'; +import {COLORS} from '../css/types/color'; + +jest.mock('../core/logger'); +jest.mock('../css/layout/bounds'); +jest.mock('../dom/document-cloner'); +jest.mock('../dom/node-parser', () => { + return { + isBodyElement: () => false, + isHTMLElement: () => false, + parseTree: jest.fn().mockImplementation(() => { + return {styles: {}}; + }) + }; +}); + +jest.mock('../render/stacking-context'); +jest.mock('../render/canvas/canvas-renderer'); + +describe('html2canvas', () => { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + const element = { + ownerDocument: { + defaultView: { + pageXOffset: 12, + pageYOffset: 34 + } + } + } as HTMLElement; + + it('should render with an element', async () => { + DocumentCloner.destroy = jest.fn().mockReturnValue(true); + await html2canvas(element); + expect(CanvasRenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ + backgroundColor: 0xffffffff, + scale: 1, + height: 50, + width: 200, + x: 0, + y: 0, + scrollX: 12, + scrollY: 34, + canvas: undefined + }) + ); + expect(DocumentCloner.destroy).toBeCalled(); + }); + + it('should have transparent background with backgroundColor: null', async () => { + await html2canvas(element, {backgroundColor: null}); + expect(CanvasRenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ + backgroundColor: COLORS.TRANSPARENT + }) + ); + }); + + it('should use existing canvas when given as option', async () => { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + const canvas = {} as HTMLCanvasElement; + await html2canvas(element, {canvas}); + expect(CanvasRenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ + canvas + }) + ); + }); + + it('should not remove cloned window when removeContainer: false', async () => { + DocumentCloner.destroy = jest.fn(); + await html2canvas(element, {removeContainer: false}); + expect(CanvasRenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ + backgroundColor: 0xffffffff, + scale: 1, + height: 50, + width: 200, + x: 0, + y: 0, + scrollX: 12, + scrollY: 34, + canvas: undefined + }) + ); + expect(DocumentCloner.destroy).not.toBeCalled(); + }); +}); diff --git a/src/core/__mocks__/logger.ts b/src/core/__mocks__/logger.ts new file mode 100644 index 0000000..8819bcd --- /dev/null +++ b/src/core/__mocks__/logger.ts @@ -0,0 +1,17 @@ +export class Logger { + debug() {} + + static create() {} + + static destroy() {} + + static getInstance(): Logger { + return logger; + } + + info() {} + + error() {} +} + +const logger = new Logger(); diff --git a/src/css/layout/__mocks__/bounds.ts b/src/css/layout/__mocks__/bounds.ts new file mode 100644 index 0000000..34daef7 --- /dev/null +++ b/src/css/layout/__mocks__/bounds.ts @@ -0,0 +1,4 @@ +export const {Bounds} = jest.requireActual('../bounds'); +export const parseBounds = () => { + return new Bounds(0, 0, 200, 50); +}; diff --git a/src/dom/__mocks__/document-cloner.ts b/src/dom/__mocks__/document-cloner.ts new file mode 100644 index 0000000..366ceec --- /dev/null +++ b/src/dom/__mocks__/document-cloner.ts @@ -0,0 +1,16 @@ +export class DocumentCloner { + clonedReferenceElement?: HTMLElement; + + constructor() { + // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion + this.clonedReferenceElement = {} as HTMLElement; + } + + toIFrame() { + return Promise.resolve({}); + } + + static destroy() { + return true; + } +} diff --git a/src/dom/document-cloner.ts b/src/dom/document-cloner.ts index 360084a..54e7ba4 100644 --- a/src/dom/document-cloner.ts +++ b/src/dom/document-cloner.ts @@ -410,6 +410,14 @@ export class DocumentCloner { : ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`; return anonymousReplacedElement; } + + static destroy(container: HTMLIFrameElement): boolean { + if (container.parentNode) { + container.parentNode.removeChild(container); + return true; + } + return false; + } } enum PseudoElementType { diff --git a/src/index.ts b/src/index.ts index 7e47073..ee5e9a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,6 +116,7 @@ const renderElement = async (element: HTMLElement, opts: Partial): Prom const renderOptions = { id: instanceName, cache: options.cache, + canvas: options.canvas, backgroundColor, scale: options.scale, x: options.x, @@ -153,7 +154,7 @@ const renderElement = async (element: HTMLElement, opts: Partial): Prom } if (options.removeContainer === true) { - if (!cleanContainer(container)) { + if (!DocumentCloner.destroy(container)) { Logger.getInstance(instanceName).error(`Cannot detach cloned iframe as it is not in the DOM anymore`); } } @@ -163,11 +164,3 @@ const renderElement = async (element: HTMLElement, opts: Partial): Prom CacheStorage.destroy(instanceName); return canvas; }; - -const cleanContainer = (container: HTMLIFrameElement): boolean => { - if (container.parentNode) { - container.parentNode.removeChild(container); - return true; - } - return false; -}; diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 87303c1..93ca923 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -71,10 +71,12 @@ export class CanvasRenderer { this.canvas = options.canvas ? options.canvas : document.createElement('canvas'); this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D; this.options = options; - this.canvas.width = Math.floor(options.width * options.scale); - this.canvas.height = Math.floor(options.height * options.scale); - this.canvas.style.width = `${options.width}px`; - this.canvas.style.height = `${options.height}px`; + if (!options.canvas) { + this.canvas.width = Math.floor(options.width * options.scale); + this.canvas.height = Math.floor(options.height * options.scale); + this.canvas.style.width = `${options.width}px`; + this.canvas.style.height = `${options.height}px`; + } this.fontMetrics = new FontMetrics(document); this.ctx.scale(this.options.scale, this.options.scale); this.ctx.translate(-options.x + options.scrollX, -options.y + options.scrollY); diff --git a/www/src/preview.ts b/www/src/preview.ts index 6d21d3b..d780a19 100644 --- a/www/src/preview.ts +++ b/www/src/preview.ts @@ -42,8 +42,10 @@ function onBrowserChange(browserTest: Test) { previewImage.src = `/results/${browserTest.screenshot}.png`; if (browserTest.devicePixelRatio > 1) { previewImage.style.transform = `scale(${1 / browserTest.devicePixelRatio})`; + previewImage.style.transformOrigin = 'top left'; } else { previewImage.style.transform = ''; + previewImage.style.transformOrigin = ''; } } }