Repository: hustcc/jest-canvas-mock
Pull request #23 (2019-01-31 19:25:17)

Add support for createImageBitmap
# goals

- [x] implement ImageBitmap shell class
- [x] implement createImageBitmap in it's own file
- [x] modify drawImage to accept ImageBitmap
- [x] test if ImageBitmap is closed in drawImage
- [x] modify createPattern to accept ImageBitmap
- [x] test if ImageBitmap is closed in createPattern
- [x] keep 100% code coverage

# Current Code Coverage
```
------------------------------|----------|----------|----------|----------|-------------------|
File                          |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------------------|----------|----------|----------|----------|-------------------|
All files                     |      100 |      100 |      100 |      100 |                   |
 classes                      |      100 |      100 |      100 |      100 |                   |
  CanvasGradient.js           |      100 |      100 |      100 |      100 |                   |
  CanvasPattern.js            |      100 |      100 |      100 |      100 |                   |
  CanvasRenderingContext2D.js |      100 |      100 |      100 |      100 |                   |
  DOMMatrix.js                |      100 |      100 |      100 |      100 |                   |
  ImageBitmap.js              |      100 |      100 |      100 |      100 |                   |
  ImageData.js                |      100 |      100 |      100 |      100 |                   |
  Path2D.js                   |      100 |      100 |      100 |      100 |                   |
  TextMetrics.js              |      100 |      100 |      100 |      100 |                   |
 mock                         |      100 |      100 |      100 |      100 |                   |
  createImageBitmap.js        |      100 |      100 |      100 |      100 |                   |
  prototype.js                |      100 |      100 |      100 |      100 |                   |
------------------------------|----------|----------|----------|----------|-------------------|
```


Patch:
diff --git a/src/classes/CanvasRenderingContext2D.js b/src/classes/CanvasRenderingContext2D.js
index b37b98e..361ae8a 100644
--- a/src/classes/CanvasRenderingContext2D.js
+++ b/src/classes/CanvasRenderingContext2D.js
@@ -18,7 +18,7 @@ function parseCSSColor(value) {
   return void 0;
 }
 
