diff --git a/.eslintrc.yml b/.eslintrc.yml index ec43d0ed4..53e1a3f02 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -45,6 +45,12 @@ rules: pathGroupsExcludedImportTypes: ['internal'], }, ] + no-restricted-imports: + - error + - patterns: + - group: + - '@/react/test-utils/*' + message: 'These utils are just for test files' settings: 'import/resolver': @@ -113,6 +119,12 @@ overrides: 'no-await-in-loop': 'off' 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }] 'regex/invalid': ['error', [{ 'regex': ') { - return {children}; -} - -function customRender(ui: ReactElement, options?: RenderOptions) { - return render(ui, { wrapper: Provider, ...options }); -} - -// re-export everything -export * from '@testing-library/react'; - -// override render method -export { customRender as render }; - -export function renderWithQueryClient(ui: React.ReactElement) { - const testQueryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); - const { rerender, ...result } = customRender( - {ui} - ); - return { - ...result, - rerender: (rerenderUi: React.ReactElement) => - rerender( - - {rerenderUi} - - ), - }; -} diff --git a/app/react/azure/DashboardView/DashboardView.test.tsx b/app/react/azure/DashboardView/DashboardView.test.tsx index 4f68f1a51..649a8cdef 100644 --- a/app/react/azure/DashboardView/DashboardView.test.tsx +++ b/app/react/azure/DashboardView/DashboardView.test.tsx @@ -1,13 +1,15 @@ import { http, HttpResponse } from 'msw'; +import { render, within } from '@testing-library/react'; -import { renderWithQueryClient, within } from '@/react-tools/test-utils'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; import { server } from '@/setup-tests/server'; import { createMockResourceGroups, createMockSubscriptions, } from '@/react-tools/test-mocks'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { DashboardView } from './DashboardView'; @@ -105,7 +107,6 @@ async function renderComponent( resourceGroupsStatus = 200 ) { const user = new UserViewModel({ Username: 'user' }); - const state = { user }; server.use( http.get('/api/endpoints/1', () => HttpResponse.json({})), @@ -135,12 +136,13 @@ async function renderComponent( } ) ); - const renderResult = renderWithQueryClient( - - - + + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(DashboardView), user) ); + const renderResult = render(); + await expect(renderResult.findByText(/Home/)).resolves.toBeVisible(); return renderResult; diff --git a/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx b/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx index 8b576f7c0..d2fcb9716 100644 --- a/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx +++ b/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx @@ -1,10 +1,12 @@ import userEvent from '@testing-library/user-event'; -import { HttpResponse } from 'msw'; +import { HttpResponse, http } from 'msw'; +import { render } from '@testing-library/react'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; -import { http, server } from '@/setup-tests/server'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { server } from '@/setup-tests/server'; import { CreateContainerInstanceForm } from './CreateContainerInstanceForm'; @@ -19,12 +21,10 @@ test('submit button should be disabled when name or image is missing', async () server.use(http.get('/api/endpoints/5', () => HttpResponse.json({}))); const user = new UserViewModel({ Username: 'user' }); - - const { findByText, getByText, getByLabelText } = renderWithQueryClient( - - - + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(CreateContainerInstanceForm), user) ); + const { findByText, getByText, getByLabelText } = render(); await expect(findByText(/Azure settings/)).resolves.toBeVisible(); diff --git a/app/react/components/Badge/Badge.test.tsx b/app/react/components/Badge/Badge.test.tsx index 12859bade..a327e0b98 100644 --- a/app/react/components/Badge/Badge.test.tsx +++ b/app/react/components/Badge/Badge.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { Badge } from './Badge'; diff --git a/app/react/components/BoxSelector/BoxSelector.test.tsx b/app/react/components/BoxSelector/BoxSelector.test.tsx index c6f4cd223..265ce872c 100644 --- a/app/react/components/BoxSelector/BoxSelector.test.tsx +++ b/app/react/components/BoxSelector/BoxSelector.test.tsx @@ -1,6 +1,5 @@ import { Rocket } from 'lucide-react'; - -import { render, fireEvent } from '@/react-tools/test-utils'; +import { render, fireEvent } from '@testing-library/react'; import { BoxSelector } from './BoxSelector'; import { BoxSelectorOption, Value } from './types'; diff --git a/app/react/components/DashboardItem/DashboardItem.test.tsx b/app/react/components/DashboardItem/DashboardItem.test.tsx index 2bbbb1da2..e32e19b2a 100644 --- a/app/react/components/DashboardItem/DashboardItem.test.tsx +++ b/app/react/components/DashboardItem/DashboardItem.test.tsx @@ -1,6 +1,5 @@ import { User } from 'lucide-react'; - -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { DashboardItem } from './DashboardItem'; diff --git a/app/react/components/DetailsTable/DetailsTable.test.tsx b/app/react/components/DetailsTable/DetailsTable.test.tsx index f9b9fe34d..a00108729 100644 --- a/app/react/components/DetailsTable/DetailsTable.test.tsx +++ b/app/react/components/DetailsTable/DetailsTable.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { DetailsTable } from './index'; diff --git a/app/react/components/EdgeIndicator.test.tsx b/app/react/components/EdgeIndicator.test.tsx index 4ecb68396..7defe27d0 100644 --- a/app/react/components/EdgeIndicator.test.tsx +++ b/app/react/components/EdgeIndicator.test.tsx @@ -1,5 +1,8 @@ +import { render } from '@testing-library/react'; + import { createMockEnvironment } from '@/react-tools/test-mocks'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; + +import { withTestQueryProvider } from '../test-utils/withTestQuery'; import { EdgeIndicator } from './EdgeIndicator'; @@ -31,8 +34,10 @@ async function renderComponent( environment.EdgeCheckinInterval = checkInInterval; environment.QueryDate = queryDate; - const queries = renderWithQueryClient( - + const Wrapped = withTestQueryProvider(EdgeIndicator); + + const queries = render( + ); await expect(queries.findByRole('status')).resolves.toBeVisible(); diff --git a/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx b/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx index 912d63d24..f8851f1cc 100644 --- a/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx +++ b/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx @@ -1,9 +1,10 @@ import { FormikErrors } from 'formik'; import { ComponentProps } from 'react'; import { HttpResponse } from 'msw'; +import { render, fireEvent } from '@testing-library/react'; -import { renderWithQueryClient, fireEvent } from '@/react-tools/test-utils'; import { http, server } from '@/setup-tests/server'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { ImageConfigFieldset } from './ImageConfigFieldset'; import { Values } from './types'; @@ -16,20 +17,20 @@ vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ })); it('should render SimpleForm when useRegistry is true', () => { - const { getByText } = render({ values: { useRegistry: true } }); + const { getByText } = renderComponent({ values: { useRegistry: true } }); expect(getByText('Advanced mode')).toBeInTheDocument(); }); it('should render AdvancedForm when useRegistry is false', () => { - const { getByText } = render({ values: { useRegistry: false } }); + const { getByText } = renderComponent({ values: { useRegistry: false } }); expect(getByText('Simple mode')).toBeInTheDocument(); }); it('should call setFieldValue with useRegistry set to false when "Advanced mode" button is clicked', () => { const setFieldValue = vi.fn(); - const { getByText } = render({ + const { getByText } = renderComponent({ values: { useRegistry: true }, setFieldValue, }); @@ -41,7 +42,7 @@ it('should call setFieldValue with useRegistry set to false when "Advanced mode" it('should call setFieldValue with useRegistry set to true when "Simple mode" button is clicked', () => { const setFieldValue = vi.fn(); - const { getByText } = render({ + const { getByText } = renderComponent({ values: { useRegistry: false }, setFieldValue, }); @@ -51,7 +52,7 @@ it('should call setFieldValue with useRegistry set to true when "Simple mode" bu expect(setFieldValue).toHaveBeenCalledWith('useRegistry', true); }); -function render({ +function renderComponent({ values = { useRegistry: true, registryId: 123, @@ -73,8 +74,10 @@ function render({ http.get('/api/endpoints/:id', () => HttpResponse.json({})) ); - return renderWithQueryClient( - { }); test('should call onSelect when clicked with id', async () => { + const user = userEvent.setup(); const options = [ { children: 'Content 1', id: 'option1', label: 'Option 1' }, { children: 'Content 2', id: 'option2', label: 'Option 2' }, @@ -42,7 +45,7 @@ test('should call onSelect when clicked with id', async () => { const { findByText } = renderComponent(options, options[1].id, onSelect); const heading = await findByText(options[0].label); - await userEvent.click(heading); + await user.click(heading); expect(onSelect).toHaveBeenCalledWith(options[0].id); }); @@ -52,7 +55,9 @@ function renderComponent( selectedId?: string | number, onSelect?: (id: string | number) => void ) { + const Wrapped = withTestRouter(NavTabs); + return render( - + ); } diff --git a/app/react/components/PageHeader/Breadcrumbs/Breadcrumbs.test.tsx b/app/react/components/PageHeader/Breadcrumbs/Breadcrumbs.test.tsx index a9e6608b7..97e3c0c76 100644 --- a/app/react/components/PageHeader/Breadcrumbs/Breadcrumbs.test.tsx +++ b/app/react/components/PageHeader/Breadcrumbs/Breadcrumbs.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { Breadcrumbs } from './Breadcrumbs'; diff --git a/app/react/components/PageHeader/HeaderTitle.test.tsx b/app/react/components/PageHeader/HeaderTitle.test.tsx index 2589d9f8c..490e9608c 100644 --- a/app/react/components/PageHeader/HeaderTitle.test.tsx +++ b/app/react/components/PageHeader/HeaderTitle.test.tsx @@ -1,8 +1,9 @@ -import { QueryClient, QueryClientProvider } from 'react-query'; +import { render } from '@testing-library/react'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; -import { render } from '@/react-tools/test-utils'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { HeaderContainer } from './HeaderContainer'; import { HeaderTitle } from './HeaderTitle'; @@ -14,7 +15,8 @@ test('should not render without a wrapping HeaderContainer', async () => { const title = 'title'; function renderComponent() { - return render(); + const Wrapped = withTestQueryProvider(HeaderTitle); + return render(); } expect(renderComponent).toThrowErrorMatchingSnapshot(); @@ -25,19 +27,22 @@ test('should not render without a wrapping HeaderContainer', async () => { test('should display a HeaderTitle', async () => { const username = 'username'; const user = new UserViewModel({ Username: username }); - const queryClient = new QueryClient(); const title = 'title'; - const { queryByText } = render( - - + + const Wrapped = withTestQueryProvider( + withUserProvider( + withTestRouter(() => ( - - + )), + user + ) ); + const { queryByText } = render(); + const heading = queryByText(title); expect(heading).toBeVisible(); diff --git a/app/react/components/PageHeader/PageHeader.test.tsx b/app/react/components/PageHeader/PageHeader.test.tsx index 75ae5e25f..f6bd1f9d0 100644 --- a/app/react/components/PageHeader/PageHeader.test.tsx +++ b/app/react/components/PageHeader/PageHeader.test.tsx @@ -1,6 +1,9 @@ -import { UserContext } from '@/react/hooks/useUser'; +import { render } from '@testing-library/react'; + import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { PageHeader } from './PageHeader'; @@ -8,13 +11,13 @@ test('should display a PageHeader', async () => { const username = 'username'; const user = new UserViewModel({ Username: username }); - const title = 'title'; - const { queryByText } = renderWithQueryClient( - - - + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(PageHeader), user) ); + const title = 'title'; + const { queryByText } = render(); + const heading = queryByText(title); expect(heading).toBeVisible(); diff --git a/app/react/components/TagSelector/TagSelector.test.tsx b/app/react/components/TagSelector/TagSelector.test.tsx index edbd5e781..59eec5e81 100644 --- a/app/react/components/TagSelector/TagSelector.test.tsx +++ b/app/react/components/TagSelector/TagSelector.test.tsx @@ -1,9 +1,11 @@ import { http, HttpResponse } from 'msw'; import { Mock } from 'vitest'; +import { render } from '@testing-library/react'; import { Tag, TagId } from '@/portainer/tags/types'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; import { server } from '@/setup-tests/server'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { TagSelector } from './TagSelector'; @@ -54,8 +56,10 @@ async function renderComponent( ) { server.use(http.get('/api/tags', () => HttpResponse.json(tags))); - const queries = renderWithQueryClient( - + const Wrapped = withTestQueryProvider(withTestRouter(TagSelector)); + + const queries = render( + ); const tagElement = await queries.findAllByText('tags', { exact: false }); diff --git a/app/react/components/buttons/AddButton.test.tsx b/app/react/components/buttons/AddButton.test.tsx index 1b7413983..0bfa80408 100644 --- a/app/react/components/buttons/AddButton.test.tsx +++ b/app/react/components/buttons/AddButton.test.tsx @@ -1,11 +1,34 @@ -import { render } from '@/react-tools/test-utils'; +import { UIView } from '@uirouter/react'; +import { render } from '@testing-library/react'; + +import { withTestRouter } from '@/react/test-utils/withRouter'; import { AddButton } from './AddButton'; function renderDefault({ label = 'default label', }: Partial<{ label: string }> = {}) { - return render({label}); + const Wrapped = withTestRouter(AddButton, { + stateConfig: [ + { + name: 'root', + url: '/', + + component: () => ( + <> +
Root
+ + + ), + }, + { + name: 'root.new', + url: 'new', + }, + ], + route: 'root', + }); + return render({label}); } test('should display a AddButton component', async () => { diff --git a/app/react/components/buttons/Button.test.tsx b/app/react/components/buttons/Button.test.tsx index fe6e06361..0c8847590 100644 --- a/app/react/components/buttons/Button.test.tsx +++ b/app/react/components/buttons/Button.test.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render } from '@testing-library/react'; import { PropsWithChildren } from 'react'; +import { fireEvent, render } from '@testing-library/react'; import { Button, Props } from './Button'; diff --git a/app/react/components/buttons/ButtonGroup.test.tsx b/app/react/components/buttons/ButtonGroup.test.tsx index 597fdb441..bcd638445 100644 --- a/app/react/components/buttons/ButtonGroup.test.tsx +++ b/app/react/components/buttons/ButtonGroup.test.tsx @@ -1,5 +1,5 @@ -import { render } from '@testing-library/react'; import { PropsWithChildren } from 'react'; +import { render } from '@testing-library/react'; import { ButtonGroup, Props } from './ButtonGroup'; diff --git a/app/react/components/buttons/LoadingButton.test.tsx b/app/react/components/buttons/LoadingButton.test.tsx index 5b5b7129c..919ec345b 100644 --- a/app/react/components/buttons/LoadingButton.test.tsx +++ b/app/react/components/buttons/LoadingButton.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { LoadingButton } from './LoadingButton'; diff --git a/app/react/components/form-components/FileUpload/FileUploadField.test.tsx b/app/react/components/form-components/FileUpload/FileUploadField.test.tsx index f49c11660..617ac7c5e 100644 --- a/app/react/components/form-components/FileUpload/FileUploadField.test.tsx +++ b/app/react/components/form-components/FileUpload/FileUploadField.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@/react-tools/test-utils'; +import { fireEvent, render } from '@testing-library/react'; import { FileUploadField } from './FileUploadField'; diff --git a/app/react/components/form-components/FileUpload/FileUploadForm.test.tsx b/app/react/components/form-components/FileUpload/FileUploadForm.test.tsx index e49ba4948..c275cdeab 100644 --- a/app/react/components/form-components/FileUpload/FileUploadForm.test.tsx +++ b/app/react/components/form-components/FileUpload/FileUploadForm.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { FileUploadForm } from './FileUploadForm'; diff --git a/app/react/components/form-components/Slider/Slider.test.tsx b/app/react/components/form-components/Slider/Slider.test.tsx index cd5d375a6..e08401503 100644 --- a/app/react/components/form-components/Slider/Slider.test.tsx +++ b/app/react/components/form-components/Slider/Slider.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { Slider, Props } from './Slider'; diff --git a/app/react/components/form-components/SwitchField/Switch.test.tsx b/app/react/components/form-components/SwitchField/Switch.test.tsx index 2bef9376d..1be505ed6 100644 --- a/app/react/components/form-components/SwitchField/Switch.test.tsx +++ b/app/react/components/form-components/SwitchField/Switch.test.tsx @@ -1,5 +1,5 @@ -import { render } from '@testing-library/react'; import { PropsWithChildren } from 'react'; +import { render } from '@testing-library/react'; import { Switch, Props } from './Switch'; diff --git a/app/react/components/form-components/SwitchField/SwitchField.test.tsx b/app/react/components/form-components/SwitchField/SwitchField.test.tsx index c26e22cb7..74f06e888 100644 --- a/app/react/components/form-components/SwitchField/SwitchField.test.tsx +++ b/app/react/components/form-components/SwitchField/SwitchField.test.tsx @@ -1,4 +1,4 @@ -import { render, fireEvent } from '@/react-tools/test-utils'; +import { render, fireEvent } from '@testing-library/react'; import { SwitchField, Props } from './SwitchField'; diff --git a/app/react/components/ui-router.test.tsx b/app/react/components/ui-router.test.tsx new file mode 100644 index 000000000..78e9cc388 --- /dev/null +++ b/app/react/components/ui-router.test.tsx @@ -0,0 +1,39 @@ +import { UISref, UIView } from '@uirouter/react'; +import { render, screen } from '@testing-library/react'; + +import { withTestRouter } from '@/react/test-utils/withRouter'; + +function RelativePathLink() { + return ( + + Link + + ); +} + +test.todo('should render a link with relative path', () => { + const WrappedComponent = withTestRouter(RelativePathLink, { + stateConfig: [ + { + name: 'parent', + url: '/', + + component: () => ( + <> +
parent
+ + + ), + }, + { + name: 'parent.custom', + url: 'custom', + }, + ], + route: 'parent', + }); + + render(); + + expect(screen.getByText('Link')).toBeInTheDocument(); +}); diff --git a/app/react/docker/networks/ItemView/NetworkContainersTable.test.tsx b/app/react/docker/networks/ItemView/NetworkContainersTable.test.tsx index ae21c3b31..d45ebd5aa 100644 --- a/app/react/docker/networks/ItemView/NetworkContainersTable.test.tsx +++ b/app/react/docker/networks/ItemView/NetworkContainersTable.test.tsx @@ -1,9 +1,11 @@ -import { HttpResponse } from 'msw'; +import { render } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; -import { http, server } from '@/setup-tests/server'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { server } from '@/setup-tests/server'; import { NetworkContainer } from '../types'; @@ -32,15 +34,18 @@ test('Network container values should be visible and the link should be valid', server.use(http.get('/api/endpoints/1', () => HttpResponse.json({}))); const user = new UserViewModel({ Username: 'test', Role: 1 }); - const { findByText } = renderWithQueryClient( - - - + + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(NetworkContainersTable), user) + ); + + const { findByText } = render( + ); await expect(findByText('Containers in network')).resolves.toBeVisible(); diff --git a/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx b/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx index 6ce2b231d..a2d778a65 100644 --- a/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx +++ b/app/react/docker/networks/ItemView/NetworkDetailsTable.test.tsx @@ -1,9 +1,10 @@ import { HttpResponse, http } from 'msw'; +import { render } from '@testing-library/react'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; import { server } from '@/setup-tests/server'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { DockerNetwork } from '../types'; @@ -26,7 +27,9 @@ test('Network details values should be visible', async () => { await expect(findByText(network.Driver)).resolves.toBeVisible(); await expect(findByText(network.Scope)).resolves.toBeVisible(); await expect( - findByText(network.IPAM?.Config[0].Gateway || 'not found', { exact: false }) + findByText(network.IPAM?.Config[0].Gateway || 'not found', { + exact: false, + }) ).resolves.toBeVisible(); await expect( findByText(network.IPAM?.Config[0].Subnet || 'not found', { exact: false }) @@ -55,13 +58,12 @@ async function renderComponent(isAdmin: boolean, network: DockerNetwork) { const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 }); - const queries = renderWithQueryClient( - - {}} - /> - + const Wrapped = withTestQueryProvider( + withUserProvider(NetworkDetailsTable, user) + ); + + const queries = render( + {}} /> ); await expect(queries.findByText('Network details')).resolves.toBeVisible(); diff --git a/app/react/docker/networks/ItemView/NetworkOptionsTable.test.tsx b/app/react/docker/networks/ItemView/NetworkOptionsTable.test.tsx index 59605c982..d68445ab3 100644 --- a/app/react/docker/networks/ItemView/NetworkOptionsTable.test.tsx +++ b/app/react/docker/networks/ItemView/NetworkOptionsTable.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { NetworkOptions } from '../types'; diff --git a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/AppTemplateFieldset.test.tsx b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/AppTemplateFieldset.test.tsx index 228abca60..5196d9888 100644 --- a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/AppTemplateFieldset.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/AppTemplateFieldset.test.tsx @@ -1,4 +1,5 @@ -import { render, screen } from '@/react-tools/test-utils'; +import { render, screen } from '@testing-library/react'; + import { EnvVarType, TemplateViewModel, diff --git a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/EnvVarsFieldset.test.tsx b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/EnvVarsFieldset.test.tsx index d90af69eb..2dceaa901 100644 --- a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/EnvVarsFieldset.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/EnvVarsFieldset.test.tsx @@ -1,7 +1,6 @@ import { vi } from 'vitest'; import userEvent from '@testing-library/user-event'; - -import { render, screen } from '@/react-tools/test-utils'; +import { render, screen } from '@testing-library/react'; import { EnvVarsFieldset, diff --git a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateNote.test.tsx b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateNote.test.tsx index 32f3d2ac1..bc4ae926c 100644 --- a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateNote.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateNote.test.tsx @@ -1,6 +1,5 @@ import { vi } from 'vitest'; - -import { render, screen } from '@/react-tools/test-utils'; +import { render, screen } from '@testing-library/react'; import { TemplateNote } from './TemplateNote'; diff --git a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateSelector.test.tsx b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateSelector.test.tsx index 2f7e939df..5aa3816b9 100644 --- a/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateSelector.test.tsx +++ b/app/react/edge/edge-stacks/CreateView/TemplateFieldset/TemplateSelector.test.tsx @@ -1,17 +1,18 @@ import { vi } from 'vitest'; import { HttpResponse, http } from 'msw'; +import { render, screen } from '@testing-library/react'; -import { renderWithQueryClient, screen } from '@/react-tools/test-utils'; import { AppTemplate } from '@/react/portainer/templates/app-templates/types'; import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types'; import { server } from '@/setup-tests/server'; import selectEvent from '@/react/test-utils/react-select'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { SelectedTemplateValue } from './types'; import { TemplateSelector } from './TemplateSelector'; test('renders TemplateSelector component', async () => { - render(); + renderComponent(); const templateSelectorElement = screen.getByLabelText('Template'); expect(templateSelectorElement).toBeInTheDocument(); @@ -30,7 +31,7 @@ test.skip('selects an edge app template', async () => { categories: ['edge'], }; - const { select } = render({ + const { select } = renderComponent({ onChange, appTemplates: [ { @@ -59,7 +60,7 @@ test.skip('selects an edge custom template', async () => { Id: 2, }; - const { select } = render({ + const { select } = renderComponent({ onChange, customTemplates: [ { @@ -75,7 +76,7 @@ test.skip('selects an edge custom template', async () => { }); test('renders with error', async () => { - render({ + renderComponent({ error: 'Invalid template', }); @@ -87,7 +88,7 @@ test('renders with error', async () => { }); test.skip('renders TemplateSelector component with no custom templates available', async () => { - render({ + renderComponent({ customTemplates: [], }); @@ -102,7 +103,7 @@ test.skip('renders TemplateSelector component with no custom templates available expect(noCustomTemplatesElement).toBeInTheDocument(); }); -function render({ +function renderComponent({ onChange = vi.fn(), appTemplates = [], customTemplates = [], @@ -123,8 +124,10 @@ function render({ ) ); - renderWithQueryClient( - { const timestamp = 1500; - server.use( - http.get('/api/backup/s3/status', () => - HttpResponse.json({ Failed: true, TimestampUTC: timestamp }) - ) - ); - const { findByText } = renderWithQueryClient(); + const { findByText } = renderComponent({ failed: true, timestamp }); await expect( findByText( @@ -27,14 +24,27 @@ test('when backup failed, should show message', async () => { }); test("when user is using less nodes then allowed he shouldn't see message", async () => { - server.use( - http.get('/api/backup/s3/status', () => - HttpResponse.json({ Failed: false }) - ) - ); - const { findByText } = renderWithQueryClient(); + const { findByText } = renderComponent({ failed: false }); await expect( findByText('The latest automated backup has failed at', { exact: false }) ).rejects.toBeTruthy(); }); + +function renderComponent({ + failed, + timestamp, +}: { + failed: boolean; + timestamp?: number; +}) { + server.use( + http.get('/api/backup/s3/status', () => + HttpResponse.json({ Failed: failed, TimestampUTC: timestamp }) + ) + ); + + const Wrapped = withTestQueryProvider(withTestRouter(BackupFailedPanel)); + + return render(); +} diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx index 1cdda9341..31b86e7bb 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.test.tsx @@ -1,16 +1,18 @@ import { http, HttpResponse } from 'msw'; +import { render } from '@testing-library/react'; import { EnvironmentGroup, EnvironmentGroupId, } from '@/react/portainer/environments/environment-groups/types'; import { Environment } from '@/react/portainer/environments/types'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; import { Tag } from '@/portainer/tags/types'; import { createMockEnvironment } from '@/react-tools/test-mocks'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; import { server } from '@/setup-tests/server'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { EnvironmentItem } from './EnvironmentItem'; @@ -43,15 +45,17 @@ function renderComponent( server.use(http.get('/api/tags', () => HttpResponse.json(tags))); - return renderWithQueryClient( - - {}} - onClickDisconnect={() => {}} - environment={env} - groupName={group.Name} - /> - + const Wrapped = withTestQueryProvider( + withTestRouter(withUserProvider(EnvironmentItem, user)) + ); + + return render( + {}} + onClickDisconnect={() => {}} + environment={env} + groupName={group.Name} + /> ); } diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.test.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.test.tsx index ba561f0e2..a6ca96b5a 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.test.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.test.tsx @@ -1,10 +1,12 @@ import { http, HttpResponse } from 'msw'; +import { render } from '@testing-library/react'; import { Environment } from '@/react/portainer/environments/types'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; import { server } from '@/setup-tests/server'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { EnvironmentList } from './EnvironmentList'; @@ -49,10 +51,12 @@ async function renderComponent( ) ); - const queries = renderWithQueryClient( - - - + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(EnvironmentList), user) + ); + + const queries = render( + ); await expect(queries.findByText('Environments')).resolves.toBeVisible(); diff --git a/app/react/portainer/HomeView/LicenseNodePanel.test.tsx b/app/react/portainer/HomeView/LicenseNodePanel.test.tsx index bc11cbdc5..bd862cc37 100644 --- a/app/react/portainer/HomeView/LicenseNodePanel.test.tsx +++ b/app/react/portainer/HomeView/LicenseNodePanel.test.tsx @@ -1,7 +1,8 @@ import { http, HttpResponse } from 'msw'; +import { render } from '@testing-library/react'; import { server } from '@/setup-tests/server'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { LicenseType } from '../licenses/types'; @@ -17,7 +18,7 @@ test('when user is using more nodes then allowed he should see message', async ( http.get('/api/system/nodes', () => HttpResponse.json({ nodes: used })) ); - const { findByText } = renderWithQueryClient(); + const { findByText } = renderComponent(); await expect( findByText( @@ -36,7 +37,7 @@ test("when user is using less nodes then allowed he shouldn't see message", asyn http.get('/api/system/nodes', () => HttpResponse.json({ nodes: used })) ); - const { findByText } = renderWithQueryClient(); + const { findByText } = renderComponent(); await expect( findByText( @@ -44,3 +45,9 @@ test("when user is using less nodes then allowed he shouldn't see message", asyn ) ).rejects.toBeTruthy(); }); + +function renderComponent() { + const Wrapped = withTestQueryProvider(LicenseNodePanel); + + return render(); +} diff --git a/app/react/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx b/app/react/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx index f756572bd..0ed1a7de3 100644 --- a/app/react/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx +++ b/app/react/portainer/access-control/AccessControlForm/AccessControlForm.stories.tsx @@ -1,9 +1,10 @@ import { Meta, Story } from '@storybook/react'; -import { useMemo, useState } from 'react'; -import { QueryClient, QueryClientProvider } from 'react-query'; +import { useState } from 'react'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; +import { Role, User } from '@/portainer/users/types'; +import { isPureAdmin } from '@/portainer/users/user.helpers'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { parseAccessControlFormData } from '../utils'; @@ -16,41 +17,25 @@ const meta: Meta = { export default meta; -enum Role { - Admin = 1, - User, -} - -const testQueryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, -}); - interface Args { userRole: Role; } function Template({ userRole }: Args) { - const isAdmin = userRole === Role.Admin; - const defaults = parseAccessControlFormData(isAdmin, 0); + const defaults = parseAccessControlFormData( + isPureAdmin({ Role: userRole } as User), + 0 + ); const [value, setValue] = useState(defaults); - const userProviderState = useMemo( - () => ({ user: new UserViewModel({ Role: userRole }) }), - [userRole] + const Wrapped = withUserProvider( + AccessControlForm, + new UserViewModel({ Role: userRole }) ); return ( - - - - - + ); } @@ -61,5 +46,5 @@ AdminAccessControl.args = { export const NonAdminAccessControl: Story = Template.bind({}); NonAdminAccessControl.args = { - userRole: Role.User, + userRole: Role.Standard, }; diff --git a/app/react/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx b/app/react/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx index b7c5e20ea..2503ce903 100644 --- a/app/react/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx +++ b/app/react/portainer/access-control/AccessControlForm/AccessControlForm.test.tsx @@ -1,12 +1,14 @@ import { http, HttpResponse } from 'msw'; +import { render, within } from '@testing-library/react'; import { server } from '@/setup-tests/server'; -import { UserContext } from '@/react/hooks/useUser'; import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient, within } from '@/react-tools/test-utils'; import { Team, TeamId } from '@/react/portainer/users/teams/types'; import { createMockTeams } from '@/react-tools/test-mocks'; import { UserId } from '@/portainer/users/types'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { withTestRouter } from '@/react/test-utils/withRouter'; import { ResourceControlOwnership, AccessControlFormData } from '../types'; import { ResourceControlViewModel } from '../models/ResourceControlViewModel'; @@ -304,7 +306,6 @@ async function renderComponent( { isAdmin = false, hideTitle = false, teams, users }: AdditionalProps = {} ) { const user = new UserViewModel({ Username: 'user', Role: isAdmin ? 1 : 2 }); - const state = { user }; if (teams) { server.use(http.get('/api/teams', () => HttpResponse.json(teams))); @@ -314,16 +315,18 @@ async function renderComponent( server.use(http.get('/api/users', () => HttpResponse.json(users))); } - const renderResult = renderWithQueryClient( - - - + const Wrapped = withTestRouter( + withTestQueryProvider(withUserProvider(AccessControlForm, user)) + ); + + const renderResult = render( + ); await expect( diff --git a/app/react/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx b/app/react/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx index 81f723d87..254844deb 100644 --- a/app/react/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx +++ b/app/react/portainer/access-control/AccessControlPanel/AccessControlPaneDetails.test.tsx @@ -1,11 +1,13 @@ import _ from 'lodash'; import { http, HttpResponse } from 'msw'; +import { render } from '@testing-library/react'; import { createMockTeams, createMockUsers } from '@/react-tools/test-mocks'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; import { server } from '@/setup-tests/server'; import { Role } from '@/portainer/users/types'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { ResourceControlOwnership, @@ -145,9 +147,11 @@ async function renderComponent( resourceType: ResourceControlType = ResourceControlType.Container, resourceControl?: ResourceControlViewModel ) { - const WithUser = withUserProvider(AccessControlPanelDetails); - const queries = renderWithQueryClient( - + const Wrapped = withTestQueryProvider( + withTestRouter(withUserProvider(AccessControlPanelDetails)) + ); + const queries = render( + ); await expect(queries.findByText('Ownership')).resolves.toBeVisible(); diff --git a/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx b/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx index fe20994fb..750e759b2 100644 --- a/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx +++ b/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx @@ -1,8 +1,10 @@ import userEvent from '@testing-library/user-event'; +import { render, waitFor } from '@testing-library/react'; -import { renderWithQueryClient, waitFor } from '@/react-tools/test-utils'; import { UserViewModel } from '@/portainer/models/user'; -import { UserContext } from '@/react/hooks/useUser'; +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { CreateUserAccessToken } from './CreateUserAccessToken'; @@ -33,9 +35,9 @@ test('the button is disabled when all fields are blank and enabled when all fiel function renderComponent() { const user = new UserViewModel({ Username: 'user' }); - return renderWithQueryClient( - - - + const Wrapped = withTestQueryProvider( + withUserProvider(withTestRouter(CreateUserAccessToken), user) ); + + return render(); } diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.test.tsx b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.test.tsx index 084430e2c..1d8344025 100644 --- a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.test.tsx +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/CustomTemplatesVariablesField.test.tsx @@ -1,7 +1,6 @@ import { vi } from 'vitest'; import userEvent from '@testing-library/user-event'; - -import { render, screen } from '@/react-tools/test-utils'; +import { render, screen } from '@testing-library/react'; import { CustomTemplatesVariablesField, diff --git a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/VariableFieldItem.test.tsx b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/VariableFieldItem.test.tsx index 9b37bf6c2..34e6e3533 100644 --- a/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/VariableFieldItem.test.tsx +++ b/app/react/portainer/custom-templates/components/CustomTemplatesVariablesField/VariableFieldItem.test.tsx @@ -1,7 +1,6 @@ import { vi } from 'vitest'; import userEvent from '@testing-library/user-event'; - -import { render, screen } from '@/react-tools/test-utils'; +import { render, screen } from '@testing-library/react'; import { VariableFieldItem } from './VariableFieldItem'; diff --git a/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx b/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx index b554bc702..e5221b8fa 100644 --- a/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx +++ b/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx @@ -1,7 +1,6 @@ import userEvent from '@testing-library/user-event'; import { PropsWithChildren } from 'react'; - -import { render } from '@/react-tools/test-utils'; +import { render } from '@testing-library/react'; import { AppTemplatesListItem } from './AppTemplatesListItem'; import { TemplateViewModel } from './view-model'; diff --git a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamAssociationSelector.test.tsx b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamAssociationSelector.test.tsx index ea84853a2..fe5dc5c34 100644 --- a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamAssociationSelector.test.tsx +++ b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamAssociationSelector.test.tsx @@ -1,6 +1,8 @@ -import { UserContext } from '@/react/hooks/useUser'; +import { render } from '@testing-library/react'; + import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { TeamAssociationSelector } from './TeamAssociationSelector'; @@ -13,9 +15,9 @@ test('renders correctly', () => { function renderComponent() { const user = new UserViewModel({ Username: 'user' }); - return renderWithQueryClient( - - - + const Wrapped = withTestQueryProvider( + withUserProvider(TeamAssociationSelector, user) ); + + return render(); } diff --git a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.test.tsx b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.test.tsx index 20cdbac85..68673474c 100644 --- a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.test.tsx +++ b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.test.tsx @@ -1,6 +1,8 @@ -import { UserContext } from '@/react/hooks/useUser'; +import { render } from '@testing-library/react'; + import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { TeamMembersList } from './TeamMembersList'; @@ -13,11 +15,11 @@ test('renders correctly', () => { function renderComponent() { const user = new UserViewModel({ Username: 'user' }); - return renderWithQueryClient( - - - + const Wrapped = withTestQueryProvider( + withUserProvider(TeamMembersList, user) ); + + return render(); } test.todo('when users list is empty, add all users button is disabled'); diff --git a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.test.tsx b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.test.tsx index 00cfc1aee..0e92ae316 100644 --- a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.test.tsx +++ b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.test.tsx @@ -1,6 +1,8 @@ -import { UserContext } from '@/react/hooks/useUser'; +import { render } from '@testing-library/react'; + import { UserViewModel } from '@/portainer/models/user'; -import { renderWithQueryClient } from '@/react-tools/test-utils'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { UsersList } from './UsersList'; @@ -12,11 +14,10 @@ test('renders correctly', () => { function renderComponent() { const user = new UserViewModel({ Username: 'user' }); - return renderWithQueryClient( - - - - ); + + const Wrapped = withTestQueryProvider(withUserProvider(UsersList, user)); + + return render(); } test.todo('when users list is empty, add all users button is disabled'); diff --git a/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.test.tsx b/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.test.tsx index 4ff95df20..04716ba8d 100644 --- a/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.test.tsx +++ b/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.test.tsx @@ -1,12 +1,15 @@ import userEvent from '@testing-library/user-event'; +import { render, waitFor } from '@testing-library/react'; -import { renderWithQueryClient, waitFor } from '@/react-tools/test-utils'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { CreateTeamForm } from './CreateTeamForm'; test('filling the name should make the submit button clickable and emptying it should make it disabled', async () => { - const { findByLabelText, findByText } = renderWithQueryClient( - + const Wrapped = withTestQueryProvider(CreateTeamForm); + + const { findByLabelText, findByText } = render( + ); const button = await findByText('Create team'); diff --git a/app/react/sidebar/AzureSidebar/AzureSidebar.test.tsx b/app/react/sidebar/AzureSidebar/AzureSidebar.test.tsx index d3df19b0e..f677c73c3 100644 --- a/app/react/sidebar/AzureSidebar/AzureSidebar.test.tsx +++ b/app/react/sidebar/AzureSidebar/AzureSidebar.test.tsx @@ -1,6 +1,8 @@ -import { UserContext } from '@/react/hooks/useUser'; +import { render, within } from '@testing-library/react'; + import { UserViewModel } from '@/portainer/models/user'; -import { render, within } from '@/react-tools/test-utils'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { withTestRouter } from '@/react/test-utils/withRouter'; import { TestSidebarProvider } from '../useSidebarState'; @@ -30,11 +32,11 @@ test('dashboard items should render correctly', () => { function renderComponent() { const user = new UserViewModel({ Username: 'user' }); + const Wrapped = withUserProvider(withTestRouter(AzureSidebar), user); + return render( - - - - - + + + ); } diff --git a/app/react/test-utils/withRouter.tsx b/app/react/test-utils/withRouter.tsx new file mode 100644 index 000000000..e536759fe --- /dev/null +++ b/app/react/test-utils/withRouter.tsx @@ -0,0 +1,49 @@ +import { ComponentType } from 'react'; +import { + ReactStateDeclaration, + UIRouter, + UIRouterReact, + UIView, + hashLocationPlugin, + servicesPlugin, +} from '@uirouter/react'; + +/** + * A helper function to wrap a component with a UIRouter Provider. + * + * should only be used in tests + */ +export function withTestRouter( + WrappedComponent: ComponentType, + { + route = '/', + stateConfig = [], + }: { route?: string; stateConfig?: Array } = {} +): ComponentType { + const router = new UIRouterReact(); + + // router.trace.enable(Category.TRANSITION); + router.plugin(servicesPlugin); + router.plugin(hashLocationPlugin); + + // Set up your custom state configuration + stateConfig.forEach((state) => router.stateRegistry.register(state)); + router.urlService.rules.initial({ state: route }); + + // Try to create a nice displayName for React Dev Tools. + const displayName = + WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + function WrapperComponent(props: T & JSX.IntrinsicAttributes) { + return ( + + + + + ); + } + + WrapperComponent.displayName = `withTestRouter(${displayName})`; + + return WrapperComponent; +} diff --git a/app/react/test-utils/withTestQuery.tsx b/app/react/test-utils/withTestQuery.tsx new file mode 100644 index 000000000..6d9f8758c --- /dev/null +++ b/app/react/test-utils/withTestQuery.tsx @@ -0,0 +1,14 @@ +import { ComponentType } from 'react'; +import { QueryClient } from 'react-query'; + +import { withReactQuery } from '@/react-tools/withReactQuery'; + +export function withTestQueryProvider( + WrappedComponent: ComponentType +) { + const testQueryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + return withReactQuery(WrappedComponent, testQueryClient); +} diff --git a/app/setup-tests/server-handlers.ts b/app/setup-tests/server-handlers.ts index e7af8957f..f30f1ae23 100644 --- a/app/setup-tests/server-handlers.ts +++ b/app/setup-tests/server-handlers.ts @@ -11,6 +11,7 @@ import { StatusResponse } from '@/react/portainer/system/useSystemStatus'; import { createMockTeams } from '@/react-tools/test-mocks'; import { PublicSettingsResponse } from '@/react/portainer/settings/types'; import { UserId } from '@/portainer/users/types'; +import { VersionResponse } from '@/react/portainer/system/useSystemVersion'; import { azureHandlers } from './setup-handlers/azure'; import { dockerHandlers } from './setup-handlers/docker'; @@ -85,6 +86,9 @@ export const handlers = [ http.get>('/api/status', () => HttpResponse.json({}) ), + http.get>('/api/system/version', () => + HttpResponse.json({ ServerVersion: 'v2.10.0' }) + ), http.get('/api/teams/:id/memberships', () => HttpResponse.json([])), http.get('/api/endpoints/agent_versions', () => HttpResponse.json([])), ]; diff --git a/app/setup-tests/setup.ts b/app/setup-tests/setup.ts new file mode 100644 index 000000000..e8560114b --- /dev/null +++ b/app/setup-tests/setup.ts @@ -0,0 +1 @@ +import 'vitest-dom/extend-expect'; diff --git a/vitest.config.mts b/vitest.config.mts index 3f97eebe1..89caa757a 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -7,7 +7,7 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', - setupFiles: ['./app/setup-tests/setup-msw.ts', './app/setup-tests/stub-modules.ts'], + setupFiles: ['./app/setup-tests/setup-msw.ts', './app/setup-tests/stub-modules.ts', './app/setup-tests/setup.ts'], coverage: { reporter: ['text', 'html'], exclude: ['node_modules/', 'app/setup-tests/global-setup.js'],