Repository: hustcc/jest-canvas-mock (js)

Issue:
new `mockPrototype(win)` implementation can cause `TypeError`
I am running into an issue when upgrading to [`2.5.1`](https://renovatebot.com/diffs/npm/jest-canvas-mock/2.5.0/2.5.1) and I believe it has something to due with changes in #100.

```log
...
client:test: TypeError: Cannot read properties of undefined (reading 'prototype')
client:test:  ❯ mockPrototype ../../node_modules/.pnpm/jest-canvas-mock@2.5.1/node_modules/jest-canvas-mock/lib/mock/prototype.js:37:50
client:test:  ❯ Proxy._default ../../node_modules/.pnpm/jest-canvas-mock@2.5.1/node_modules/jest-canvas-mock/lib/window.js:61:26
client:test:  ❯ src/setupTests.ts:31:22
client:test:      [29](<code-under-test>/actions/runs/5090740096/jobs/9149933044?pr=1141#step:8:30)| ] as const;
client:test:      [30](<code-under-test>/actions/runs/5090740096/jobs/9149933044?pr=1141#step:8:31)| 
client:test:      [31](<code-under-test>/actions/runs/5090740096/jobs/9149933044?pr=1141#step:8:32)| const canvasWindow = getCanvasWindow({ document: window.document });
client:test:        |                      ^
client:test:      [32](<code-under-test>/actions/runs/5090740096/jobs/9149933044?pr=1141#step:8:33)| 
client:test:      [33](<code-under-test>/actions/runs/5090740096/jobs/9149933044?pr=1141#step:8:34)| apis.forEach((api) => {
...
```

looking at [the test case](https://github.com/hustcc/jest-canvas-mock/blob/master/__tests__/mock/window.js), it only covers when there is a wellformed window object.

Not sure what the expected behavior when something other than a wellformed window object is; however I was not seeing the above error in `2.5.0` and below. There seems to some delta between the global objects (`HTMLCanvasElement`, etc.) and this newly passed window object.

currently failing line: https://github.com/hustcc/jest-canvas-mock/blob/master/src/mock/prototype.js#L37

```js
...
  win.HTMLCanvasElement.prototype.getContext = getContext2D;
...
```

---

## suggestion

_suggestion_ - update the code to have this change be more conditional where we check if the passed in `win` object has the necessary props to not error in functions like `getContext2D`, and if it does not; fall back to the old behavior ( `mockPrototype()` ).

drawbacks, this potentially might be masking something in a project where the user expects the browser to behave one way; and due to all this mocking abstraction it behaves differently during testing. I think this is acceptable since, we are already basically doing this now, it just will behave slightly differently with the proposed suggestion than it does now, so I wanted to call it out.

_Originally posted by @virtuoushub in https://github.com/hustcc/jest-canvas-mock/issues/100#issuecomment-1564406609_
            

Pull request description:
fix(jest-canvas-mock): account for possible `TypeError`
closes #115 

Patch:
diff --git a/package.json b/package.json
index b36d02b..a8e47ef 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "jest-canvas-mock",
-  "version": "2.5.1",
+  "version": "2.5.2",
   "description": "Mock a canvas in your jest tests.",
   "main": "lib/index.js",
   "types": "types/index.d.ts",
diff --git a/src/mock/prototype.js b/src/mock/prototype.js
index 3983654..9b31b5c 100644
--- a/src/mock/prototype.js
+++ b/src/mock/prototype.js
@@ -29,12 +29,17 @@ export default function mockPrototype(win) {
     return getContext2D.internal.call(this, type);
   });
 
-  if (!jest.isMockFunction(win.HTMLCanvasElement.prototype.getContext)) {
-    getContext2D.internal = win.HTMLCanvasElement.prototype.getContext;
+  let htmlCanvasElementPrototype = HTMLCanvasElement.prototype;
+  if (win?.HTMLCanvasElement?.prototype) {
+    htmlCanvasElementPrototype = win?.HTMLCanvasElement?.prototype;
+  }
+
+  if (!jest.isMockFunction(htmlCanvasElementPrototype.getContext)) {
+    getContext2D.internal = htmlCanvasElementPrototype.getContext;
   } else {
-    getContext2D.internal = win.HTMLCanvasElement.prototype.getContext.internal;
+    getContext2D.internal = htmlCanvasElementPrototype.getContext.internal;
   }
-  win.HTMLCanvasElement.prototype.getContext = getContext2D;
+  htmlCanvasElementPrototype.getContext = getContext2D;
 
   /**
    * This function technically throws SecurityError at runtime, but it cannot be mocked, because
@@ -73,12 +78,12 @@ export default function mockPrototype(win) {
     setTimeout(() => callback(blob), 0);
   });
 
-  if (!jest.isMockFunction(win.HTMLCanvasElement.prototype.toBlob)) {
-    toBlobOverride.internal = win.HTMLCanvasElement.prototype.toBlob;
+  if (!jest.isMockFunction(htmlCanvasElementPrototype.toBlob)) {
+    toBlobOverride.internal = htmlCanvasElementPrototype.toBlob;
   } else {
-    toBlobOverride.internal = win.HTMLCanvasElement.prototype.toBlob.internal;
+    toBlobOverride.internal = htmlCanvasElementPrototype.toBlob.internal;
   }
-  win.HTMLCanvasElement.prototype.toBlob = toBlobOverride;
+  htmlCanvasElementPrototype.toBlob = toBlobOverride;
 
   /**
    * This section creates a dataurl with a validated mime type. This is not actually valid, because
@@ -103,11 +108,10 @@ export default function mockPrototype(win) {
     return 'data:' + type + ';base64,00';
   });
 
-  if (!jest.isMockFunction(win.HTMLCanvasElement.prototype.toDataURL)) {
-    toDataURLOverride.internal = win.HTMLCanvasElement.prototype.toDataURL;
+  if (!jest.isMockFunction(htmlCanvasElementPrototype.toDataURL)) {
+    toDataURLOverride.internal = htmlCanvasElementPrototype.toDataURL;
   } else {
-    toDataURLOverride.internal =
-      win.HTMLCanvasElement.prototype.toDataURL.internal;
+    toDataURLOverride.internal = htmlCanvasElementPrototype.toDataURL.internal;
   }
-  win.HTMLCanvasElement.prototype.toDataURL = toDataURLOverride;
+  htmlCanvasElementPrototype.toDataURL = toDataURLOverride;
 }


Test patch:
diff --git a/__tests__/mock/prototype.js b/__tests__/mock/prototype.js
index aa8f0c8..459834a 100644
--- a/__tests__/mock/prototype.js
+++ b/__tests__/mock/prototype.js
@@ -1,3 +1,6 @@
+import { JSDOM } from 'jsdom';
+import mockPrototype from '../../src/mock/prototype';
+
 let canvas;
 
 beforeEach(() => {
@@ -117,4 +120,33 @@ describe('mock', () => {
     const second = canvas.getContext('2d');
     expect(first).toBe(second);
   });
+
+  it('should work in both cases where passed in window object has and does not have .HTMLCanvasElement.prototype', () => {
+    // arrange/act
+    const mockWin = new JSDOM().window;
+    mockPrototype();
+    // assert (should not have effected the mockWindow since we did not pass it in )
+    expect(
+      jest.isMockFunction(mockWin.HTMLCanvasElement.prototype.getContext)
+    ).toBe(false);
+    expect(
+      jest.isMockFunction(mockWin.HTMLCanvasElement.prototype.toBlob)
+    ).toBe(false);
+    expect(
+      jest.isMockFunction(mockWin.HTMLCanvasElement.prototype.toDataURL)
+    ).toBe(false);
+
+    // act
+    mockPrototype(mockWin);
+    // assert ( should mock out expected fields in passed in window object )
+    expect(
+      jest.isMockFunction(mockWin.HTMLCanvasElement.prototype.getContext)
+    ).toBe(true);
+    expect(
+      jest.isMockFunction(mockWin.HTMLCanvasElement.prototype.toBlob)
+    ).toBe(true);
+    expect(
+      jest.isMockFunction(mockWin.HTMLCanvasElement.prototype.toDataURL)
+    ).toBe(true);
+  });
 });
diff --git a/__tests__/mock/window.js b/__tests__/mock/window.js
index b3db593..39cdd3a 100644
--- a/__tests__/mock/window.js
+++ b/__tests__/mock/window.js
@@ -27,4 +27,21 @@ describe('mockWindow', () => {
       true
     );
   });
+
+  it('mocks without a fully formed passed in window object', () => {
+    const win = mockWindow({ document: {} });
+
+    expect(win.Path2D).not.toBeNull();
+    expect(win.CanvasGradient).not.toBeNull();
+    expect(win.CanvasPattern).not.toBeNull();
+    expect(win.CanvasRenderingContext2D).not.toBeNull();
+    expect(win.DOMMatrix).not.toBeNull();
+    expect(win.ImageData).not.toBeNull();
+    expect(win.TextMetrics).not.toBeNull();
+    expect(win.ImageBitmap).not.toBeNull();
+    expect(win.createImageBitmap).not.toBeNull();
+
+    expect(jest.isMockFunction(win.HTMLCanvasElement)).toBe(false);
+    expect(win.HTMLCanvasElement).toBeUndefined();
+  });
 });