From 4bf18b1d651ed7ddb1819bb77e1edcad1f72a0d5 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 14 Feb 2024 17:25:37 +0200 Subject: [PATCH] feat(ui): write tests [EE-6685] (#11081) --- .../CreateContainerInstanceForm.test.tsx | 6 +- app/react/components/Badge/Badge.test.tsx | 26 +++++ .../ImageConfigFieldset.test.tsx | 90 ++++++++++++++++ app/react/components/NavTabs/NavTabs.test.tsx | 2 +- .../CreateUserAccessToken.test.tsx | 6 +- .../AppTemplatesListItem.test.tsx | 102 ++++++++++++++++++ .../templates/components/TemplateItem.tsx | 1 + .../CreateTeamForm/CreateTeamForm.test.tsx | 2 +- package.json | 2 +- yarn.lock | 10 +- 10 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 app/react/components/Badge/Badge.test.tsx create mode 100644 app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx create mode 100644 app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx diff --git a/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx b/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx index 0f1bd8ffc..15e325f4a 100644 --- a/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx +++ b/app/react/azure/container-instances/CreateView/CreateContainerInstanceForm.test.tsx @@ -29,15 +29,15 @@ test('submit button should be disabled when name or image is missing', async () expect(button).toBeDisabled(); const nameInput = getByLabelText(/name/i); - userEvent.type(nameInput, 'name'); + await userEvent.type(nameInput, 'name'); const imageInput = getByLabelText(/image/i); - userEvent.type(imageInput, 'image'); + await userEvent.type(imageInput, 'image'); await expect(findByText(/Deploy the container/)).resolves.toBeEnabled(); expect(nameInput).toHaveValue('name'); - userEvent.clear(nameInput); + await userEvent.clear(nameInput); await expect(findByText(/Deploy the container/)).resolves.toBeDisabled(); }); diff --git a/app/react/components/Badge/Badge.test.tsx b/app/react/components/Badge/Badge.test.tsx new file mode 100644 index 000000000..12859bade --- /dev/null +++ b/app/react/components/Badge/Badge.test.tsx @@ -0,0 +1,26 @@ +import { render } from '@/react-tools/test-utils'; + +import { Badge } from './Badge'; + +test('should render a Badge component with default type', () => { + const { getByText } = render(Default Badge); + const badgeElement = getByText('Default Badge'); + expect(badgeElement).toBeInTheDocument(); + expect(badgeElement).toHaveClass('text-blue-9 bg-blue-2'); +}); + +test('should render a Badge component with custom type', () => { + const { getByText } = render(Success Badge); + const badgeElement = getByText('Success Badge'); + expect(badgeElement).toBeInTheDocument(); + expect(badgeElement).toHaveClass('text-success-9 bg-success-2'); +}); + +test('should render a Badge component with custom className', () => { + const { getByText } = render( + Custom Badge + ); + const badgeElement = getByText('Custom Badge'); + expect(badgeElement).toBeInTheDocument(); + expect(badgeElement).toHaveClass('custom-class'); +}); diff --git a/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx b/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx new file mode 100644 index 000000000..912d63d24 --- /dev/null +++ b/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx @@ -0,0 +1,90 @@ +import { FormikErrors } from 'formik'; +import { ComponentProps } from 'react'; +import { HttpResponse } from 'msw'; + +import { renderWithQueryClient, fireEvent } from '@/react-tools/test-utils'; +import { http, server } from '@/setup-tests/server'; + +import { ImageConfigFieldset } from './ImageConfigFieldset'; +import { Values } from './types'; + +vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ + ...(await importOriginal()), + useCurrentStateAndParams: vi.fn(() => ({ + params: { endpointId: 1 }, + })), +})); + +it('should render SimpleForm when useRegistry is true', () => { + const { getByText } = render({ values: { useRegistry: true } }); + + expect(getByText('Advanced mode')).toBeInTheDocument(); +}); + +it('should render AdvancedForm when useRegistry is false', () => { + const { getByText } = render({ 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({ + values: { useRegistry: true }, + setFieldValue, + }); + + fireEvent.click(getByText('Advanced mode')); + + expect(setFieldValue).toHaveBeenCalledWith('useRegistry', false); +}); + +it('should call setFieldValue with useRegistry set to true when "Simple mode" button is clicked', () => { + const setFieldValue = vi.fn(); + const { getByText } = render({ + values: { useRegistry: false }, + setFieldValue, + }); + + fireEvent.click(getByText('Simple mode')); + + expect(setFieldValue).toHaveBeenCalledWith('useRegistry', true); +}); + +function render({ + values = { + useRegistry: true, + registryId: 123, + image: '', + }, + errors = {}, + setFieldValue = vi.fn(), + onChangeImage = vi.fn(), + onRateLimit = vi.fn(), +}: { + values?: Partial; + errors?: FormikErrors; + setFieldValue?: ComponentProps['setFieldValue']; + onChangeImage?: ComponentProps['onChangeImage']; + onRateLimit?: ComponentProps['onRateLimit']; +} = {}) { + server.use( + http.get('/api/registries/:id', () => HttpResponse.json({})), + http.get('/api/endpoints/:id', () => HttpResponse.json({})) + ); + + return renderWithQueryClient( + + ); +} diff --git a/app/react/components/NavTabs/NavTabs.test.tsx b/app/react/components/NavTabs/NavTabs.test.tsx index f9973e677..22eb59fba 100644 --- a/app/react/components/NavTabs/NavTabs.test.tsx +++ b/app/react/components/NavTabs/NavTabs.test.tsx @@ -42,7 +42,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); - userEvent.click(heading); + await userEvent.click(heading); expect(onSelect).toHaveBeenCalledWith(options[0].id); }); diff --git a/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx b/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx index f60090100..fe20994fb 100644 --- a/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx +++ b/app/react/portainer/account/CreateAccessTokenView/CreateUserAccessToken.test.tsx @@ -17,14 +17,14 @@ test('the button is disabled when all fields are blank and enabled when all fiel const descriptionField = getByLabelText(/Description/); const passwordField = getByLabelText(/Current password/); - userEvent.type(passwordField, 'password'); - userEvent.type(descriptionField, 'description'); + await userEvent.type(passwordField, 'password'); + await userEvent.type(descriptionField, 'description'); await waitFor(() => { expect(button).toBeEnabled(); }); - userEvent.clear(descriptionField); + await userEvent.clear(descriptionField); await waitFor(() => { expect(button).toBeDisabled(); }); diff --git a/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx b/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx new file mode 100644 index 000000000..b554bc702 --- /dev/null +++ b/app/react/portainer/templates/app-templates/AppTemplatesListItem.test.tsx @@ -0,0 +1,102 @@ +import userEvent from '@testing-library/user-event'; +import { PropsWithChildren } from 'react'; + +import { render } from '@/react-tools/test-utils'; + +import { AppTemplatesListItem } from './AppTemplatesListItem'; +import { TemplateViewModel } from './view-model'; +import { TemplateType } from './types'; + +test('should render AppTemplatesListItem component', () => { + const template: TemplateViewModel = { + Title: 'Test Template', + // provide necessary properties for the template object + } as TemplateViewModel; + + const onSelect = vi.fn(); + const isSelected = false; + + const { getByText } = render( + + ); + + expect(getByText(template.Title, { exact: false })).toBeInTheDocument(); +}); + +const copyAsCustomTestCases = [ + { + type: TemplateType.Container, + expected: false, + }, + { + type: TemplateType.ComposeStack, + expected: true, + }, + { + type: TemplateType.SwarmStack, + expected: true, + }, +]; + +// TODO - remove after fixing workaround for UISref +vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ + ...(await importOriginal()), + UISref: ({ children }: PropsWithChildren) => children, // Mocking UISref to render its children directly +})); + +copyAsCustomTestCases.forEach(({ type, expected }) => { + test(`copy as custom button should ${ + expected ? '' : 'not ' + }be rendered for type ${type}`, () => { + const onSelect = vi.fn(); + const isSelected = false; + + const { queryByText, unmount } = render( + + ); + + if (expected) { + expect(queryByText('Copy as Custom')).toBeVisible(); + } else { + expect(queryByText('Copy as Custom')).toBeNull(); + } + + unmount(); + }); +}); + +test('should call onSelect when clicked', async () => { + const user = userEvent.setup(); + const template: TemplateViewModel = { + Title: 'Test Template', + // provide necessary properties for the template object + } as TemplateViewModel; + + const onSelect = vi.fn(); + const isSelected = false; + + const { getByLabelText } = render( + + ); + + const button = getByLabelText(template.Title); + await user.click(button); + + expect(onSelect).toHaveBeenCalledWith(template); +}); diff --git a/app/react/portainer/templates/components/TemplateItem.tsx b/app/react/portainer/templates/components/TemplateItem.tsx index b08191c80..ffb5ac055 100644 --- a/app/react/portainer/templates/components/TemplateItem.tsx +++ b/app/react/portainer/templates/components/TemplateItem.tsx @@ -45,6 +45,7 @@ export function TemplateItem({ as={linkParams ? Link : undefined} to={linkParams?.to} params={linkParams?.params} + aria-label={template.Title} >
{ expect(nameField).toHaveDisplayValue(newValue); diff --git a/package.json b/package.json index 0c4eba2fe..57c0faaff 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "@svgr/webpack": "^8.1.0", "@testing-library/dom": "^9.3.4", "@testing-library/react": "^12", - "@testing-library/user-event": "^13.5.0", + "@testing-library/user-event": "^14.5.2", "@types/angular": "^1.8.3", "@types/file-saver": "^2.0.4", "@types/filesize-parser": "^1.5.1", diff --git a/yarn.lock b/yarn.lock index 709b6b416..2941ed1b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5624,12 +5624,10 @@ "@testing-library/dom" "^8.0.0" "@types/react-dom" "<18.0.0" -"@testing-library/user-event@^13.5.0": - version "13.5.0" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" - integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== - dependencies: - "@babel/runtime" "^7.12.5" +"@testing-library/user-event@^14.5.2": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== "@tippyjs/react@^4.2.6": version "4.2.6"