diff --git a/package.json b/package.json index 11edec7cc6..3aabffc736 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@principalstudio/html-webpack-inject-preload": "^1.2.7", "@sentry/webpack-plugin": "^1.18.1", "@svgr/webpack": "^5.5.0", + "@testing-library/react": "^12.1.5", "@types/flux": "^3.1.9", "@types/jest": "^29.0.0", "@types/modernizr": "^3.5.3", @@ -122,7 +123,9 @@ "fs-extra": "^0.30.0", "html-webpack-plugin": "^4.5.2", "jest": "^29.0.0", + "jest-canvas-mock": "^2.3.0", "jest-environment-jsdom": "^29.0.0", + "jest-mock": "^27.5.1", "jest-raw-loader": "^1.0.1", "jest-sonar-reporter": "^2.0.0", "json-loader": "^0.5.7", @@ -174,6 +177,9 @@ "testMatch": [ "/test/**/*-test.[tj]s?(x)" ], + "setupFiles": [ + "jest-canvas-mock" + ], "setupFilesAfterEnv": [ "/node_modules/matrix-react-sdk/test/setupTests.js" ], diff --git a/src/favicon.ts b/src/favicon.ts index 2212d2aad8..7488cac299 100644 --- a/src/favicon.ts +++ b/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.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 = { n: ((typeof n) === "number") ? Math.abs(n as number | 0) : n, len: ("" + n).length, @@ -124,7 +131,7 @@ export default class Favicon { return opt; } - private circle(n: number | string, opts?: Partial) { + private circle(n: number | string, opts?: Partial): void { const params = { ...this.params, ...opts }; const opt = this.options(n, params); @@ -177,19 +184,19 @@ export default class Favicon { this.context.closePath(); } - private ready() { + private ready(): void { if (this.isReady) return; this.isReady = true; this.readyCb?.(); } - private setIcon(canvas) { + private setIcon(canvas: HTMLCanvasElement): void { setImmediate(() => { this.setIconSrc(canvas.toDataURL("image/png")); }); } - private setIconSrc(url) { + private setIconSrc(url: string): void { // if is attached to fav icon if (this.browser.ff || this.browser.opera) { // for FF we need to "recreate" element, attach to dom and remove old @@ -200,9 +207,7 @@ export default class Favicon { newIcon.setAttribute("type", "image/png"); window.document.getElementsByTagName("head")[0].appendChild(newIcon); newIcon.setAttribute("href", url); - if (old.parentNode) { - old.parentNode.removeChild(old); - } + old.parentNode?.removeChild(old); } else { this.icons.forEach(icon => { icon.setAttribute("href", url); @@ -210,7 +215,7 @@ export default class Favicon { } } - public badge(content: number | string, opts?: Partial) { + public badge(content: number | string, opts?: Partial): void { if (!this.isReady) { this.readyCb = () => { this.badge(content, opts); @@ -227,7 +232,7 @@ export default class Favicon { this.setIcon(this.canvas); } - private static getLinks() { + private static getLinks(): HTMLLinkElement[] { const icons: HTMLLinkElement[] = []; const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link"); for (const link of links) { @@ -238,7 +243,7 @@ export default class Favicon { return icons; } - private static getIcons() { + private static getIcons(): HTMLLinkElement[] { // get favicon link elements let elms = Favicon.getLinks(); if (elms.length === 0) { diff --git a/test/unit-tests/__snapshots__/favicon-test.ts.snap b/test/unit-tests/__snapshots__/favicon-test.ts.snap new file mode 100644 index 0000000000..10ec81ed1a --- /dev/null +++ b/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": , + "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": , + "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": , + "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", + }, +] +`; diff --git a/test/unit-tests/async-components/structures/ErrorView-test.tsx b/test/unit-tests/async-components/structures/ErrorView-test.tsx new file mode 100644 index 0000000000..86898b0398 --- /dev/null +++ b/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("", () => { + it("should match snapshot", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap b/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap new file mode 100644 index 0000000000..9af471f05b --- /dev/null +++ b/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should match snapshot 1`] = ` + +
+
+
+ +

+ Failed to start +

+
+
+
+
+

+ TITLE +

+

+ MSG1 +

+

+ MSG2 +

+
+
+
+ +
+
+
+`; diff --git a/test/unit-tests/favicon-test.ts b/test/unit-tests/favicon-test.ts new file mode 100644 index 0000000000..8e2f45864c --- /dev/null +++ b/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); + }); +}); diff --git a/test/unit-tests/vector/platform/PWAPlatform-test.ts b/test/unit-tests/vector/platform/PWAPlatform-test.ts index 23c41399ec..4829fc04d3 100644 --- a/test/unit-tests/vector/platform/PWAPlatform-test.ts +++ b/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. */ +import { mocked } from "jest-mock"; + import PWAPlatform from "../../../../src/vector/platform/PWAPlatform"; +import WebPlatform from "../../../../src/vector/platform/WebPlatform"; + +jest.mock("../../../../src/vector/platform/WebPlatform"); describe('PWAPlatform', () => { beforeEach(() => { @@ -29,5 +34,29 @@ describe('PWAPlatform', () => { platform.setNotificationCount(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(); + }); }); }); diff --git a/test/unit-tests/vector/routing-test.ts b/test/unit-tests/vector/routing-test.ts new file mode 100644 index 0000000000..28676a1c89 --- /dev/null +++ b/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(); + }); +}); diff --git a/test/unit-tests/url_utils-test.ts b/test/unit-tests/vector/url_utils-test.ts similarity index 94% rename from test/unit-tests/url_utils-test.ts rename to test/unit-tests/vector/url_utils-test.ts index 784dde2d1c..7f1d2e9c0d 100644 --- a/test/unit-tests/url_utils-test.ts +++ b/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. */ -import { parseQsFromFragment, parseQs } from "../../src/vector/url_utils"; +import { parseQsFromFragment, parseQs } from "../../../src/vector/url_utils"; describe("url_utils.ts", function() { // @ts-ignore diff --git a/yarn.lock b/yarn.lock index 24a1e02ae8..95ca323e4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1388,6 +1388,17 @@ slash "^3.0.0" 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": version "28.1.3" 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" 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": version "17.0.12" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.12.tgz#0745ff3e4872b4ace98616d4b7e37ccbd75f9526" @@ -3749,7 +3767,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 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" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 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" 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: version "4.0.8" 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" 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: version "29.0.0" 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" 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: version "29.0.3" 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" 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" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962" integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA== @@ -8650,6 +8689,13 @@ modernizr@^3.12.0: requirejs "^2.3.6" 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: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"