Browse Source

Add test coverage (#23341)

Michael Telatynski 3 years ago
parent
commit
5ec96f5abe

+ 6 - 0
package.json

@@ -90,6 +90,7 @@
     "@principalstudio/html-webpack-inject-preload": "^1.2.7",
     "@principalstudio/html-webpack-inject-preload": "^1.2.7",
     "@sentry/webpack-plugin": "^1.18.1",
     "@sentry/webpack-plugin": "^1.18.1",
     "@svgr/webpack": "^5.5.0",
     "@svgr/webpack": "^5.5.0",
+    "@testing-library/react": "^12.1.5",
     "@types/flux": "^3.1.9",
     "@types/flux": "^3.1.9",
     "@types/jest": "^29.0.0",
     "@types/jest": "^29.0.0",
     "@types/modernizr": "^3.5.3",
     "@types/modernizr": "^3.5.3",
@@ -122,7 +123,9 @@
     "fs-extra": "^0.30.0",
     "fs-extra": "^0.30.0",
     "html-webpack-plugin": "^4.5.2",
     "html-webpack-plugin": "^4.5.2",
     "jest": "^29.0.0",
     "jest": "^29.0.0",
+    "jest-canvas-mock": "^2.3.0",
     "jest-environment-jsdom": "^29.0.0",
     "jest-environment-jsdom": "^29.0.0",
+    "jest-mock": "^27.5.1",
     "jest-raw-loader": "^1.0.1",
     "jest-raw-loader": "^1.0.1",
     "jest-sonar-reporter": "^2.0.0",
     "jest-sonar-reporter": "^2.0.0",
     "json-loader": "^0.5.7",
     "json-loader": "^0.5.7",
@@ -174,6 +177,9 @@
     "testMatch": [
     "testMatch": [
       "<rootDir>/test/**/*-test.[tj]s?(x)"
       "<rootDir>/test/**/*-test.[tj]s?(x)"
     ],
     ],
+    "setupFiles": [
+      "jest-canvas-mock"
+    ],
     "setupFilesAfterEnv": [
     "setupFilesAfterEnv": [
       "<rootDir>/node_modules/matrix-react-sdk/test/setupTests.js"
       "<rootDir>/node_modules/matrix-react-sdk/test/setupTests.js"
     ],
     ],

+ 17 - 12
src/favicon.ts

@@ -84,12 +84,19 @@ export default class Favicon {
         }
         }
     }
     }
 
 
-    private reset() {
+    private reset(): void {
         this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
         this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
         this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
         this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
     }
     }
 
 
