import {deepStrictEqual, fail} from 'assert'; import {FEATURES} from '../features'; import {CacheStorage} from '../cache-storage'; import {Logger} from '../logger'; const proxy = 'http://example.com/proxy'; const createMockContext = (origin: string, opts = {}) => { const context = { location: { href: origin }, document: { createElement(_name: string) { let _href = ''; return { set href(value: string) { _href = value; }, get href() { return _href; }, get protocol() { return new URL(_href).protocol; }, get hostname() { return new URL(_href).hostname; }, get port() { return new URL(_href).port; } }; } } }; CacheStorage.setContext(context as Window); Logger.create({id: 'test', enabled: false}); return CacheStorage.create('test', { imageTimeout: 0, useCORS: false, allowTaint: false, proxy, ...opts }); }; const images: ImageMock[] = []; const xhr: XMLHttpRequestMock[] = []; const sleep = async (timeout: number) => await new Promise(resolve => setTimeout(resolve, timeout)); class ImageMock { src?: string; crossOrigin?: string; onload?: () => {}; constructor() { images.push(this); } } class XMLHttpRequestMock { sent: boolean; status: number; timeout: number; method?: string; url?: string; response?: string; onload?: () => {}; ontimeout?: () => {}; constructor() { this.sent = false; this.status = 500; this.timeout = 5000; xhr.push(this); } async load(status: number, response: string) { this.response = response; this.status = status; if (this.onload) { this.onload(); } await sleep(0); } open(method: string, url: string) { this.method = method; this.url = url; } send() { this.sent = true; } } Object.defineProperty(global, 'Image', {value: ImageMock, writable: true}); Object.defineProperty(global, 'XMLHttpRequest', { value: XMLHttpRequestMock, writable: true }); const setFeatures = (opts: {[key: string]: boolean} = {}) => { const defaults: {[key: string]: boolean} = { SUPPORT_SVG_DRAWING: true, SUPPORT_CORS_IMAGES: true, SUPPORT_CORS_XHR: true, SUPPORT_RESPONSE_TYPE: false }; Object.keys(defaults).forEach(key => { Object.defineProperty(FEATURES, key, { value: typeof opts[key] === 'boolean' ? opts[key] : defaults[key], writable: true }); }); }; describe('cache-storage', () => { beforeEach(() => setFeatures()); afterEach(() => { xhr.splice(0, xhr.length); images.splice(0, images.length); }); it('addImage adds images to cache', async () => { const cache = createMockContext('http://example.com', {proxy: null}); await cache.addImage('http://example.com/test.jpg'); await cache.addImage('http://example.com/test2.jpg'); deepStrictEqual(images.length, 2); deepStrictEqual(images[0].src, 'http://example.com/test.jpg'); deepStrictEqual(images[1].src, 'http://example.com/test2.jpg'); }); it('addImage should not add duplicate entries', async () => { const cache = createMockContext('http://example.com'); await cache.addImage('http://example.com/test.jpg'); await cache.addImage('http://example.com/test.jpg'); deepStrictEqual(images.length, 1); deepStrictEqual(images[0].src, 'http://example.com/test.jpg'); }); describe('svg', () => { it('should add svg images correctly', async () => { const cache = createMockContext('http://example.com'); await cache.addImage('http://example.com/test.svg'); await cache.addImage('http://example.com/test2.svg'); deepStrictEqual(images.length, 2); deepStrictEqual(images[0].src, 'http://example.com/test.svg'); deepStrictEqual(images[1].src, 'http://example.com/test2.svg'); }); it('should omit svg images if not supported', async () => { setFeatures({SUPPORT_SVG_DRAWING: false}); const cache = createMockContext('http://example.com'); await cache.addImage('http://example.com/test.svg'); await cache.addImage('http://example.com/test2.svg'); deepStrictEqual(images.length, 0); }); }); describe('cross-origin', () => { it('addImage should not add images it cannot load/render', async () => { 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', { allowTaint: true, proxy: undefined }); 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, undefined); }); it('addImage should add images if cors enabled', async () => { 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 not add images if cors enabled but not supported', async () => { setFeatures({SUPPORT_CORS_IMAGES: false}); const cache = createMockContext('http://example.com', { useCORS: true, proxy: undefined }); await cache.addImage('http://html2canvas.hertzen.com/test.jpg'); deepStrictEqual(images.length, 0); }); it('addImage should not add images to proxy if cors enabled', async () => { 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'); await cache.addImage('http://html2canvas.hertzen.com/test.jpg'); deepStrictEqual(xhr.length, 1); deepStrictEqual( xhr[0].url, `${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text` ); await xhr[0].load(200, ''); deepStrictEqual(images.length, 1); deepStrictEqual(images[0].src, ''); }); it('proxy should respect imageTimeout', async () => { const cache = createMockContext('http://example.com', { imageTimeout: 10 }); await cache.addImage('http://html2canvas.hertzen.com/test.jpg'); deepStrictEqual(xhr.length, 1); deepStrictEqual( xhr[0].url, `${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text` ); deepStrictEqual(xhr[0].timeout, 10); if (xhr[0].ontimeout) { xhr[0].ontimeout(); } try { await cache.match('http://html2canvas.hertzen.com/test.jpg'); fail('Expected result to timeout'); } catch (e) {} }); }); it('match should return cache entry', async () => { const cache = createMockContext('http://example.com'); await cache.addImage('http://example.com/test.jpg'); if (images[0].onload) { images[0].onload(); } const response = await cache.match('http://example.com/test.jpg'); deepStrictEqual(response.src, 'http://example.com/test.jpg'); }); it('image should respect imageTimeout', async () => { const cache = createMockContext('http://example.com', {imageTimeout: 10}); cache.addImage('http://example.com/test.jpg'); try { await cache.match('http://example.com/test.jpg'); fail('Expected result to timeout'); } catch (e) {} }); });