-const testFuncs = ['getTransform', 'getImageData', 'save', 'restore', 'createPattern', 'createRadialGradient', 'addHitRegion', 'arc', 'arcTo', 'beginPath', 'clip', 'closePath', 'scale', 'stroke', 'clearHitRegions', 'clearRect', 'fillRect', 'strokeRect', 'rect', 'resetTransform', 'translate', 'moveTo', 'lineTo', 'bezierCurveTo', 'createLinearGradient', 'ellipse', 'measureText', 'rotate', 'drawImage', 'drawFocusIfNeeded', 'isPointInPath', 'isPointInStroke', 'putImageData', 'strokeText', 'fillText', 'quadraticCurveTo', 'removeHitRegion', 'fill', 'transform', 'scrollPathIntoView', 'createImageData'];
+const testFuncs = ['setTransform', 'getTransform', 'getImageData', 'save', 'restore', 'createPattern', 'createRadialGradient', 'addHitRegion', 'arc', 'arcTo', 'beginPath', 'clip', 'closePath', 'scale', 'stroke', 'clearHitRegions', 'clearRect', 'fillRect', 'strokeRect', 'rect', 'resetTransform', 'translate', 'moveTo', 'lineTo', 'bezierCurveTo', 'createLinearGradient', 'ellipse', 'measureText', 'rotate', 'drawImage', 'drawFocusIfNeeded', 'isPointInPath', 'isPointInStroke', 'putImageData', 'strokeText', 'fillText', 'quadraticCurveTo', 'removeHitRegion', 'fill', 'transform', 'scrollPathIntoView', 'createImageData'];
 const compositeOperations = ['source-over', 'source-in', 'source-out', 'source-atop', 'destination-over', 'destination-in', 'destination-out', 'destination-atop', 'lighter', 'copy', 'xor', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity', 'fillText', 'strokeText'];
 
 export default class CanvasRenderingContext2D {
@@ -152,10 +152,13 @@ export default class CanvasRenderingContext2D {
     if (type === '') type = 'repeat';
 
     if (type === 'repeat' || type === 'repeat-x' || type === 'repeat-y' || type === 'no-repeat') {
-      if (image instanceof HTMLImageElement) return new CanvasPattern(); // if (image instanceof SVGImageElement) return new CanvasPattern();
-
+      if (image instanceof HTMLImageElement) return new CanvasPattern();
+      if (image instanceof ImageBitmap) {
+        if (image._closed) throw new DOMException('ValidationError', 'DOMException: Failed to execute \'createPattern\' on \'CanvasRenderingContext2D\': The image source is detached.');
+        return new CanvasPattern();
+      }
       if (image instanceof HTMLVideoElement) return new CanvasPattern();
-      if (image instanceof HTMLCanvasElement) return new CanvasPattern(); // if (image instanceof ImageBitmap) return new CanvasPattern();
+      if (image instanceof HTMLCanvasElement) return new CanvasPattern();
     } else {
       throw new TypeError('Failed to execute \'createPattern\' on \'' + this.constructor.name + '\': The provided type (\'' + type + '\') is not one of \'repeat\', \'no-repeat\', \'repeat-x\', or \'repeat-y\'.');
     }
@@ -216,10 +219,13 @@ export default class CanvasRenderingContext2D {
     if (arguments.length < 3) throw new TypeError('Failed to execute \'drawImage\' on \'' + this.constructor.name + '\': 3 arguments required, but only ' + arguments.length + ' present.');
     if (arguments.length === 4 || arguments.length > 5 && arguments.length < 9) throw new TypeError('Failed to execute \'drawImage\' on \'' + this.constructor.name + '\': Valid arities are: [3, 5, 9], but 4 arguments provided.');
 
-    if (img instanceof HTMLImageElement || img instanceof HTMLCanvasElement || img instanceof HTMLVideoElement) {
+    if (img instanceof HTMLImageElement) return;
+    if (img instanceof ImageBitmap) {
+      if (img._closed) throw new DOMException('ValidationError', 'DOMException: Failed to execute \'drawImage\' on \'CanvasRenderingContext2D\': The image source is detached.');
       return;
     }
-
+    if (img instanceof HTMLVideoElement) return;
+    if (img instanceof HTMLCanvasElement) return;
     throw new TypeError('Failed to execute \'drawImage\' on \'' + this.constructor.name + '\': The provided value is not of type \'(CSSImageValue or HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)\'');
   }
 
diff --git a/src/classes/ImageBitmap.js b/src/classes/ImageBitmap.js
new file mode 100644
index 0000000..cf2265f
--- /dev/null
+++ b/src/classes/ImageBitmap.js
@@ -0,0 +1,18 @@
+export default class ImageBitmap {
+  width = 0;
+  height = 0;
+
+  _closed = false;
+
+  constructor(width, height) {
+    this.width = width;
+    this.height = height;
+    this.close = jest.fn(this.close.bind(this));
+  }
+
+  close() {
+    this.width = 0;
+    this.height = 0;
+    this._closed = true;
+  }
+}
diff --git a/src/mock/createImageBitmap.js b/src/mock/createImageBitmap.js
new file mode 100644
index 0000000..d1f9f19
--- /dev/null
+++ b/src/mock/createImageBitmap.js
@@ -0,0 +1,41 @@
+import ImageBitmap from '../classes/ImageBitmap';
+
+export default jest.fn(function createImageBitmap(img, sx, sy, sWidth, sHeight, options) {
+  var length = arguments.length;
+  return new Promise((resolve, reject) => {
+    if (length === 0) return reject(new TypeError('Failed to execute \'createImageBitmap\' on \'Window\': 1 argument required, but only 0 present.'));
+    if (length === 3 || length === 4) return reject(new TypeError('Failed to execute \'createImageBitmap\' on \'Window\': Valid arities are: [1, 2, 5, 6], but ' + length +' arguments provided.'));
+    let validImage = false;
+    if (img instanceof HTMLImageElement) validImage = true;
+    if (img instanceof HTMLVideoElement) validImage = true;
+    if (img instanceof HTMLCanvasElement) validImage = true;
+    if (img instanceof Blob) validImage = true;
+    if (img instanceof ImageBitmap) validImage = true;
+    if (img instanceof ImageData) validImage = true;
+    if (!validImage) return reject(new TypeError('Failed to execute \'createImageBitmap\' on \'Window\': The provided value is not of type \'(HTMLImageElement or SVGImageElement or HTMLVideoElement or HTMLCanvasElement or Blob or ImageData or ImageBitmap or OffscreenCanvas)\''));
+    if (length >= 2) {
+      let index = 6;
+      if (length === 2) {
+        index = 2;
+        options = sx;
+      }
+      if (length === 5) options = null;
+      if (options !== null && options !== void 0) {
+        if (typeof options !== 'object') throw new TypeError('Failed to execute \'createImageBitmap\' on \'Window\': parameter ' + index + ' (\'options\') is not an object.');
+      }
+    }
+
+    if (length >= 5) {
+      sWidth = Number(sWidth);
+      sHeight = Number(sHeight);
+      if (sWidth === 0 || !Number.isFinite(sWidth)) return reject(new RangeError('The crop rect width is 0.'));
+      if (sHeight === 0 || !Number.isFinite(sHeight)) return reject(new RangeError('The crop rect height is 0.'));
+      sWidth = Math.abs(sWidth);
+      sHeight = Math.abs(sHeight);
+    } else {
+      sWidth = img.width || 1;
+      sHeight = img.height || 1;
+    }
+    return resolve(new ImageBitmap(sWidth, sHeight));
+  });
+});
diff --git a/src/window.js b/src/window.js
index 0e7bffa..8524acf 100644
--- a/src/window.js
+++ b/src/window.js
@@ -10,7 +10,9 @@ import CanvasRenderingContext2D from './classes/CanvasRenderingContext2D';
 import DOMMatrix from './classes/DOMMatrix';
 import ImageData from './classes/ImageData';
 import TextMetrics from './classes/TextMetrics';
+import ImageBitmap from './classes/ImageBitmap';
 import mockPrototype from './mock/prototype';
+import createImageBitmap from './mock/createImageBitmap';
 
 export default win => {
   const d = win.document;
@@ -49,7 +51,10 @@ export default win => {
   if (!win.DOMMatrix) win.DOMMatrix = DOMMatrix;
   if (!win.ImageData) win.ImageData = ImageData;
   if (!win.TextMetrics) win.TextMetrics = TextMetrics;
+  if (!win.ImageBitmap) win.ImageBitmap = ImageBitmap;
+  if (!win.createImageBitmap) win.createImageBitmap = createImageBitmap;
 
   mockPrototype();
+
   return win;
 }


Test patch:
diff --git a/__tests__/classes/CanvasRenderingContext2D.createImagePattern.js b/__tests__/classes/CanvasRenderingContext2D.createImagePattern.js
index 47f6a08..5c0856d 100644
--- a/__tests__/classes/CanvasRenderingContext2D.createImagePattern.js
+++ b/__tests__/classes/CanvasRenderingContext2D.createImagePattern.js
@@ -54,6 +54,16 @@ describe('createImagePattern', () => {
     expect(ctx.createPattern(canvas, 'repeat')).toBeInstanceOf(CanvasPattern);
   });
 
+  it('should create a pattern when image is ImageBitmap', () => {
+    expect(ctx.createPattern(new ImageBitmap(400, 300), 'repeat')).toBeInstanceOf(CanvasPattern);
+  });
+
+  it('should not create a pattern if the image bitmap is closed', () => {
+    const bmp = new ImageBitmap(400, 300);
+    bmp.close();
+    expect(() => ctx.createPattern(bmp, 'repeat')).toThrow(DOMException);
+  });
+
   it('should create a valid pattern for all repeat types', () => {
     const image = new Image();
     image.src = 'test/myImage.jpg';
diff --git a/__tests__/classes/CanvasRenderingContext2D.drawImage.js b/__tests__/classes/CanvasRenderingContext2D.drawImage.js
index 5c79d3d..8c3a971 100644
--- a/__tests__/classes/CanvasRenderingContext2D.drawImage.js
+++ b/__tests__/classes/CanvasRenderingContext2D.drawImage.js
@@ -29,6 +29,16 @@ describe('drawImage', () => {
     expect(() => ctx.drawImage(canvas, 1, 2)).not.toThrow();
   });
 
+  it('should draw when image is ImageBitmap', () => {
+    expect(() => ctx.drawImage(new ImageBitmap(100, 100), 1, 2)).not.toThrow();
+  });
+
+  it('should not draw if the image bitmap is closed', () => {
+    const bmp = new ImageBitmap(100, 100);
+    bmp.close();
+    expect(() => ctx.drawImage(bmp, 1, 2)).toThrow(DOMException);
+  });
+
   it('should accept 3, 5, and 9 parameters', () => {
     expect(() => ctx.drawImage(img, 1, 2)).not.toThrow();
     expect(() => ctx.drawImage(img, 1, 2, 3, 4)).not.toThrow();
@@ -54,4 +64,3 @@ describe('drawImage', () => {
     });
   });
 });
-
diff --git a/__tests__/classes/CanvasRenderingContext2D.setTransform.js b/__tests__/classes/CanvasRenderingContext2D.setTransform.js
index 3506b8c..a71ccd0 100644
--- a/__tests__/classes/CanvasRenderingContext2D.setTransform.js
+++ b/__tests__/classes/CanvasRenderingContext2D.setTransform.js
@@ -19,6 +19,15 @@ function every(items, callback) {
 }
 
 describe('setTransform', () => {
+  it('should be a function', () => {
+    expect(typeof ctx.setTransform).toBe('function');
+  });
+
+  it('should be callable', () => {
+    ctx.setTransform(1, 2, 3, 4, 5, 6);
+    expect(ctx.setTransform).toBeCalled();
+  });
+
   it('should validate setTransform input', () => {
     [
       [1, 2, 3, 4, 5, 6],
diff --git a/__tests__/classes/ImageBitmap.js b/__tests__/classes/ImageBitmap.js
new file mode 100644
index 0000000..83d0b56
--- /dev/null
+++ b/__tests__/classes/ImageBitmap.js
@@ -0,0 +1,131 @@
+var img = new Image();
+img.src = 'test.jpg';
+
+/**
+ * If a promise is expected to reject, then this function, when passed to the `.then()` function,
+ * will throw -1 instead of the expected error *first*.
+ */
+function throwError() {
+  throw -1;
+}
+
+function expectTypeError(e) {
+  expect(e).toBeInstanceOf(TypeError);
+}
+
+function expectRangeError(e) {
+  expect(e).toBeInstanceOf(RangeError);
+}
+
+function expectImageBitmap(e) {
+  expect(e).toBeInstanceOf(ImageBitmap);
+}
+
+describe('image bitmaps', () => {
+  it('should be a function', () => {
+    expect(typeof createImageBitmap).toBe('function');
+  });
+
+  it('should be callable', () => {
+    createImageBitmap(img);
+    expect(createImageBitmap).toBeCalled();
+  });
+
+  it('should return a promise', () => {
+    expect(createImageBitmap(img)).toBeInstanceOf(Promise);
+  });
+
+  it('should resolve to ImageBitmap', () => {
+    return createImageBitmap(img).then(expectImageBitmap);
+  });
+
+  it('should resolve to a type error if no parameters are passed to it', () => {
+    return createImageBitmap().then(throwError).catch(expectTypeError);
+  });
+
+  it('should reject non images', () => {
+    return Promise.all([0, null, void 0, '', 'test', window, Infinity, document]
+      .map(e => createImageBitmap(e).then(throwError).catch(expectTypeError))
+    );
+  });
+
+  it('should reject if second parameter is not an object', () => {
+    return Promise.all(['', 0, NaN, Infinity].map(e => {
+      return createImageBitmap(img, e).then(throwError).catch(expectTypeError);
+    }));
+  });
+
+  it('should throw if arity is 3 or 4', () => {
+    return Promise.all([
+      createImageBitmap(img, 1, 2).then(throwError).catch(expectTypeError),
+      createImageBitmap(img, 1, 2, 3).then(throwError).catch(expectTypeError),
+    ]);
+  });
+
+  it('should accept 5 parameters if the last 4 parameters are numbers', () => {
+    return createImageBitmap(img, 1, 2, 3, 4).then(expectImageBitmap);
+  });
+
+  it('should throw if width or height is not finite or 0', () => {
+    return Promise.all([
+      createImageBitmap(img, 1, 2, NaN, 3).then(throwError).catch(expectRangeError),
+      createImageBitmap(img, 1, 2, 0, 3).then(throwError).catch(expectRangeError),
+      createImageBitmap(img, 1, 2, 3, NaN).then(throwError).catch(expectRangeError),
+      createImageBitmap(img, 1, 2, 3, 0).then(throwError).catch(expectRangeError),
+    ]);
+  });
+
+  it('should throw if last parameter is not object if source rect is provided', () => {
+    return Promise.all(
+      ['', 0, NaN, Infinity].map(
+        e => createImageBitmap(img, 1, 2, 3, 4, e).then(throwError).catch(expectTypeError)
+      )
+    );
+  });
+
+  it('should have a close function', () => {
+    return createImageBitmap(img).then(e => {
+      expect(typeof e.close).toBe('function');
+    });
+  });
+
+  it('should have a callable close function', () => {
+    return createImageBitmap(img).then(e => {
+      e.close();
+      expect(e.close).toBeCalled();
+    });
+  });
+
+  it('should close the bitmap', () => {
+    return createImageBitmap(img).then(e => {
+      e.close();
+      expect(e.width).toBe(0);
+      expect(e.height).toBe(0);
+      expect(e._closed).toBe(true);
+    });
+  });
+
+  it('should create image bitmaps from valid sources', () => {
+    return Promise.all([
+      document.createElement('img'),
+      new Image(),
+      document.createElement('canvas'),
+      document.createElement('video'),
+      new Blob([new Uint8Array(1)]),
+      new ImageData(400, 300),
+      new ImageBitmap(100, 100), // this is just to verify class input
+    ].map(e => createImageBitmap(e).then(expectImageBitmap)));
+  });
+
+  it('should reject if sixth parameter is not an object', () => {
+    return Promise.all(['', 0, NaN, Infinity].map(e => {
+      return createImageBitmap(img, 1, 2, 3, 4, e).then(throwError).catch(expectTypeError);
+    }));
+  });
+
+  it('should accept if sixth parameter is null, undefined, or object', () => {
+    return Promise.all([null, void 0, {}].map(e => {
+      return createImageBitmap(img, 1, 2, 3, 4, e).then(expectImageBitmap);
+    }));
+  });
+});