Skip to content

Commit

Permalink
fix: element cropping & scrolling (#2625)
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasvh authored Aug 4, 2021
1 parent 1338c7b commit 878e37a
Show file tree
Hide file tree
Showing 90 changed files with 751 additions and 553 deletions.
12 changes: 8 additions & 4 deletions src/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,18 @@ describe('html2canvas', () => {
DocumentCloner.destroy = jest.fn().mockReturnValue(true);
await html2canvas(element);
expect(CanvasRenderer).toHaveBeenLastCalledWith(
expect.objectContaining({
cache: expect.any(Object),
logger: expect.any(Object),
windowBounds: expect.objectContaining({left: 12, top: 34})
}),
expect.objectContaining({
backgroundColor: 0xffffffff,
scale: 1,
height: 50,
width: 200,
x: 0,
y: 0,
scrollX: 12,
scrollY: 34,
canvas: undefined
})
);
Expand All @@ -52,6 +55,7 @@ describe('html2canvas', () => {
it('should have transparent background with backgroundColor: null', async () => {
await html2canvas(element, {backgroundColor: null});
expect(CanvasRenderer).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
backgroundColor: COLORS.TRANSPARENT
})
Expand All @@ -62,6 +66,7 @@ describe('html2canvas', () => {
const canvas = {} as HTMLCanvasElement;
await html2canvas(element, {canvas});
expect(CanvasRenderer).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
canvas
})
Expand All @@ -72,15 +77,14 @@ describe('html2canvas', () => {
DocumentCloner.destroy = jest.fn();
await html2canvas(element, {removeContainer: false});
expect(CanvasRenderer).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
backgroundColor: 0xffffffff,
scale: 1,
height: 50,
width: 200,
x: 0,
y: 0,
scrollX: 12,
scrollY: 34,
canvas: undefined
})
);
Expand Down
23 changes: 1 addition & 22 deletions src/core/__mocks__/cache-storage.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1 @@
class MockCache {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _cache: {[key: string]: Promise<any>};

constructor() {
this._cache = {};
}

addImage(src: string): Promise<void> {
const result = Promise.resolve();
this._cache[src] = result;
return result;
}
}

const current = new MockCache();

export class CacheStorage {
static getInstance(): MockCache {
return current;
}
}
export class CacheStorage {}
19 changes: 19 additions & 0 deletions src/core/__mocks__/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {logger, Logger} from './logger';

export class Context {
readonly logger: Logger = logger;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly _cache: {[key: string]: Promise<any>} = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly cache: any;

constructor() {
this.cache = {
addImage: jest.fn().mockImplementation((src: string): Promise<void> => {
const result = Promise.resolve();
this._cache[src] = result;
return result;
})
};
}
}
2 changes: 1 addition & 1 deletion src/core/__mocks__/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ export class Logger {
error(): void {}
}

const logger = new Logger();
export const logger = new Logger();
49 changes: 27 additions & 22 deletions src/core/__tests__/cache-storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {deepStrictEqual, fail} from 'assert';
import {FEATURES} from '../features';
import {CacheStorage} from '../cache-storage';
import {Logger} from '../logger';
import {Context} from '../context';
import {Bounds} from '../../css/layout/bounds';

const proxy = 'http://example.com/proxy';

Expand Down Expand Up @@ -35,14 +36,18 @@ const createMockContext = (origin: string, opts = {}) => {
};

CacheStorage.setContext(context as Window);
Logger.create({id: 'test', enabled: false});
return CacheStorage.create('test', {
imageTimeout: 0,
useCORS: false,
allowTaint: false,
proxy,
...opts
});

return new Context(
{
logging: false,
imageTimeout: 0,
useCORS: false,
allowTaint: false,
proxy,
...opts
},
new Bounds(0, 0, 0, 0)
);
};

const images: ImageMock[] = [];
Expand Down Expand Up @@ -121,7 +126,7 @@ describe('cache-storage', () => {
images.splice(0, images.length);
});
it('addImage adds images to cache', async () => {
const cache = createMockContext('http://example.com', {proxy: null});
const {cache} = createMockContext('http://example.com', {proxy: null});
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test2.jpg');

Expand All @@ -131,7 +136,7 @@ describe('cache-storage', () => {
});

it('addImage should not add duplicate entries', async () => {
const cache = createMockContext('http://example.com');
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test.jpg');

Expand All @@ -141,7 +146,7 @@ describe('cache-storage', () => {

describe('svg', () => {
it('should add svg images correctly', async () => {
const cache = createMockContext('http://example.com');
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');

Expand All @@ -152,7 +157,7 @@ describe('cache-storage', () => {

it('should omit svg images if not supported', async () => {
setFeatures({SUPPORT_SVG_DRAWING: false});
const cache = createMockContext('http://example.com');
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');

Expand All @@ -162,15 +167,15 @@ describe('cache-storage', () => {

describe('cross-origin', () => {
it('addImage should not add images it cannot load/render', async () => {
const cache = createMockContext('http://example.com', {
const {cache} = createMockContext('http://example.com', {
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});

it('addImage should add images if tainting enabled', async () => {
const cache = createMockContext('http://example.com', {
const {cache} = createMockContext('http://example.com', {
allowTaint: true,
proxy: undefined
});
Expand All @@ -181,7 +186,7 @@ describe('cache-storage', () => {
});

it('addImage should add images if cors enabled', async () => {
const cache = createMockContext('http://example.com', {useCORS: true});
const {cache} = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
Expand All @@ -191,7 +196,7 @@ describe('cache-storage', () => {
it('addImage should not add images if cors enabled but not supported', async () => {
setFeatures({SUPPORT_CORS_IMAGES: false});

const cache = createMockContext('http://example.com', {
const {cache} = createMockContext('http://example.com', {
useCORS: true,
proxy: undefined
});
Expand All @@ -200,15 +205,15 @@ describe('cache-storage', () => {
});

it('addImage should not add images to proxy if cors enabled', async () => {
const cache = createMockContext('http://example.com', {useCORS: true});
const {cache} = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});

it('addImage should use proxy ', async () => {
const cache = createMockContext('http://example.com');
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(xhr.length, 1);
deepStrictEqual(
Expand All @@ -222,7 +227,7 @@ describe('cache-storage', () => {
});

it('proxy should respect imageTimeout', async () => {
const cache = createMockContext('http://example.com', {
const {cache} = createMockContext('http://example.com', {
imageTimeout: 10
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
Expand All @@ -244,7 +249,7 @@ describe('cache-storage', () => {
});

it('match should return cache entry', async () => {
const cache = createMockContext('http://example.com');
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');

if (images[0].onload) {
Expand All @@ -257,7 +262,7 @@ describe('cache-storage', () => {
});

it('image should respect imageTimeout', async () => {
const cache = createMockContext('http://example.com', {imageTimeout: 10});
const {cache} = createMockContext('http://example.com', {imageTimeout: 10});
cache.addImage('http://example.com/test.jpg');

try {
Expand Down
51 changes: 5 additions & 46 deletions src/core/cache-storage.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
import {FEATURES} from './features';
import {Logger} from './logger';
import type {Context} from './context';

export class CacheStorage {
private static _caches: {[key: string]: Cache} = {};
private static _link?: HTMLAnchorElement;
private static _origin = 'about:blank';
private static _current: Cache | null = null;

static create(name: string, options: ResourceOptions): Cache {
return (CacheStorage._caches[name] = new Cache(name, options));
}

static destroy(name: string): void {
delete CacheStorage._caches[name];
}

static open(name: string): Cache {
const cache = CacheStorage._caches[name];
if (typeof cache !== 'undefined') {
return cache;
}

throw new Error(`Cache with key "${name}" not found`);
}

static getOrigin(url: string): string {
const link = CacheStorage._link;
Expand All @@ -43,22 +24,6 @@ export class CacheStorage {
CacheStorage._link = window.document.createElement('a');
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
}

static getInstance(): Cache {
const current = CacheStorage._current;
if (current === null) {
throw new Error(`No cache instance attached`);
}
return current;
}

static attachInstance(cache: Cache): void {
CacheStorage._current = cache;
}

static detachInstance(): void {
CacheStorage._current = null;
}
}

export interface ResourceOptions {
Expand All @@ -70,15 +35,9 @@ export interface ResourceOptions {

export class Cache {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _cache: {[key: string]: Promise<any>};
private readonly _options: ResourceOptions;
private readonly id: string;

constructor(id: string, options: ResourceOptions) {
this.id = id;
this._options = options;
this._cache = {};
}
private readonly _cache: {[key: string]: Promise<any>} = {};

constructor(private readonly context: Context, private readonly _options: ResourceOptions) {}

addImage(src: string): Promise<void> {
const result = Promise.resolve();
Expand Down Expand Up @@ -128,7 +87,7 @@ export class Cache {
src = await this.proxy(src);
}

Logger.getInstance(this.id).debug(`Added image ${key.substring(0, 256)}`);
this.context.logger.debug(`Added image ${key.substring(0, 256)}`);

return await new Promise((resolve, reject) => {
const img = new Image();
Expand Down
21 changes: 21 additions & 0 deletions src/core/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Logger} from './logger';
import {Cache, ResourceOptions} from './cache-storage';
import {Bounds} from '../css/layout/bounds';

export type ContextOptions = {
logging: boolean;
cache?: Cache;
} & ResourceOptions;

export class Context {
private readonly instanceName = `#${Context.instanceCount++}`;
readonly logger: Logger;
readonly cache: Cache;

private static instanceCount = 1;

constructor(options: ContextOptions, public windowBounds: Bounds) {
this.logger = new Logger({id: this.instanceName, enabled: options.logging});
this.cache = options.cache ?? new Cache(this, options);
}
}
Loading

0 comments on commit 878e37a

Please sign in to comment.