diff --git a/.eslintrc.json b/.eslintrc.json index 5875299217..96e3530419 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,6 +10,10 @@ "plugin:storybook/recommended", "plugin:tailwindcss/recommended" ], + "plugins": ["@vitest"], + "globals": { + "vi": true + }, "ignorePatterns": [ "!src/**/*.{js,jsx,ts,tsx}", "src/old_ui/Icon/svg/*.jsx", diff --git a/package.json b/package.json index eb8876a5e8..9481658098 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "@vitejs/plugin-react": "^4.3.1", "@vitest/coverage-istanbul": "^2.1.1", "@vitest/coverage-v8": "^2.1.1", + "@vitest/eslint-plugin": "^1.1.4", "@vitest/ui": "^2.1.1", "autoprefixer": "^10.4.14", "eslint": "^8.39.0", diff --git a/src/layouts/BaseLayout/InstallationHelpBanner/InstallationHelpBanner.test.jsx b/src/layouts/BaseLayout/InstallationHelpBanner/InstallationHelpBanner.test.jsx index 6a559cbe6d..1f2e466b72 100644 --- a/src/layouts/BaseLayout/InstallationHelpBanner/InstallationHelpBanner.test.jsx +++ b/src/layouts/BaseLayout/InstallationHelpBanner/InstallationHelpBanner.test.jsx @@ -4,7 +4,6 @@ import userEvent from '@testing-library/user-event' import { graphql, HttpResponse } from 'msw2' import { setupServer } from 'msw2/node' import { MemoryRouter, Route, Switch } from 'react-router-dom' -import { vi } from 'vitest' import InstallationHelpBanner from './InstallationHelpBanner' diff --git a/src/layouts/Footer/Footer.test.tsx b/src/layouts/Footer/Footer.test.tsx index 3bfd5ac3fc..705e7e30fa 100644 --- a/src/layouts/Footer/Footer.test.tsx +++ b/src/layouts/Footer/Footer.test.tsx @@ -1,6 +1,5 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter, Route } from 'react-router-dom' -import { vi } from 'vitest' import config from 'config' diff --git a/src/layouts/SidebarLayout/SidebarLayout.spec.jsx b/src/layouts/SidebarLayout/SidebarLayout.test.tsx similarity index 53% rename from src/layouts/SidebarLayout/SidebarLayout.spec.jsx rename to src/layouts/SidebarLayout/SidebarLayout.test.tsx index 7c11282cc0..49dc9c51d8 100644 --- a/src/layouts/SidebarLayout/SidebarLayout.spec.jsx +++ b/src/layouts/SidebarLayout/SidebarLayout.test.tsx @@ -3,74 +3,82 @@ import { MemoryRouter } from 'react-router-dom' import SidebarLayout from './SidebarLayout' -jest.mock('../shared/ErrorBoundary', () => ({ children }) => <>{children}) -jest.mock('layouts/Footer', () => () => 'Footer') +vi.mock('../shared/ErrorBoundary', () => ({ + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})) +vi.mock('layouts/Footer', () => ({ default: () => 'Footer' })) const robinQuote = 'Holy Tintinnabulation!' const batmanQuote = 'Why do we fall? So that we can learn to pick ourselves back up.' describe('SidebarLayout', () => { - function setup(content, overrideClass) { - render( - {robinQuote}} - > - {content} - , - { - wrapper: MemoryRouter, - } - ) - } - describe('it renders with no children', () => { - beforeEach(() => { - setup() - }) - it('renders the sidebar', () => { + render( + {robinQuote}}>, + { wrapper: MemoryRouter } + ) + const sidebar = screen.getByText(robinQuote) expect(sidebar).toBeInTheDocument() }) }) describe('it renders with children', () => { - beforeEach(() => { - setup(

{batmanQuote}

) - }) - it('renders the sidebar', () => { + render( + {robinQuote}}> +

{batmanQuote}

+
, + { wrapper: MemoryRouter } + ) + const sidebar = screen.getByText(robinQuote) expect(sidebar).toBeInTheDocument() }) it('renders the content of the page (children)', () => { + render( + {robinQuote}}> +

{batmanQuote}

+
, + { wrapper: MemoryRouter } + ) + const content = screen.getByText(batmanQuote) expect(content).toBeInTheDocument() }) }) describe('it renders the content with default styles', () => { - beforeEach(() => { - setup(

{batmanQuote}

) - }) - it('renders the sidebar', () => { + render( + {robinQuote}}> +

{batmanQuote}

+
, + { wrapper: MemoryRouter } + ) + const content = screen.getByTestId('sidebar-content') expect(content).toHaveClass('pl-0 lg:pl-8') }) }) describe('it renders the content with custom styles', () => { - beforeEach(() => { - setup(

{batmanQuote}

, 'batcave') - }) - it('renders the sidebar', () => { + render( + {robinQuote}} + > +

{batmanQuote}

+
, + { wrapper: MemoryRouter } + ) + const content = screen.getByTestId('sidebar-content') - expect(content).toHaveClass('batcave') + expect(content).toHaveClass('text-red-500') }) }) }) diff --git a/src/layouts/SidebarLayout/SidebarLayout.jsx b/src/layouts/SidebarLayout/SidebarLayout.tsx similarity index 74% rename from src/layouts/SidebarLayout/SidebarLayout.jsx rename to src/layouts/SidebarLayout/SidebarLayout.tsx index 417e1c189e..f08ec25002 100644 --- a/src/layouts/SidebarLayout/SidebarLayout.jsx +++ b/src/layouts/SidebarLayout/SidebarLayout.tsx @@ -1,10 +1,18 @@ import cs from 'classnames' -import PropType from 'prop-types' import ErrorBoundary from '../shared/ErrorBoundary' import NetworkErrorBoundary from '../shared/NetworkErrorBoundary' -function SidebarLayout({ sidebar, children, className = '' }) { +interface SidebarLayoutProps { + sidebar: React.ReactNode + className?: string +} + +const SidebarLayout: React.FC> = ({ + sidebar, + children, + className = '', +}) => { return (
@@ -22,8 +30,4 @@ function SidebarLayout({ sidebar, children, className = '' }) { ) } -SidebarLayout.propTypes = { - sidebar: PropType.element.isRequired, -} - export default SidebarLayout diff --git a/src/layouts/SidebarLayout/index.js b/src/layouts/SidebarLayout/index.ts similarity index 100% rename from src/layouts/SidebarLayout/index.js rename to src/layouts/SidebarLayout/index.ts diff --git a/src/layouts/ToastNotifications/ToastNotifications.spec.jsx b/src/layouts/ToastNotifications/ToastNotifications.test.jsx similarity index 88% rename from src/layouts/ToastNotifications/ToastNotifications.spec.jsx rename to src/layouts/ToastNotifications/ToastNotifications.test.jsx index 5715c1367b..00271007f6 100644 --- a/src/layouts/ToastNotifications/ToastNotifications.spec.jsx +++ b/src/layouts/ToastNotifications/ToastNotifications.test.jsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react' +import { vi } from 'vitest' import { useNotifications, @@ -22,10 +23,10 @@ const notifications = [ }, ] -jest.mock('services/toastNotification') +vi.mock('services/toastNotification') describe('ToastNotifications', () => { - const removeNotification = jest.fn() + const removeNotification = vi.fn() function setup() { useNotifications.mockReturnValue(notifications) @@ -35,7 +36,7 @@ describe('ToastNotifications', () => { describe('when rendered', () => { beforeEach(() => { - jest.useFakeTimers() + vi.useFakeTimers() removeNotification.mockReset() setup() }) @@ -47,7 +48,7 @@ describe('ToastNotifications', () => { describe('when enough time passes', () => { beforeEach(() => { - jest.runOnlyPendingTimers() + vi.runOnlyPendingTimers() }) it('calls removeNotification with the notification that disappear', () => { diff --git a/src/layouts/ToastNotifications/index.js b/src/layouts/ToastNotifications/index.ts similarity index 100% rename from src/layouts/ToastNotifications/index.js rename to src/layouts/ToastNotifications/index.ts diff --git a/src/layouts/shared/ErrorBoundary/ErrorBoundary.spec.jsx b/src/layouts/shared/ErrorBoundary/ErrorBoundary.test.jsx similarity index 95% rename from src/layouts/shared/ErrorBoundary/ErrorBoundary.spec.jsx rename to src/layouts/shared/ErrorBoundary/ErrorBoundary.test.jsx index 9d2eb4970e..4461ee45b3 100644 --- a/src/layouts/shared/ErrorBoundary/ErrorBoundary.spec.jsx +++ b/src/layouts/shared/ErrorBoundary/ErrorBoundary.test.jsx @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/browser' import { render, screen } from '@testing-library/react' import { MemoryRouter, Route } from 'react-router-dom' +import { vi } from 'vitest' import ErrorBoundary from './ErrorBoundary' @@ -10,15 +11,14 @@ function BadComponent() { } // https://docs.sentry.io/platforms/javascript/guides/react/components/errorboundary/#using-multiple-error-boundaries -const sentryMockScope = jest.fn() +const sentryMockScope = vi.fn() describe('Error Boundary', () => { let mockError - beforeAll(() => jest.disableAutomock()) - afterAll(() => jest.enableAutomock()) + beforeEach(() => { - mockError = jest.fn() - const spy = jest.spyOn(console, 'error') + mockError = vi.fn() + const spy = vi.spyOn(console, 'error') spy.mockImplementation(mockError) }) diff --git a/src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.spec.jsx b/src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.test.jsx similarity index 99% rename from src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.spec.jsx rename to src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.test.jsx index 775d45bca8..8f9f64250d 100644 --- a/src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.spec.jsx +++ b/src/layouts/shared/NetworkErrorBoundary/NetworkErrorBoundary.test.jsx @@ -4,14 +4,15 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Component, useState } from 'react' import { MemoryRouter, useHistory } from 'react-router-dom' +import { vi } from 'vitest' import config from 'config' import NetworkErrorBoundary from './NetworkErrorBoundary' // silence all verbose console.error -jest.spyOn(console, 'error').mockImplementation() -jest.mock('config') +vi.spyOn(console, 'error').mockImplementation(() => undefined) +vi.mock('config') const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, @@ -354,7 +355,7 @@ describe('NetworkErrorBoundary', () => { }) // Mock the global fetch function - global.fetch = jest.fn(() => + global.fetch = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}), diff --git a/src/layouts/shared/NetworkErrorBoundary/index.js b/src/layouts/shared/NetworkErrorBoundary/index.ts similarity index 100% rename from src/layouts/shared/NetworkErrorBoundary/index.js rename to src/layouts/shared/NetworkErrorBoundary/index.ts diff --git a/src/layouts/shared/NetworkErrorBoundary/networkErrorMetrics.spec.ts b/src/layouts/shared/NetworkErrorBoundary/networkErrorMetrics.test.ts similarity index 100% rename from src/layouts/shared/NetworkErrorBoundary/networkErrorMetrics.spec.ts rename to src/layouts/shared/NetworkErrorBoundary/networkErrorMetrics.test.ts diff --git a/src/layouts/shared/SilentNetworkError/SilentNetworkError.spec.jsx b/src/layouts/shared/SilentNetworkError/SilentNetworkError.test.jsx similarity index 95% rename from src/layouts/shared/SilentNetworkError/SilentNetworkError.spec.jsx rename to src/layouts/shared/SilentNetworkError/SilentNetworkError.test.jsx index fba96174a0..bcf9de68bf 100644 --- a/src/layouts/shared/SilentNetworkError/SilentNetworkError.spec.jsx +++ b/src/layouts/shared/SilentNetworkError/SilentNetworkError.test.jsx @@ -1,9 +1,10 @@ import { render, screen } from '@testing-library/react' +import { vi } from 'vitest' import SilentNetworkError from './SilentNetworkError' // silence all verbose console.error -jest.spyOn(console, 'error').mockImplementation() +vi.spyOn(console, 'error').mockImplementation(() => undefined) describe('SilentNetworkError', () => { function setup(ComponentToRender, props = {}) { diff --git a/src/layouts/shared/SilentNetworkError/index.js b/src/layouts/shared/SilentNetworkError/index.ts similarity index 100% rename from src/layouts/shared/SilentNetworkError/index.js rename to src/layouts/shared/SilentNetworkError/index.ts diff --git a/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.spec.jsx b/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.test.tsx similarity index 87% rename from src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.spec.jsx rename to src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.test.tsx index a0c79ee35f..f55bd3cd22 100644 --- a/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.spec.jsx +++ b/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.test.tsx @@ -3,15 +3,13 @@ import { render, screen } from '@testing-library/react' import SilentNetworkError from './SilentNetworkErrorWrapper' describe('SilentNetworkErrorWrapper', () => { - function setup(data) { + function setup() { render(Hi) } - beforeEach(() => { + it('renders children', async () => { setup() - }) - it('renders children', async () => { const Hello = await screen.findByText(/Hi/) expect(Hello).toBeInTheDocument() }) diff --git a/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.jsx b/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.tsx similarity index 77% rename from src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.jsx rename to src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.tsx index cfb2236aa6..89c0f9d94f 100644 --- a/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.jsx +++ b/src/layouts/shared/SilentNetworkErrorWrapper/SilentNetworkErrorWrapper.tsx @@ -3,7 +3,9 @@ import { Suspense } from 'react' import SilentNetworkError from 'layouts/shared/SilentNetworkError' // IMPORTANT! Make sure to lazy load the children component -function SilentNetworkErrorWrapper({ children }) { +const SilentNetworkErrorWrapper: React.FC = ({ + children, +}) => { return ( {children} diff --git a/src/layouts/shared/SilentNetworkErrorWrapper/index.js b/src/layouts/shared/SilentNetworkErrorWrapper/index.ts similarity index 100% rename from src/layouts/shared/SilentNetworkErrorWrapper/index.js rename to src/layouts/shared/SilentNetworkErrorWrapper/index.ts diff --git a/src/old_ui/Message/Message.test.jsx b/src/old_ui/Message/Message.test.jsx index 0750868590..fe86caa7a0 100644 --- a/src/old_ui/Message/Message.test.jsx +++ b/src/old_ui/Message/Message.test.jsx @@ -1,6 +1,5 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { vi } from 'vitest' import Message from '.' diff --git a/src/ui/TotalsNumber/TotalsNumber.spec.tsx b/src/ui/TotalsNumber/TotalsNumber.test.tsx similarity index 50% rename from src/ui/TotalsNumber/TotalsNumber.spec.tsx rename to src/ui/TotalsNumber/TotalsNumber.test.tsx index f918492e9e..19a67cdfb6 100644 --- a/src/ui/TotalsNumber/TotalsNumber.spec.tsx +++ b/src/ui/TotalsNumber/TotalsNumber.test.tsx @@ -1,42 +1,70 @@ -import { render, screen } from '@testing-library/react' +import { cleanup, render, screen } from '@testing-library/react' -import TotalsNumber from '.' +import TotalsNumber from './TotalsNumber' -describe('TotalsNumber', () => { - function setup({ value, variant }: { value?: number; variant: string }) { - render( - - ) - } +afterEach(() => { + cleanup() +}) +describe('TotalsNumber', () => { describe('when rendered', () => { it('renders commit change when there is a valid value', () => { - setup({ value: 23, variant: 'default' }) + render( + + ) + const changeValue = screen.getByTestId('number-value') expect(changeValue).toHaveTextContent('23.00%') expect(changeValue).toHaveClass("before:content-['+']") }) it('renders negative number when change is negative', () => { - setup({ value: -17, variant: 'default' }) + render( + + ) + const changeValue = screen.getByTestId('change-value') expect(changeValue).toHaveTextContent('-17.00%') }) it('renders - when there is an invalid value', () => { - setup({ value: undefined, variant: 'default' }) + render( + + ) + const changeValue = screen.getByTestId('change-value') expect(changeValue).toHaveTextContent('-') }) it('renders 0 when you get 0 change', () => { - setup({ value: 0, variant: 'default' }) + render( + + ) + const changeValue = screen.getByTestId('change-value') expect(changeValue).toHaveTextContent('0') }) diff --git a/src/ui/TruncatedMessage/TruncatedMessage.spec.tsx b/src/ui/TruncatedMessage/TruncatedMessage.test.tsx similarity index 96% rename from src/ui/TruncatedMessage/TruncatedMessage.spec.tsx rename to src/ui/TruncatedMessage/TruncatedMessage.test.tsx index 6aafd865cb..505e04651a 100644 --- a/src/ui/TruncatedMessage/TruncatedMessage.spec.tsx +++ b/src/ui/TruncatedMessage/TruncatedMessage.test.tsx @@ -1,10 +1,14 @@ -import { render, screen } from '@testing-library/react' +import { cleanup, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useTruncation } from './hooks' import TruncatedMessage from './TruncatedMessage' -jest.mock('./hooks') +vi.mock('./hooks') + +afterEach(() => { + cleanup() +}) describe('TruncatedMessage', () => { function setup({ canTruncate = false }) { diff --git a/src/ui/TruncatedMessage/hooks/useTruncation.spec.ts b/src/ui/TruncatedMessage/hooks/useTruncation.test.ts similarity index 84% rename from src/ui/TruncatedMessage/hooks/useTruncation.spec.ts rename to src/ui/TruncatedMessage/hooks/useTruncation.test.ts index 926f452f6a..aa1744df20 100644 --- a/src/ui/TruncatedMessage/hooks/useTruncation.spec.ts +++ b/src/ui/TruncatedMessage/hooks/useTruncation.test.ts @@ -1,5 +1,4 @@ -import { renderHook } from '@testing-library/react' -import React from 'react' +import { cleanup, renderHook } from '@testing-library/react' import { useTruncation } from './useTruncation' @@ -24,9 +23,24 @@ class ResizeObserver { global.window.ResizeObserver = ResizeObserver -describe('useTruncation', () => { - let useRefSpy: any +const mocks = vi.hoisted(() => ({ + useRef: vi.fn(), +})) + +vi.mock('react', async () => { + const original = await vi.importActual('react') + + return { + ...original, + useRef: mocks.useRef, + } +}) +afterEach(() => { + cleanup() +}) + +describe('useTruncation', () => { function setup({ clientHeight = 0, scrollHeight = 0, @@ -45,7 +59,8 @@ describe('useTruncation', () => { scrollWidth, }, } - useRefSpy = jest.spyOn(React, 'useRef').mockReturnValue(refReturn) + + mocks.useRef.mockReturnValue(refReturn) if (enableEntry) { entry = { @@ -62,7 +77,7 @@ describe('useTruncation', () => { } afterEach(() => { - jest.restoreAllMocks() + vi.restoreAllMocks() }) describe('scrolls are larger then clients', () => { @@ -71,7 +86,7 @@ describe('useTruncation', () => { setup({ scrollHeight: 10, scrollWidth: 10 }) const { result } = renderHook(() => useTruncation()) - expect(useRefSpy).toHaveBeenCalled() + expect(mocks.useRef).toHaveBeenCalled() expect(result.current.canTruncate).toBeTruthy() }) }) @@ -81,7 +96,7 @@ describe('useTruncation', () => { setup({ scrollHeight: 10, scrollWidth: 0 }) const { result } = renderHook(() => useTruncation()) - expect(useRefSpy).toHaveBeenCalled() + expect(mocks.useRef).toHaveBeenCalled() expect(result.current.canTruncate).toBeTruthy() }) }) @@ -91,7 +106,7 @@ describe('useTruncation', () => { setup({ scrollHeight: 0, scrollWidth: 10 }) const { result } = renderHook(() => useTruncation()) - expect(useRefSpy).toHaveBeenCalled() + expect(mocks.useRef).toHaveBeenCalled() expect(result.current.canTruncate).toBeTruthy() }) }) @@ -103,7 +118,7 @@ describe('useTruncation', () => { setup({ clientHeight: 10, clientWidth: 10 }) const { result } = renderHook(() => useTruncation()) - expect(useRefSpy).toHaveBeenCalled() + expect(mocks.useRef).toHaveBeenCalled() expect(result.current.canTruncate).toBeFalsy() }) }) @@ -113,7 +128,7 @@ describe('useTruncation', () => { setup({ clientHeight: 10, clientWidth: 0 }) const { result } = renderHook(() => useTruncation()) - expect(useRefSpy).toHaveBeenCalled() + expect(mocks.useRef).toHaveBeenCalled() expect(result.current.canTruncate).toBeFalsy() }) }) @@ -123,7 +138,7 @@ describe('useTruncation', () => { setup({ clientHeight: 0, clientWidth: 10 }) const { result } = renderHook(() => useTruncation()) - expect(useRefSpy).toHaveBeenCalled() + expect(mocks.useRef).toHaveBeenCalled() expect(result.current.canTruncate).toBeFalsy() }) }) diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index 04b5458531..43131cdb46 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -1,6 +1,5 @@ import * as matchers from '@testing-library/jest-dom/matchers' import { cleanup } from '@testing-library/react' -import { vi } from 'vitest' import '@testing-library/jest-dom/vitest' // not sure why this lint is being fired here so I'm disabling it diff --git a/yarn.lock b/yarn.lock index 06aa13a2e3..4eebaee762 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6871,6 +6871,25 @@ __metadata: languageName: node linkType: hard +"@vitest/eslint-plugin@npm:^1.1.4": + version: 1.1.4 + resolution: "@vitest/eslint-plugin@npm:1.1.4" + peerDependencies: + "@typescript-eslint/utils": ">= 8.0" + eslint: ">= 8.57.0" + typescript: ">= 5.0.0" + vitest: "*" + peerDependenciesMeta: + "@typescript-eslint/utils": + optional: true + typescript: + optional: true + vitest: + optional: true + checksum: 10c0/e1de76593acefa063498081978746750fcd4906f9de31a463e7675b98d44b0e600c1b9d54d1873f6c01efcf2230184f414d3a4e9a7933e7105ae60c0ea18591c + languageName: node + linkType: hard + "@vitest/expect@npm:2.1.1": version: 2.1.1 resolution: "@vitest/expect@npm:2.1.1" @@ -11736,6 +11755,7 @@ __metadata: "@vitejs/plugin-react": "npm:^4.3.1" "@vitest/coverage-istanbul": "npm:^2.1.1" "@vitest/coverage-v8": "npm:^2.1.1" + "@vitest/eslint-plugin": "npm:^1.1.4" "@vitest/ui": "npm:^2.1.1" autoprefixer: "npm:^10.4.14" classnames: "npm:^2.3.1"