diff --git a/.github/workflows/linting-and-tests.yml b/.github/workflows/linting-and-tests.yml index 2cea8736..6e51b360 100644 --- a/.github/workflows/linting-and-tests.yml +++ b/.github/workflows/linting-and-tests.yml @@ -69,9 +69,9 @@ jobs: if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true' working-directory: grafana-plugin run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000 - - name: Build frontend (will run linter and tests) + - name: Build, lint and test frontend working-directory: grafana-plugin - run: yarn build + run: yarn lint && yarn test && yarn build test-technical-documentation: name: "Test technical documentation" diff --git a/grafana-plugin/.gitignore b/grafana-plugin/.gitignore index c10b9a5f..a833c0d1 100644 --- a/grafana-plugin/.gitignore +++ b/grafana-plugin/.gitignore @@ -18,3 +18,7 @@ grafana-plugin.yml /playwright-report* /playwright/.cache/ /e2e-tests/storageState.json + +# Jest test report +jest_html_reporters.html +jest-html-reporters* \ No newline at end of file diff --git a/grafana-plugin/jest.config.js b/grafana-plugin/jest.config.js index a191ec34..2c261cb4 100644 --- a/grafana-plugin/jest.config.js +++ b/grafana-plugin/jest.config.js @@ -26,6 +26,18 @@ module.exports = { setupFilesAfterEnv: ['/jest.setup.ts'], + reporters: process.env.HTML_REPORT_ENABLED + ? [ + 'default', + [ + 'jest-html-reporters', + { + openReport: process.env.NODE_ENV !== 'production', + }, + ], + ] + : ['default'], + testTimeout: 10000, testPathIgnorePatterns: ['/node_modules/', '/e2e-tests/'], transform: { diff --git a/grafana-plugin/jest.setup.ts b/grafana-plugin/jest.setup.ts index 1dd2c85f..07f46cf4 100644 --- a/grafana-plugin/jest.setup.ts +++ b/grafana-plugin/jest.setup.ts @@ -8,6 +8,25 @@ import 'plugin/dayjs'; import { TextEncoder, TextDecoder } from 'util'; +jest.mock('@grafana/runtime', () => ({ + __esModule: true, + config: { + featureToggles: { + topNav: false, + }, + bootData: { + user: { + timezone: 'UTC', + }, + }, + }, + getBackendSrv: jest.fn().mockImplementation(() => ({ + get: jest.fn(), + post: jest.fn(), + })), + getLocationSrv: jest.fn(), +})); + Object.assign(global, { TextDecoder, TextEncoder }); // https://stackoverflow.com/a/66055672 diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index bb168755..3bc45f0a 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -12,6 +12,7 @@ "labels:link": "yarn --cwd ../../gops-labels/frontend link && yarn link \"@grafana/labels\" && yarn --cwd ../../gops-labels/frontend watch", "labels:unlink": "yarn --cwd ../../gops-labels/frontend unlink", "test": "jest --verbose", + "test:report": "HTML_REPORT_ENABLED=true jest", "test:silent": "jest --silent", "test:e2e": "yarn playwright test --grep-invert @expensive", "test:e2e-expensive": "yarn playwright test --grep @expensive", @@ -90,6 +91,7 @@ "identity-obj-proxy": "3.0.0", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "jest-html-reporters": "^3.1.7", "knip": "^5.0.3", "lint-staged": "^10.2.11", "lodash-es": "^4.17.21", diff --git a/grafana-plugin/src/components/Collapse/Collapse.test.tsx b/grafana-plugin/src/components/Collapse/Collapse.test.tsx index 193da368..d55af721 100644 --- a/grafana-plugin/src/components/Collapse/Collapse.test.tsx +++ b/grafana-plugin/src/components/Collapse/Collapse.test.tsx @@ -5,7 +5,7 @@ import { render, fireEvent, screen } from '@testing-library/react'; import { Collapse, CollapseProps } from 'components/Collapse/Collapse'; -describe.skip('Collapse', () => { +describe('Collapse', () => { function getProps(isOpen: boolean, onClick: jest.Mock = jest.fn()) { return { label: 'Toggle', diff --git a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.test.tsx b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.test.tsx index 9b3af55a..b9a90be1 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.test.tsx +++ b/grafana-plugin/src/containers/AddResponders/parts/AddRespondersPopup/AddRespondersPopup.test.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import { Provider } from 'mobx-react'; +import { UserHelper } from 'models/user/user.helpers'; + import { AddRespondersPopup } from './AddRespondersPopup'; describe('AddRespondersPopup', () => { @@ -30,11 +32,10 @@ describe('AddRespondersPopup', () => { getSearchResult: jest.fn().mockReturnValue(teams), updateItems: jest.fn(), }, - userStore: { - search: jest.fn().mockReturnValue({ results: [] }), - }, }; + UserHelper.search = jest.fn().mockReturnValue({ results: [] }); + const component = render( ({ isTopNavbar: () => false, })); -jest.mock('@grafana/runtime', () => ({ - __esModule: true, - - config: { - featureToggles: { - topNav: false, - }, - }, - - getBackendSrv: jest.fn().mockImplementation(() => ({ - get: jest.fn(), - post: jest.fn(), - })), - - getLocationSrv: jest.fn(), -})); - jest.mock('utils/authorization/authorization', () => ({ ...jest.requireActual('utils/authorization/authorization'), isUserActionAllowed: jest.fn().mockReturnValue(true), @@ -44,7 +27,7 @@ jest.mock('state/rootStore', () => ({ const mockRootStore = (rest?: any, connected = false, cloud_connected = true) => { rootStore.userStore = { - loadUser: loadUserMock, + fetchItemById: loadUserMock, currentUser: { messaging_backends: { MOBILE_APP: { connected }, @@ -76,13 +59,11 @@ describe('MobileAppConnection', () => { beforeEach(() => { loadUserMock.mockClear(); (rootStore as any).mockClear(); + mockRootStore(); + UserHelper.fetchBackendConfirmationCode = jest.fn().mockResolvedValueOnce('dfd'); }); test('it shows a loading message if it is currently fetching the QR code', async () => { - mockRootStore({ - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dfd'), - }); - const component = render(); expect(component.container).toMatchSnapshot(); @@ -93,10 +74,7 @@ describe('MobileAppConnection', () => { }); test('it shows an error message if there was an error fetching the QR code', async () => { - mockRootStore({ - fetchBackendConfirmationCode: jest.fn().mockRejectedValueOnce('dfd'), - }); - + UserHelper.fetchBackendConfirmationCode = jest.fn().mockRejectedValueOnce('dfd'); const component = render(); await screen.findByText(/.*error fetching your QR code.*/); @@ -109,10 +87,6 @@ describe('MobileAppConnection', () => { }); test("it shows a QR code if the app isn't already connected", async () => { - mockRootStore({ - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dfd'), - }); - const component = render(); expect(component.container).toMatchSnapshot(); @@ -125,7 +99,6 @@ describe('MobileAppConnection', () => { test('if we disconnect the app, it disconnects and fetches a new QR code', async () => { mockRootStore( { - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dfd'), unlinkBackend: jest.fn().mockResolvedValueOnce('asdfadsfafds'), }, true @@ -153,7 +126,6 @@ describe('MobileAppConnection', () => { test('it shows a loading message if it is currently disconnecting', async () => { mockRootStore( { - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dfd'), unlinkBackend: jest.fn().mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))), }, true @@ -184,7 +156,6 @@ describe('MobileAppConnection', () => { test('it shows an error message if there was an error disconnecting the mobile app', async () => { mockRootStore( { - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dfd'), unlinkBackend: jest.fn().mockRejectedValueOnce('asdfadsfafds'), }, true @@ -213,7 +184,6 @@ describe('MobileAppConnection', () => { test('it polls loadUser on first render if not connected', async () => { mockRootStore( { - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dfd'), unlinkBackend: jest.fn().mockRejectedValueOnce('asdfadsfafds'), }, false @@ -232,7 +202,6 @@ describe('MobileAppConnection', () => { test('it polls loadUser after disconnect', async () => { mockRootStore( { - fetchBackendConfirmationCode: jest.fn().mockResolvedValueOnce('dff'), unlinkBackend: jest.fn().mockRejectedValueOnce('asdff'), }, true diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.test.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.test.tsx index e4c5242b..785ce486 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.test.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.test.tsx @@ -30,7 +30,6 @@ enum License { CLOUD = 'some-other-license', } -const SELF_HOSTED_INSTALL_PLUGIN_ERROR_MESSAGE = 'ohhh nooo an error msg from self hosted install plugin'; const CHECK_IF_PLUGIN_IS_CONNECTED_ERROR_MESSAGE = 'ohhh nooo a plugin connection error'; const UPDATE_PLUGIN_STATUS_ERROR_MESSAGE = 'ohhh noooo a sync issue'; const PLUGIN_CONFIGURATION_FORM_DATA_ID = 'plugin-configuration-form'; @@ -197,39 +196,6 @@ describe('PluginConfigPage', () => { expect(component.container).toMatchSnapshot(); }); - test("If onCallApiUrl is not set in the plugin's meta jsonData, and ONCALL_API_URL is passed in process.env, it calls selfHostedInstallPlugin", async () => { - // mocks - const processEnvOnCallApiUrl = 'onCallApiUrlFromProcessEnv'; - process.env.ONCALL_API_URL = processEnvOnCallApiUrl; - - PluginState.selfHostedInstallPlugin = jest.fn(); - mockCheckTokenAndIfPluginIsConnected(); - - // test setup - render(); - - // assertions - expect(PluginState.selfHostedInstallPlugin).toHaveBeenCalledTimes(1); - expect(PluginState.selfHostedInstallPlugin).toHaveBeenCalledWith(processEnvOnCallApiUrl, true); - }); - - test("If onCallApiUrl is not set in the plugin's meta jsonData, and ONCALL_API_URL is passed in process.env, and there is an error calling selfHostedInstallPlugin, it sets an error message", async () => { - // mocks - const processEnvOnCallApiUrl = 'onCallApiUrlFromProcessEnv'; - process.env.ONCALL_API_URL = processEnvOnCallApiUrl; - - PluginState.selfHostedInstallPlugin = jest.fn().mockResolvedValueOnce(SELF_HOSTED_INSTALL_PLUGIN_ERROR_MESSAGE); - - // test setup - const component = render(); - await screen.findByTestId(STATUS_MESSAGE_BLOCK_DATA_ID); - - // assertions - expect(PluginState.selfHostedInstallPlugin).toHaveBeenCalledTimes(1); - expect(PluginState.selfHostedInstallPlugin).toHaveBeenCalledWith(processEnvOnCallApiUrl, true); - expect(component.container).toMatchSnapshot(); - }); - test('If onCallApiUrl is set, and updatePluginStatus returns an error, it sets an error message', async () => { // mocks const processEnvOnCallApiUrl = 'onCallApiUrlFromProcessEnv'; diff --git a/grafana-plugin/src/containers/PluginConfigPage/__snapshots__/PluginConfigPage.test.tsx.snap b/grafana-plugin/src/containers/PluginConfigPage/__snapshots__/PluginConfigPage.test.tsx.snap index e04f2889..4e07514a 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/__snapshots__/PluginConfigPage.test.tsx.snap +++ b/grafana-plugin/src/containers/PluginConfigPage/__snapshots__/PluginConfigPage.test.tsx.snap @@ -1,60 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonData, and ONCALL_API_URL is passed in process.env, and there is an error calling selfHostedInstallPlugin, it sets an error message 1`] = ` -
- - Configure Grafana OnCall - -

- This page will help you configure the OnCall plugin 👋 -

-
-    
-      ohhh nooo an error msg from self hosted install plugin
-    
-  
-
-
- -
-
- -
-
-
-`; - exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonData, or in process.env, updatePluginStatus is not called, and the configuration form is shown 1`] = `
-
+
-
-
+
- - Remove current configuration - - + + Remove current configuration + + +
@@ -282,38 +229,40 @@ exports[`PluginConfigPage OnCallApiUrl is set, and checkTokenAndIfPluginIsConnec Connected to OnCall (v1.2.3, OpenSource) -
+
-
-
+
- - Remove current configuration - - + + Remove current configuration + + +
diff --git a/grafana-plugin/src/models/user/user.test.ts b/grafana-plugin/src/models/user/user.test.ts deleted file mode 100644 index 1d9ef587..00000000 --- a/grafana-plugin/src/models/user/user.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { makeRequest as makeRequestOriginal } from 'network/network'; -import { RootStore } from 'state/rootStore'; - -import { UserStore } from './user'; -import { UserHelper } from './user.helpers'; - -const makeRequest = makeRequestOriginal as jest.Mock>; - -jest.mock('network/network'); - -afterEach(() => { - jest.resetAllMocks(); -}); - -describe('UserStore.fetchBackendConfirmationCode', () => { - const userPk = '5'; - const backend = 'dfkjfdjkfdkjfdaaa'; - const mockedQrCode = 'dfkjfdkjfdkjfdjk'; - - test('it makes the proper API call and returns the response', async () => { - makeRequest.mockResolvedValueOnce(mockedQrCode); - - expect(await UserHelper.fetchBackendConfirmationCode(userPk, backend)).toEqual(mockedQrCode); - - expect(makeRequest).toHaveBeenCalledTimes(1); - expect(makeRequest).toHaveBeenCalledWith(`/users/${userPk}/get_backend_verification_code?backend=${backend}`, { - method: 'GET', - }); - }); -}); - -describe('UserStore.unlinkBackend', () => { - const rootStore = new RootStore(); - const userStore = new UserStore(rootStore); - - const userPk = '5'; - const backend = 'dfkjfdjkfdkjfdaaa'; - - test('it makes the proper API call and returns the response', async () => { - makeRequest.mockResolvedValueOnce('hello'); - - Object.defineProperty(userStore, 'loadCurrentUser', { value: jest.fn() }); - - await userStore.unlinkBackend(userPk, backend); - - expect(makeRequest).toHaveBeenCalledTimes(1); - expect(makeRequest).toHaveBeenCalledWith(`/users/${userPk}/unlink_backend/?backend=${backend}`, { - method: 'POST', - }); - - expect(userStore.loadCurrentUser).toHaveBeenCalledTimes(1); - expect(userStore.loadCurrentUser).toHaveBeenCalledWith(); - }); -}); diff --git a/grafana-plugin/src/network/oncall-api/http-client.test.ts b/grafana-plugin/src/network/oncall-api/http-client.test.ts index a3acc789..83d659f5 100644 --- a/grafana-plugin/src/network/oncall-api/http-client.test.ts +++ b/grafana-plugin/src/network/oncall-api/http-client.test.ts @@ -63,12 +63,10 @@ describe('customFetch', () => { describe('if there is otel', () => { const spanEndMock = jest.fn(); - const setStatusMock = jest.fn(); const setAttributeMock = jest.fn(); const spanStartMock = jest.fn(() => ({ setAttribute: setAttributeMock, end: spanEndMock, - setStatus: setStatusMock, })); const otel = { trace: { @@ -93,11 +91,9 @@ describe('customFetch', () => { expect(spanStartMock).toHaveBeenCalledTimes(1); }); - it(`adds 'X-Idempotency-Key' header`, async () => { + it(`passes request config`, async () => { await customFetch(URL, REQUEST_CONFIG); - expect(fetchMock).toHaveBeenCalledWith(expect.any(String), { - headers: { ...REQUEST_CONFIG.headers, 'X-Idempotency-Key': expect.any(String) }, - }); + expect(fetchMock).toHaveBeenCalledWith(expect.any(String), REQUEST_CONFIG); }); describe('if response is successful', () => { @@ -116,7 +112,6 @@ describe('customFetch', () => { await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(ERROR_MOCK); expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL }); expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK); - expect(setStatusMock).toHaveBeenCalledTimes(1); expect(spanEndMock).toHaveBeenCalledTimes(1); }); }); diff --git a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts index 29c555d7..e80a58dc 100644 --- a/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts +++ b/grafana-plugin/src/state/rootBaseStore/RootBaseStore.test.ts @@ -16,6 +16,10 @@ jest.mock('grafana/app/core/core', () => ({ }, }, })); +jest.mock('network/network', () => ({ + __esModule: true, + makeRequest: () => ({ pk: '1' }), +})); const onCallApiUrl = 'http://oncall-dev-engine:8080'; @@ -166,7 +170,6 @@ describe('rootBaseStore', () => { { is_installed: true, token_ok: false }, ])('signup is allowed, user is an admin, plugin installation is triggered', async (scenario) => { const rootBaseStore = new RootBaseStore(); - const mockedLoadCurrentUser = jest.fn(); contextSrv.user.orgRole = OrgRole.Admin; contextSrv.licensedAccessControlEnabled = jest.fn().mockResolvedValueOnce(false); @@ -181,8 +184,6 @@ describe('rootBaseStore', () => { }); isUserActionAllowed.mockReturnValueOnce(true); PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null); - Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); - // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); @@ -209,7 +210,6 @@ describe('rootBaseStore', () => { }, ])('signup is allowed, licensedAccessControlEnabled, various roles and permissions', async (scenario) => { const rootBaseStore = new RootBaseStore(); - const mockedLoadCurrentUser = jest.fn(); contextSrv.user.orgRole = scenario.role; contextSrv.licensedAccessControlEnabled = jest.fn().mockReturnValue(true); @@ -224,7 +224,6 @@ describe('rootBaseStore', () => { }); isUserActionAllowed.mockReturnValueOnce(true); PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null); - Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); @@ -290,7 +289,6 @@ describe('rootBaseStore', () => { test('when the plugin is installed, a data sync is triggered', async () => { const rootBaseStore = new RootBaseStore(); - const mockedLoadCurrentUser = jest.fn(); PluginState.updatePluginStatus = jest.fn().mockResolvedValueOnce({ is_user_anonymous: false, @@ -300,24 +298,17 @@ describe('rootBaseStore', () => { version: 'asdfasdf', license: 'asdfasdf', }); - Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); - // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); // assertions expect(PluginState.updatePluginStatus).toHaveBeenCalledTimes(1); expect(PluginState.updatePluginStatus).toHaveBeenCalledWith(onCallApiUrl); - - expect(mockedLoadCurrentUser).toHaveBeenCalledTimes(1); - expect(mockedLoadCurrentUser).toHaveBeenCalledWith(); - expect(rootBaseStore.initializationError).toBeNull(); }); test('when the plugin is installed, and the data sync returns an error, it is properly handled', async () => { const rootBaseStore = new RootBaseStore(); - const mockedLoadCurrentUser = jest.fn(); const updatePluginStatusError = 'asdasdfasdfasf'; PluginState.updatePluginStatus = jest.fn().mockResolvedValueOnce({ @@ -329,7 +320,6 @@ describe('rootBaseStore', () => { license: 'asdfasdf', }); PluginState.updatePluginStatus = jest.fn().mockResolvedValueOnce(updatePluginStatusError); - Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser }); // test await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl)); diff --git a/grafana-plugin/src/utils/faro.test.tsx b/grafana-plugin/src/utils/faro.test.tsx index e40e371f..26cd2434 100644 --- a/grafana-plugin/src/utils/faro.test.tsx +++ b/grafana-plugin/src/utils/faro.test.tsx @@ -14,6 +14,8 @@ jest.mock('@grafana/faro-web-sdk', () => ({ pushLog: jest.fn(), }, }), + LogLevel: jest.requireActual('@grafana/faro-web-sdk').LogLevel, + InternalLoggerLevel: jest.requireActual('@grafana/faro-web-sdk').InternalLoggerLevel, getWebInstrumentations: () => [], })); diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index c354ef2e..03299d72 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -5746,6 +5746,11 @@ define-data-property@^1.1.2, define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -8175,6 +8180,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -8464,6 +8474,13 @@ is-windows@^1.0.1, is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -8751,6 +8768,14 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" +jest-html-reporters@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/jest-html-reporters/-/jest-html-reporters-3.1.7.tgz#d8cb6f5d15fd518e601841f90165f37765e7ff34" + integrity sha512-GTmjqK6muQ0S0Mnksf9QkL9X9z2FGIpNSxC52E0PHDzjPQ1XDu2+XTI3B3FS43ZiUzD1f354/5FfwbNIBzT7ew== + dependencies: + fs-extra "^10.0.0" + open "^8.0.3" + jest-leak-detector@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" @@ -10429,6 +10454,15 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^8.0.3: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + openapi-fetch@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.8.1.tgz#a2bda1f72a8311e92cc789d1c8fec7b2d8ca28b6"