-    private options(n: number | string, params: IParams) {
+    private options(n: number | string, params: IParams): {
+        n: string | number;
+        len: number;
+        x: number;
+        y: number;
+        w: number;
+        h: number;
+    } {
         const opt = {
         const opt = {
             n: ((typeof n) === "number") ? Math.abs(n as number | 0) : n,
             n: ((typeof n) === "number") ? Math.abs(n as number | 0) : n,
             len: ("" + n).length,
             len: ("" + n).length,
@@ -124,7 +131,7 @@ export default class Favicon {
         return opt;
         return opt;
     }
     }
 
 
-    private circle(n: number | string, opts?: Partial<IParams>) {
+    private circle(n: number | string, opts?: Partial<IParams>): void {
         const params = { ...this.params, ...opts };
         const params = { ...this.params, ...opts };
         const opt = this.options(n, params);
         const opt = this.options(n, params);
 
 
@@ -177,19 +184,19 @@ export default class Favicon {
         this.context.closePath();
         this.context.closePath();
     }
     }
 
 
-    private ready() {
+    private ready(): void {
         if (this.isReady) return;
         if (this.isReady) return;
         this.isReady = true;
         this.isReady = true;
         this.readyCb?.();
         this.readyCb?.();
     }
     }
 
 
-    private setIcon(canvas) {
+    private setIcon(canvas: HTMLCanvasElement): void {
         setImmediate(() => {
         setImmediate(() => {
             this.setIconSrc(canvas.toDataURL("image/png"));
             this.setIconSrc(canvas.toDataURL("image/png"));
         });
         });
     }
     }
 
 
-    private setIconSrc(url) {
+    private setIconSrc(url: string): void {
         // if is attached to fav icon
         // if is attached to fav icon
         if (this.browser.ff || this.browser.opera) {
         if (this.browser.ff || this.browser.opera) {
             // for FF we need to "recreate" element, attach to dom and remove old <link>
             // for FF we need to "recreate" element, attach to dom and remove old <link>
@@ -200,9 +207,7 @@ export default class Favicon {
             newIcon.setAttribute("type", "image/png");
             newIcon.setAttribute("type", "image/png");
             window.document.getElementsByTagName("head")[0].appendChild(newIcon);
             window.document.getElementsByTagName("head")[0].appendChild(newIcon);
             newIcon.setAttribute("href", url);
             newIcon.setAttribute("href", url);
-            if (old.parentNode) {
-                old.parentNode.removeChild(old);
-            }
+            old.parentNode?.removeChild(old);
         } else {
         } else {
             this.icons.forEach(icon => {
             this.icons.forEach(icon => {
                 icon.setAttribute("href", url);
                 icon.setAttribute("href", url);
@@ -210,7 +215,7 @@ export default class Favicon {
         }
         }
     }
     }
 
 
-    public badge(content: number | string, opts?: Partial<IParams>) {
+    public badge(content: number | string, opts?: Partial<IParams>): void {
         if (!this.isReady) {
         if (!this.isReady) {
             this.readyCb = () => {
             this.readyCb = () => {
                 this.badge(content, opts);
                 this.badge(content, opts);
@@ -227,7 +232,7 @@ export default class Favicon {
         this.setIcon(this.canvas);
         this.setIcon(this.canvas);
     }
     }
 
 
-    private static getLinks() {
+    private static getLinks(): HTMLLinkElement[] {
         const icons: HTMLLinkElement[] = [];
         const icons: HTMLLinkElement[] = [];
         const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link");
         const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link");
         for (const link of links) {
         for (const link of links) {
@@ -238,7 +243,7 @@ export default class Favicon {
         return icons;
         return icons;
     }
     }
 
 
-    private static getIcons() {
+    private static getIcons(): HTMLLinkElement[] {
         // get favicon link elements
         // get favicon link elements
         let elms = Favicon.getLinks();
         let elms = Favicon.getLinks();
         if (elms.length === 0) {
         if (elms.length === 0) {

+ 431 - 0
test/unit-tests/__snapshots__/favicon-test.ts.snap

@@ -0,0 +1,431 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Favicon should clear a badge if called with a zero value 1`] = `
+[
+  {
+    "props": {
+      "height": 32,
+      "width": 32,
+      "x": 0,
+      "y": 0,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "clearRect",
+  },
+  {
+    "props": {
+      "dHeight": 32,
+      "dWidth": 32,
+      "dx": 0,
+      "dy": 0,
+      "img": <img
+        height="32"
+        width="32"
+      />,
+      "sHeight": 32,
+      "sWidth": 32,
+      "sx": 0,
+      "sy": 0,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "drawImage",
+  },
+  {
+    "props": {
+      "fillRule": "nonzero",
+      "path": [
+        {
+          "props": {},
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "beginPath",
+        },
+        {
+          "props": {
+            "x": 16.159999999999997,
+            "y": 12.8,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "moveTo",
+        },
+        {
+          "props": {
+            "x": 22.4,
+            "y": 12.8,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+        {
+          "props": {
+            "x": 31.999999999999996,
+            "y": 22.4,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+        {
+          "props": {
+            "x": 9.92,
+            "y": 32,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+        {
+          "props": {
+            "x": 0.3200000000000003,
+            "y": 22.4,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+      ],
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "fill",
+  },
+  {
+    "props": {
+      "path": [
+        {
+          "props": {},
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "beginPath",
+        },
+      ],
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "stroke",
+  },
+  {
+    "props": {
+      "maxWidth": null,
+      "text": "123",
+      "x": 16,
+      "y": 29,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "fillText",
+  },
+  {
+    "props": {
+      "height": 32,
+      "width": 32,
+      "x": 0,
+      "y": 0,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "clearRect",
+  },
+  {
+    "props": {
+      "dHeight": 32,
+      "dWidth": 32,
+      "dx": 0,
+      "dy": 0,
+      "img": <img
+        height="32"
+        width="32"
+      />,
+      "sHeight": 32,
+      "sWidth": 32,
+      "sx": 0,
+      "sy": 0,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "drawImage",
+  },
+]
+`;
+
+exports[`Favicon should draw a badge if called with a non-zero value 1`] = `
+[
+  {
+    "props": {
+      "height": 32,
+      "width": 32,
+      "x": 0,
+      "y": 0,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "clearRect",
+  },
+  {
+    "props": {
+      "dHeight": 32,
+      "dWidth": 32,
+      "dx": 0,
+      "dy": 0,
+      "img": <img
+        height="32"
+        width="32"
+      />,
+      "sHeight": 32,
+      "sWidth": 32,
+      "sx": 0,
+      "sy": 0,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "drawImage",
+  },
+  {
+    "props": {
+      "fillRule": "nonzero",
+      "path": [
+        {
+          "props": {},
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "beginPath",
+        },
+        {
+          "props": {
+            "x": 16.159999999999997,
+            "y": 12.8,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "moveTo",
+        },
+        {
+          "props": {
+            "x": 22.4,
+            "y": 12.8,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+        {
+          "props": {
+            "x": 31.999999999999996,
+            "y": 22.4,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+        {
+          "props": {
+            "x": 9.92,
+            "y": 32,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+        {
+          "props": {
+            "x": 0.3200000000000003,
+            "y": 22.4,
+          },
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "lineTo",
+        },
+      ],
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "fill",
+  },
+  {
+    "props": {
+      "path": [
+        {
+          "props": {},
+          "transform": [
+            1,
+            0,
+            0,
+            1,
+            0,
+            0,
+          ],
+          "type": "beginPath",
+        },
+      ],
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "stroke",
+  },
+  {
+    "props": {
+      "maxWidth": null,
+      "text": "123",
+      "x": 16,
+      "y": 29,
+    },
+    "transform": [
+      1,
+      0,
+      0,
+      1,
+      0,
+      0,
+    ],
+    "type": "fillText",
+  },
+]
+`;

+ 27 - 0
test/unit-tests/async-components/structures/ErrorView-test.tsx

@@ -0,0 +1,27 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as React from "react";
+import { render } from "@testing-library/react";
+
+import ErrorView from "../../../../src/async-components/structures/ErrorView";
+
+describe("<ErrorView />", () => {
+    it("should match snapshot", () => {
+        const { asFragment } = render(<ErrorView title="TITLE" messages={["MSG1", "MSG2"]} />);
+        expect(asFragment()).toMatchSnapshot();
+    });
+});

+ 66 - 0
test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap

@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<ErrorView /> should match snapshot 1`] = `
+<DocumentFragment>
+  <div
+    class="mx_ErrorView"
+  >
+    <div
+      class="mx_ErrorView_container"
+    >
+      <div
+        class="mx_HomePage_header"
+      >
+        <span
+          class="mx_HomePage_logo"
+        >
+          <img
+            alt="Element"
+            height="42"
+            src="themes/element/img/logos/element-logo.svg"
+          />
+        </span>
+        <h1>
+          Failed to start
+        </h1>
+      </div>
+      <div
+        class="mx_HomePage_col"
+      >
+        <div
+          class="mx_HomePage_row"
+        >
+          <div>
+            <h2
+              id="step1_heading"
+            >
+              TITLE
+            </h2>
+            <p>
+              MSG1
+            </p>
+            <p>
+              MSG2
+            </p>
+          </div>
+        </div>
+      </div>
+      <div
+        class="mx_HomePage_row mx_Center mx_Spacer"
+      >
+        <p
+          class="mx_Spacer"
+        >
+          <a
+            class="mx_FooterLink"
+            href="https://element.io"
+            target="_blank"
+          >
+            Go to element.io
+          </a>
+        </p>
+      </div>
+    </div>
+  </div>
+</DocumentFragment>
+`;

+ 61 - 0
test/unit-tests/favicon-test.ts

@@ -0,0 +1,61 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import "jest-canvas-mock";
+
+import Favicon from "../../src/favicon";
+
+jest.useFakeTimers();
+
+describe("Favicon", () => {
+    beforeEach(() => {
+        const head = document.createElement("head");
+        window.document.documentElement.prepend(head);
+    });
+
+    it("should create a link element if one doesn't yet exist", () => {
+        const favicon = new Favicon();
+        expect(favicon).toBeTruthy();
+        const link = window.document.querySelector("link");
+        expect(link.rel).toContain("icon");
+    });
+
+    it("should draw a badge if called with a non-zero value", () => {
+        const favicon = new Favicon();
+        favicon.badge(123);
+        jest.runAllTimers();
+        expect(favicon["context"].__getDrawCalls()).toMatchSnapshot();
+    });
+
+    it("should clear a badge if called with a zero value", () => {
+        const favicon = new Favicon();
+        favicon.badge(123);
+        jest.runAllTimers();
+        favicon.badge(0);
+        expect(favicon["context"].__getDrawCalls()).toMatchSnapshot();
+    });
+
+    it("should recreate link element for firefox and opera", () => {
+        window["InstallTrigger"] = {};
+        window["opera"] = {};
+        const favicon = new Favicon();
+        const originalLink = window.document.querySelector("link");
+        favicon.badge(123);
+        jest.runAllTimers();
+        const newLink = window.document.querySelector("link");
+        expect(originalLink).not.toStrictEqual(newLink);
+    });
+});

+ 29 - 0
test/unit-tests/vector/platform/PWAPlatform-test.ts

@@ -14,7 +14,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
 
 
+import { mocked } from "jest-mock";
+
 import PWAPlatform from "../../../../src/vector/platform/PWAPlatform";
 import PWAPlatform from "../../../../src/vector/platform/PWAPlatform";
+import WebPlatform from "../../../../src/vector/platform/WebPlatform";
+
+jest.mock("../../../../src/vector/platform/WebPlatform");
 
 
 describe('PWAPlatform', () => {
 describe('PWAPlatform', () => {
     beforeEach(() => {
     beforeEach(() => {
@@ -29,5 +34,29 @@ describe('PWAPlatform', () => {
             platform.setNotificationCount(123);
             platform.setNotificationCount(123);
             expect(navigator.setAppBadge).toHaveBeenCalledWith(123);
             expect(navigator.setAppBadge).toHaveBeenCalledWith(123);
         });
         });
+
+        it("should no-op if the badge count isn't changing", () => {
+            navigator.setAppBadge = jest.fn().mockResolvedValue(undefined);
+            const platform = new PWAPlatform();
+            platform.setNotificationCount(123);
+            expect(navigator.setAppBadge).toHaveBeenCalledTimes(1);
+            platform.setNotificationCount(123);
+            expect(navigator.setAppBadge).toHaveBeenCalledTimes(1);
+        });
+
+        it("should fall back to WebPlatform::setNotificationCount if no Navigator::setAppBadge", () => {
+            navigator.setAppBadge = undefined;
+            const platform = new PWAPlatform();
+            const superMethod = mocked(WebPlatform.prototype.setNotificationCount);
+            expect(superMethod).not.toHaveBeenCalled();
+            platform.setNotificationCount(123);
+            expect(superMethod).toHaveBeenCalledWith(123);
+        });
+
+        it("should handle Navigator::setAppBadge rejecting gracefully", () => {
+            navigator.setAppBadge = jest.fn().mockRejectedValue(new Error);
+            const platform = new PWAPlatform();
+            expect(() => platform.setNotificationCount(123)).not.toThrow();
+        });
     });
     });
 });
 });

+ 43 - 0
test/unit-tests/vector/routing-test.ts

@@ -0,0 +1,43 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { onNewScreen } from "../../../src/vector/routing";
+
+describe("onNewScreen", () => {
+    it("should replace history if stripping via fields", () => {
+        delete window.location;
+        window.location = {
+            hash: "#/room/!room:server?via=abc",
+            replace: jest.fn(),
+            assign: jest.fn(),
+        } as unknown as Location;
+        onNewScreen("room/!room:server");
+        expect(window.location.assign).not.toHaveBeenCalled();
+        expect(window.location.replace).toHaveBeenCalled();
+    });
+
+    it("should not replace history if changing rooms", () => {
+        delete window.location;
+        window.location = {
+            hash: "#/room/!room1:server?via=abc",
+            replace: jest.fn(),
+            assign: jest.fn(),
+        } as unknown as Location;
+        onNewScreen("room/!room2:server");
+        expect(window.location.assign).toHaveBeenCalled();
+        expect(window.location.replace).not.toHaveBeenCalled();
+    });
+});

+ 1 - 1
test/unit-tests/url_utils-test.ts → test/unit-tests/vector/url_utils-test.ts

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
 
 
-import { parseQsFromFragment, parseQs } from "../../src/vector/url_utils";
+import { parseQsFromFragment, parseQs } from "../../../src/vector/url_utils";
 
 
 describe("url_utils.ts", function() {
 describe("url_utils.ts", function() {
     // @ts-ignore
     // @ts-ignore

+ 48 - 2
yarn.lock

@@ -1388,6 +1388,17 @@
     slash "^3.0.0"
     slash "^3.0.0"
     write-file-atomic "^4.0.1"
     write-file-atomic "^4.0.1"
 
 
+"@jest/types@^27.5.1":
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80"
+  integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^16.0.0"
+    chalk "^4.0.0"
+
 "@jest/types@^28.1.3":
 "@jest/types@^28.1.3":
   version "28.1.3"
   version "28.1.3"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b"
@@ -2194,6 +2205,13 @@
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
   integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
   integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
 
 
+"@types/yargs@^16.0.0":
+  version "16.0.4"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
+  integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
+  dependencies:
+    "@types/yargs-parser" "*"
+
 "@types/yargs@^17.0.8":
 "@types/yargs@^17.0.8":
   version "17.0.12"
   version "17.0.12"
   resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.12.tgz#0745ff3e4872b4ace98616d4b7e37ccbd75f9526"
   resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.12.tgz#0745ff3e4872b4ace98616d4b7e37ccbd75f9526"
@@ -3749,7 +3767,7 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
   integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
 
 
-color-name@^1.0.0, color-name@~1.1.4:
+color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
   version "1.1.4"
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -4241,6 +4259,11 @@ cssesc@^3.0.0:
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
 
+cssfontparser@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
+  integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==
+
 cssnano-preset-default@^4.0.8:
 cssnano-preset-default@^4.0.8:
   version "4.0.8"
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
   resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff"
@@ -7257,6 +7280,14 @@ istanbul-reports@^3.1.3:
     html-escaper "^2.0.0"
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
     istanbul-lib-report "^3.0.0"
 
 
+jest-canvas-mock@^2.3.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
+  integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
+  dependencies:
+    cssfontparser "^1.2.1"
+    moo-color "^1.0.2"
+
 jest-changed-files@^29.0.0:
 jest-changed-files@^29.0.0:
   version "29.0.0"
   version "29.0.0"
   resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.0.0.tgz#aa238eae42d9372a413dd9a8dadc91ca1806dce0"
   resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.0.0.tgz#aa238eae42d9372a413dd9a8dadc91ca1806dce0"
@@ -7487,6 +7518,14 @@ jest-message-util@^29.0.3:
     slash "^3.0.0"
     slash "^3.0.0"
     stack-utils "^2.0.3"
     stack-utils "^2.0.3"
 
 
+jest-mock@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6"
+  integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==
+  dependencies:
+    "@jest/types" "^27.5.1"
+    "@types/node" "*"
+
 jest-mock@^29.0.3:
 jest-mock@^29.0.3:
   version "29.0.3"
   version "29.0.3"
   resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.0.3.tgz#4f0093f6a9cb2ffdb9c44a07a3912f0c098c8de9"
   resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.0.3.tgz#4f0093f6a9cb2ffdb9c44a07a3912f0c098c8de9"
@@ -8330,7 +8369,7 @@ matrix-web-i18n@^1.3.0:
     "@babel/traverse" "^7.18.5"
     "@babel/traverse" "^7.18.5"
     walk "^2.3.15"
     walk "^2.3.15"
 
 
-matrix-widget-api@^1.1.1:
+matrix-widget-api@^1.0.0, matrix-widget-api@^1.1.1:
   version "1.1.1"
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962"
   resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962"
   integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA==
   integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA==
@@ -8650,6 +8689,13 @@ modernizr@^3.12.0:
     requirejs "^2.3.6"
     requirejs "^2.3.6"
     yargs "^15.4.1"
     yargs "^15.4.1"
 
 
+moo-color@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
+  integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==
+  dependencies:
+    color-name "^1.1.4"
+
 move-concurrently@^1.0.1:
 move-concurrently@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"