From 08d08ef678e3afd70fbe0d25cacec9e15d4ebb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Matias=CC=8Cko?= Date: Thu, 18 Apr 2024 22:51:43 +0200 Subject: [PATCH] added jest tests --- .babelrc | 24 +- src/common/services/translate.ts | 11 + .../Atomic/ContentMenu/ContentMenu.test.tsx | 62 +++++ .../Atomic/ContentMenu/ContentMenu.tsx | 2 +- .../__snapshots__/ContentMenu.test.tsx.snap | 244 ++++++++++++++++++ .../ContentSwitch/ContentSwitch.test.tsx | 60 +++++ .../__snapshots__/ContentSwitch.test.tsx.snap | 13 + .../Atomic/CopyIcon/CopyIcon.test.tsx | 24 ++ src/components/Atomic/CopyIcon/CopyIcon.tsx | 7 +- .../Atomic/CopyIcon/CopyIcon.types.ts | 1 + .../__snapshots__/CopyIcon.test.tsx.snap | 33 +++ .../FloatingPanel/FloatingPanel.styles.ts | 23 +- .../Atomic/FloatingPanel/FloatingPanel.tsx | 11 +- .../FloatingPanel/FloatingPanel.types.ts | 1 - .../NotificationCenter/NotificationCenter.tsx | 116 +++------ .../components/Bell/Bell.types.ts | 2 +- .../SimpleStripTable/SimpleStripTable.tsx | 7 +- src/stories/Asset/CopyIcon.stories.tsx | 18 ++ src/stories/{ => Asset}/Icon.stories.tsx | 4 +- src/stories/Layout/ContentMenu.stories.jsx | 70 +++++ 20 files changed, 596 insertions(+), 137 deletions(-) create mode 100644 src/common/services/translate.ts create mode 100644 src/components/Atomic/ContentMenu/ContentMenu.test.tsx create mode 100644 src/components/Atomic/ContentMenu/__snapshots__/ContentMenu.test.tsx.snap create mode 100644 src/components/Atomic/ContentSwitch/ContentSwitch.test.tsx create mode 100644 src/components/Atomic/ContentSwitch/__snapshots__/ContentSwitch.test.tsx.snap create mode 100644 src/components/Atomic/CopyIcon/CopyIcon.test.tsx create mode 100644 src/components/Atomic/CopyIcon/__snapshots__/CopyIcon.test.tsx.snap create mode 100644 src/stories/Asset/CopyIcon.stories.tsx rename src/stories/{ => Asset}/Icon.stories.tsx (95%) create mode 100644 src/stories/Layout/ContentMenu.stories.jsx diff --git a/.babelrc b/.babelrc index c0235e36..08358a20 100644 --- a/.babelrc +++ b/.babelrc @@ -1,25 +1,5 @@ { - "presets": [ - [ - "react-app", - { - "flow": false, - "typescript": true, - "helpers": false - } - ], - "@babel/preset-env" - ], - "plugins": [ - "@emotion/babel-plugin", - "@babel/plugin-transform-runtime", - [ - "@babel/plugin-transform-react-jsx", - { - "runtime": "automatic", - "importSource": "@emotion/react" - } - ] - ], + "presets": [], + "plugins": [], "comments": false } diff --git a/src/common/services/translate.ts b/src/common/services/translate.ts new file mode 100644 index 00000000..3d0d1578 --- /dev/null +++ b/src/common/services/translate.ts @@ -0,0 +1,11 @@ +let formatMessage: any = null + +export const translate = { + setTranslator: (t: any) => (formatMessage = t), + translate: (key: { defaultMessage: string; id: string }, params?: any) => { + if (formatMessage) { + return formatMessage(key, params) + } + return key + }, +} diff --git a/src/components/Atomic/ContentMenu/ContentMenu.test.tsx b/src/components/Atomic/ContentMenu/ContentMenu.test.tsx new file mode 100644 index 00000000..9b2ae7bd --- /dev/null +++ b/src/components/Atomic/ContentMenu/ContentMenu.test.tsx @@ -0,0 +1,62 @@ +import { render, fireEvent } from '@testing-library/react' +import ContentMenu from './ContentMenu' + +describe('', () => { + const mockHandleItemClick = jest.fn() + const mockHandleSubItemClick = jest.fn() + + const defaultProps = { + activeItem: 'item1', + handleItemClick: mockHandleItemClick, + handleSubItemClick: mockHandleSubItemClick, + id: 'test-menu', + menu: [ + { + id: 'item1', + link: 'item1', + title: 'Item 1', + children: [ + { + id: 'subitem1', + title: 'Subitem 1', + }, + ], + }, + { + id: 'item2', + link: 'item2', + title: 'Item 2', + }, + ], + } + + it('renders without crashing', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + }) + + it('calls handleItemClick when an item is clicked', () => { + const { getByText } = render() + fireEvent.click(getByText('Item 1')) + expect(mockHandleItemClick).toHaveBeenCalled() + }) + + it('calls handleSubItemClick when a subitem is clicked', () => { + const { getByText } = render() + fireEvent.click(getByText('Subitem 1')) + expect(mockHandleSubItemClick).toHaveBeenCalled() + }) + + it('does not call handleSubItemClick when a subitem is clicked but handleSubItemClick is not provided', () => { + const { getByText } = render() + fireEvent.click(getByText('Subitem 1')) + expect(mockHandleSubItemClick).not.toHaveBeenCalled() + }) + + it('filters menu items based on search input', () => { + const { getByText, getByRole } = render() + fireEvent.change(getByRole('search'), { target: { value: 'Item 2' } }) + expect(getByText('Item 2')).toBeInTheDocument() + expect(() => getByText('Item 1')).toThrow() + }) +}) diff --git a/src/components/Atomic/ContentMenu/ContentMenu.tsx b/src/components/Atomic/ContentMenu/ContentMenu.tsx index 57f6d0f7..0f2ba234 100644 --- a/src/components/Atomic/ContentMenu/ContentMenu.tsx +++ b/src/components/Atomic/ContentMenu/ContentMenu.tsx @@ -45,7 +45,7 @@ const ContentMenu: FC = (props) => { - setSearch(e.target.value)} type='search' value={search} /> + setSearch(e.target.value)} role='search' type='search' value={search} /> )}
    diff --git a/src/components/Atomic/ContentMenu/__snapshots__/ContentMenu.test.tsx.snap b/src/components/Atomic/ContentMenu/__snapshots__/ContentMenu.test.tsx.snap new file mode 100644 index 00000000..c1ee1975 --- /dev/null +++ b/src/components/Atomic/ContentMenu/__snapshots__/ContentMenu.test.tsx.snap @@ -0,0 +1,244 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders without crashing 1`] = ` + + .emotion-0 { + padding: 16px 12px 12px 12px; + border-radius: 16px; + border: 1px solid; + box-shadow: 0 1px 2px 0 rgba(228, 229, 231, 0.24); +} + +.emotion-1 { + list-style: none; + padding: 0; + margin: 0; +} + +.emotion-1 .item-enter-done { + max-height: 200px; +} + +.emotion-1 .item-enter-active { + max-height: 200px; +} + +.emotion-1 .item-exit { + max-height: 0; +} + +.emotion-1 .item-exit-active { + max-height: 0; +} + +.emotion-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 10px 12px; + -webkit-text-decoration: none; + text-decoration: none; + min-height: 40px; + box-sizing: border-box; + border-radius: 8px; + -webkit-transition: all 0.25s; + transition: all 0.25s; + position: relative; + overflow: hidden; +} + +.emotion-2:hover { + -webkit-text-decoration: none!important; + text-decoration: none!important; +} + +.emotion-2 .icon { + color: !important; +} + +.emotion-2:hover { + -webkit-text-decoration: none!important; + text-decoration: none!important; +} + +.emotion-3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin-left: 12px; + font-weight: bold; + margin-left: 0; +} + +.emotion-4 { + position: absolute; + right: 8px; + top: 50%; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + width: 10px; + height: 10px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -webkit-transition: all 0.25s; + transition: all 0.25s; + -webkit-transform: translateY(-50%) rotate(180deg); + -moz-transform: translateY(-50%) rotate(180deg); + -ms-transform: translateY(-50%) rotate(180deg); + transform: translateY(-50%) rotate(180deg); +} + +.emotion-5 { + max-height: 0; + overflow: hidden; + -webkit-transition: all 0.45s; + transition: all 0.45s; +} + +.emotion-6 { + margin: 0; + padding: 10px 0 0 20px; + list-style: none; +} + +.emotion-7 { + display: block; + position: relative; + padding: 8px 8px 8px 20px; + white-space: nowrap; + margin: 4px 0 4px 0; +} + +.emotion-8 { + width: 14px; + height: 55px; + position: absolute; + left: 0; + bottom: 50%; +} + +.emotion-9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 10px 12px; + -webkit-text-decoration: none; + text-decoration: none; + min-height: 40px; + box-sizing: border-box; + border-radius: 8px; + -webkit-transition: all 0.25s; + transition: all 0.25s; + position: relative; + overflow: hidden; +} + +.emotion-9:hover { + -webkit-text-decoration: none!important; + text-decoration: none!important; +} + + + +`; diff --git a/src/components/Atomic/ContentSwitch/ContentSwitch.test.tsx b/src/components/Atomic/ContentSwitch/ContentSwitch.test.tsx new file mode 100644 index 00000000..a2c32a11 --- /dev/null +++ b/src/components/Atomic/ContentSwitch/ContentSwitch.test.tsx @@ -0,0 +1,60 @@ +import { render, act, waitFor } from '@testing-library/react' +import ContentSwitch from './ContentSwitch' + +describe('', () => { + it('renders without crashing', () => { + const { asFragment } = render( + +
    Item 1
    +
    Item 2
    +
    + ) + + expect(asFragment()).toMatchSnapshot() + }) + + it('renders the active item', () => { + const { getByText } = render( + +
    Item 1
    +
    Item 2
    +
    + ) + expect(getByText('Item 1')).toBeInTheDocument() + }) + + it('does not render inactive items', () => { + const { queryByText } = render( + +
    Item 1
    +
    Item 2
    +
    + ) + expect(queryByText('Item 2')).toBeNull() + }) + + it('calls onAnimationComplete when animation completes', async () => { + const onAnimationComplete = jest.fn() + const { rerender } = render( + +
    Item 1
    +
    Item 2
    +
    + ) + + rerender( + +
    Item 1
    +
    Item 2
    +
    + ) + + act(() => { + jest.runAllTimers() + }) + + await waitFor(() => { + expect(onAnimationComplete).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/Atomic/ContentSwitch/__snapshots__/ContentSwitch.test.tsx.snap b/src/components/Atomic/ContentSwitch/__snapshots__/ContentSwitch.test.tsx.snap new file mode 100644 index 00000000..d71c44f5 --- /dev/null +++ b/src/components/Atomic/ContentSwitch/__snapshots__/ContentSwitch.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders without crashing 1`] = ` + +
    +
    + Item 1 +
    +
    +
    +`; diff --git a/src/components/Atomic/CopyIcon/CopyIcon.test.tsx b/src/components/Atomic/CopyIcon/CopyIcon.test.tsx new file mode 100644 index 00000000..8305d111 --- /dev/null +++ b/src/components/Atomic/CopyIcon/CopyIcon.test.tsx @@ -0,0 +1,24 @@ +import { render, fireEvent } from '@testing-library/react' +import { forEach } from 'lodash' + +import CopyIcon from './CopyIcon' + +describe('CopyIcon', () => { + it('renders without crashing', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + }) + + it('calls copyToClipboard function on click', () => { + const copyToClipboardMock = jest.spyOn(require('../../../common/utils/copy-to-clipboard'), 'copyToClipboard') + + const { container } = render() + const buttons = container.querySelectorAll('[data-test-id="test-icon"]') + + forEach(buttons, (button) => { + fireEvent.click(button) + }) + + expect(copyToClipboardMock).toHaveBeenCalledWith('Test value') + }) +}) diff --git a/src/components/Atomic/CopyIcon/CopyIcon.tsx b/src/components/Atomic/CopyIcon/CopyIcon.tsx index cd3043f5..fc54a181 100644 --- a/src/components/Atomic/CopyIcon/CopyIcon.tsx +++ b/src/components/Atomic/CopyIcon/CopyIcon.tsx @@ -4,13 +4,14 @@ import * as styles from '../SimpleStripTable/SimpleStripTable.styles' import IconCopy from '../Icon/components/IconCopy' import { copyToClipboard } from '../../../common/utils' import Tooltip from '../Tooltip' +import { Props } from './CopyIcon.types' -const CopyIcon: FC = (props) => { - const { i18n, value } = props +const CopyIcon: FC = (props) => { + const { dataTestId, i18n, value } = props return ( - copyToClipboard(value)} /> + copyToClipboard(value)} /> ) } diff --git a/src/components/Atomic/CopyIcon/CopyIcon.types.ts b/src/components/Atomic/CopyIcon/CopyIcon.types.ts index 74bcace1..b82feaa7 100644 --- a/src/components/Atomic/CopyIcon/CopyIcon.types.ts +++ b/src/components/Atomic/CopyIcon/CopyIcon.types.ts @@ -3,4 +3,5 @@ export type Props = { content: string } value: string | number + dataTestId?: string } diff --git a/src/components/Atomic/CopyIcon/__snapshots__/CopyIcon.test.tsx.snap b/src/components/Atomic/CopyIcon/__snapshots__/CopyIcon.test.tsx.snap new file mode 100644 index 00000000..b851a624 --- /dev/null +++ b/src/components/Atomic/CopyIcon/__snapshots__/CopyIcon.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyIcon renders without crashing 1`] = ` + + .emotion-0 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-transition: all 0.3s; + transition: all 0.3s; + margin-left: 8px; + cursor: pointer; +} + +
    + + + +
    +
    +`; diff --git a/src/components/Atomic/FloatingPanel/FloatingPanel.styles.ts b/src/components/Atomic/FloatingPanel/FloatingPanel.styles.ts index f9cca9c4..1d84212b 100644 --- a/src/components/Atomic/FloatingPanel/FloatingPanel.styles.ts +++ b/src/components/Atomic/FloatingPanel/FloatingPanel.styles.ts @@ -1,7 +1,6 @@ import { css } from '@emotion/react' import { ThemeType, get } from '../_theme' -import { commonStyles } from '../_utils/commonStyles' export const referenceItem = (theme: ThemeType) => css` display: flex; @@ -29,26 +28,6 @@ export const floatingPanel = (theme: ThemeType) => css` max-width: 600px; ` -export const header = (theme: ThemeType) => css` - padding: 16px; - display: flex; - height: 56px; - box-sizing: border-box; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid ${get(theme, `FloatingPanel.Content.Headline.borderColor`)}; -` - -export const headline = (theme: ThemeType) => css` - font-family: ${commonStyles.fontSecondary}; - font-style: normal; - font-weight: 700; - font-size: 16px; - line-height: 140%; - letter-spacing: -0.5px; - color: ${get(theme, `FloatingPanel.Content.Headline.color`)}; -` - export const content = css` - padding: 0 16px; + padding: 0; ` diff --git a/src/components/Atomic/FloatingPanel/FloatingPanel.tsx b/src/components/Atomic/FloatingPanel/FloatingPanel.tsx index d91d66d1..dd9af141 100644 --- a/src/components/Atomic/FloatingPanel/FloatingPanel.tsx +++ b/src/components/Atomic/FloatingPanel/FloatingPanel.tsx @@ -8,7 +8,7 @@ import * as styles from './FloatingPanel.styles' import { hasEventBlocker } from '../_utils/envets' const FloatingPanel: FC = (props) => { - const { children, reference, title } = props + const { children, reference } = props const [open, setOpen] = useState(false) const ref = useRef(null) const { x, y, refs, strategy } = useFloating({ @@ -63,13 +63,8 @@ const FloatingPanel: FC = (props) => { { type: 'spring', damping: 20, stiffness: 300 } } > -
    - {title ? ( -
    -
    {title}
    -
    - ) : null} -
    {children}
    +
    + {children}
    )} diff --git a/src/components/Atomic/FloatingPanel/FloatingPanel.types.ts b/src/components/Atomic/FloatingPanel/FloatingPanel.types.ts index 36e0199b..760051a4 100644 --- a/src/components/Atomic/FloatingPanel/FloatingPanel.types.ts +++ b/src/components/Atomic/FloatingPanel/FloatingPanel.types.ts @@ -3,5 +3,4 @@ import { ReactNode } from 'react' export type Props = { children: ReactNode reference: any - title?: string } diff --git a/src/components/Atomic/NotificationCenter/NotificationCenter.tsx b/src/components/Atomic/NotificationCenter/NotificationCenter.tsx index 64e8f5f1..e4dd94b0 100644 --- a/src/components/Atomic/NotificationCenter/NotificationCenter.tsx +++ b/src/components/Atomic/NotificationCenter/NotificationCenter.tsx @@ -1,7 +1,5 @@ import { FC, useCallback, useEffect, useRef, useState } from 'react' -import { offset, shift, useFloating } from '@floating-ui/react' import { motion, AnimatePresence } from 'framer-motion' -import { FloatingPortal } from '@floating-ui/react-dom-interactions' import { useNotificationCenter } from 'react-toastify/addons/use-notification-center' import isFunction from 'lodash/isFunction' @@ -11,16 +9,12 @@ import * as styles from './NotificationCenter.styles' import { hasEventBlocker } from '../_utils/envets' import InnerToast from './components/InnerToast/InnerToast' import Scrollbars from '../Scrollbars' +import FloatingPanel from '../FloatingPanel' const NotificationCenter: FC = (props) => { const { defaultNotification, i18n, onNotification, readAllNotifications } = { ...defaultProps, ...props } const [open, setOpen] = useState(false) const ref = useRef(null) - const { x, y, refs, strategy } = useFloating({ - placement: 'bottom-end', - strategy: 'fixed', - middleware: [shift(), offset(4)], - }) const notificationsCount = useRef(defaultNotification?.length ?? 0) const { notifications, markAllAsRead, unreadCount } = useNotificationCenter({ @@ -56,75 +50,45 @@ const NotificationCenter: FC = (props) => { }, [notifications, onNotification]) return ( -
    - 0} innerRef={refs.setReference} notificationsCount={notifications.length} onClick={() => setOpen(!open)} /> - - - {open && ( - -
    -
    -
    {i18n.notifications}
    - {notifications.length > 0 && ( - { - e.preventDefault() - readAll() - }} - > - {i18n.markAllAsRead} - - )} -
    - - - - - {notifications.length === 0 &&
    {i18n.noNotifications}
    } - {notifications.length > 0 && - notifications.map((notification: any, i) => ( - - ))} -
    -
    -
    -
    -
    -
    - )} -
    -
    -
    + 0} notificationsCount={notifications.length} onClick={() => setOpen(!open)} />}> +
    +
    {i18n.notifications}
    + {notifications.length > 0 && ( + { + e.preventDefault() + readAll() + }} + > + {i18n.markAllAsRead} + + )} +
    + + + + + {notifications.length === 0 &&
    {i18n.noNotifications}
    } + {notifications.length > 0 && + notifications.map((notification: any, i) => )} +
    +
    +
    +
    +
    ) } diff --git a/src/components/Atomic/NotificationCenter/components/Bell/Bell.types.ts b/src/components/Atomic/NotificationCenter/components/Bell/Bell.types.ts index b8994133..d2fe5415 100644 --- a/src/components/Atomic/NotificationCenter/components/Bell/Bell.types.ts +++ b/src/components/Atomic/NotificationCenter/components/Bell/Bell.types.ts @@ -1,6 +1,6 @@ export type Props = { hasUnRead: boolean - innerRef: any + innerRef?: any notificationsCount: number onClick: () => void } diff --git a/src/components/Atomic/SimpleStripTable/SimpleStripTable.tsx b/src/components/Atomic/SimpleStripTable/SimpleStripTable.tsx index 73ef7e15..1b79df3b 100644 --- a/src/components/Atomic/SimpleStripTable/SimpleStripTable.tsx +++ b/src/components/Atomic/SimpleStripTable/SimpleStripTable.tsx @@ -48,7 +48,12 @@ const SimpleStripTable: FC = (props) => { data-test-id={r.dataTestId?.concat('-value')} > {r.value} - {r.copyValue && } + {r.copyValue && ( + + )}
    diff --git a/src/stories/Asset/CopyIcon.stories.tsx b/src/stories/Asset/CopyIcon.stories.tsx new file mode 100644 index 00000000..20618153 --- /dev/null +++ b/src/stories/Asset/CopyIcon.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { StoryFn } from '@storybook/react' +import CopyIcon from '../../components/Atomic/CopyIcon' + +export default { + title: 'Assets/CopyIcon', + component: CopyIcon, + argTypes: {}, +} + +const Template = (args: any) => ( +
    + +
    +) + +export const Default: StoryFn = Template.bind({}) +Default.args = {} diff --git a/src/stories/Icon.stories.tsx b/src/stories/Asset/Icon.stories.tsx similarity index 95% rename from src/stories/Icon.stories.tsx rename to src/stories/Asset/Icon.stories.tsx index 1faced27..8c43bd09 100644 --- a/src/stories/Icon.stories.tsx +++ b/src/stories/Asset/Icon.stories.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { Icon, IconsRaw } from '../components/Atomic/Icon' -import './global.css' +import { Icon, IconsRaw } from '../../components/Atomic/Icon' +import '../global.css' import { StoryFn } from '@storybook/react' export default { diff --git a/src/stories/Layout/ContentMenu.stories.jsx b/src/stories/Layout/ContentMenu.stories.jsx new file mode 100644 index 00000000..0d2790fd --- /dev/null +++ b/src/stories/Layout/ContentMenu.stories.jsx @@ -0,0 +1,70 @@ +import React from 'react' +import ContentMenu from '../../components/Atomic/ContentMenu' + +export default { + title: 'Layout/ContentMenu', + component: ContentMenu, + argTypes: {}, +} + +const Template = (args) => { + const [activeItem, setActiveItem] = React.useState('item1') + return ( +
    + setActiveItem(item.id)} + menu={[ + { + id: 'item1', + link: 'item1', + title: 'Item 1', + children: [ + { + id: 'subitem11', + title: 'Subitem 1', + }, + { + id: 'subitem12', + title: 'Subitem 2', + }, + { + id: 'subitem13', + title: 'Subitem 3', + }, + ], + }, + { + id: 'item2', + link: 'item2', + title: 'Item 2', + children: [ + { + id: 'subitem21', + title: 'Subitem 1', + }, + { + id: 'subitem22', + title: 'Subitem 2', + }, + { + id: 'subitem23', + title: 'Subitem 3', + }, + ], + }, + { + id: 'item3', + link: 'item3', + title: 'Item 3', + }, + ]} + /> +
    + ) +} + +export const Default = Template.bind({}) +Default.args = {} +Default.parameters = {}