{beforeHeader || null}
-
{title}
+
{title}
{!hideCloseBtn && (
-
+
)}
diff --git a/web/app/components/base/modal/index.spec.tsx b/web/app/components/base/modal/index.spec.tsx
new file mode 100644
index 0000000000..cab95c7cb1
--- /dev/null
+++ b/web/app/components/base/modal/index.spec.tsx
@@ -0,0 +1,185 @@
+import { act, fireEvent, render, screen } from '@testing-library/react'
+import Modal from '.'
+
+describe('Modal', () => {
+ describe('Render', () => {
+ it('should not render content when isShow is false', () => {
+ render(
+
+ Modal Content
+ ,
+ )
+
+ expect(screen.queryByText('Test Modal')).not.toBeInTheDocument()
+ expect(screen.queryByText('Modal Content')).not.toBeInTheDocument()
+ })
+
+ it('should render content when isShow is true', async () => {
+ await act(async () => {
+ render(
+
+ Modal Content
+ ,
+ )
+ })
+
+ expect(screen.getByText('Test Modal')).toBeInTheDocument()
+ expect(screen.getByText('Modal Content')).toBeInTheDocument()
+ })
+
+ it('should render description when provided', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ expect(screen.getByText('Test Description')).toBeInTheDocument()
+ })
+ })
+
+ describe('Interaction', () => {
+ it('should call onClose when close button is clicked', async () => {
+ const handleClose = vi.fn()
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ const closeButton = screen.getByTestId('modal-close-button')
+ expect(closeButton).toBeInTheDocument()
+ await act(async () => {
+ fireEvent.click(closeButton!)
+ })
+ expect(handleClose).toHaveBeenCalledTimes(1)
+ })
+
+ it('should prevent propagation when clicking the scrollable container', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ const wrapper = document.querySelector('.overflow-y-auto')
+ expect(wrapper).toBeInTheDocument()
+
+ const event = new MouseEvent('click', { bubbles: true, cancelable: true })
+ const stopPropagationSpy = vi.spyOn(event, 'stopPropagation')
+ const preventDefaultSpy = vi.spyOn(event, 'preventDefault')
+
+ await act(async () => {
+ wrapper!.dispatchEvent(event)
+ })
+
+ expect(stopPropagationSpy).toHaveBeenCalled()
+ expect(preventDefaultSpy).toHaveBeenCalled()
+ })
+
+ it('should handle clickOutsideNotClose prop', async () => {
+ const handleClose = vi.fn()
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ await act(async () => {
+ fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape', code: 'Escape' })
+ })
+
+ expect(handleClose).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Props', () => {
+ it('should apply custom className to the panel', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ const panel = screen.getByText('Test Modal').parentElement
+ expect(panel).toHaveClass('custom-panel-class')
+ })
+
+ it('should apply wrapperClassName and containerClassName', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ const dialog = document.querySelector('.custom-wrapper')
+ expect(dialog).toBeInTheDocument()
+ const container = document.querySelector('.custom-container')
+ expect(container).toBeInTheDocument()
+ })
+
+ it('should apply highPriority z-index when highPriority is true', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ const dialog = document.querySelector('.z-\\[1100\\]')
+ expect(dialog).toBeInTheDocument()
+ })
+
+ it('should apply overlayOpacity background when overlayOpacity is true', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+
+ const overlay = document.querySelector('.bg-workflow-canvas-canvas-overlay')
+ expect(overlay).toBeInTheDocument()
+ })
+
+ it('should toggle overflow-visible class based on overflowVisible prop', async () => {
+ const { rerender } = render(
+
+ Content
+ ,
+ )
+
+ let panel = screen.getByText('Test Modal').parentElement
+ expect(panel).toHaveClass('overflow-visible')
+
+ await act(async () => {
+ rerender(
+
+ Content
+ ,
+ )
+ })
+ panel = screen.getByText('Test Modal').parentElement
+ expect(panel).toHaveClass('overflow-hidden')
+ })
+ })
+})
diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx
index 192fb7b70a..023934b674 100644
--- a/web/app/components/base/modal/index.tsx
+++ b/web/app/components/base/modal/index.tsx
@@ -1,5 +1,4 @@
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
-import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { Fragment } from 'react'
import { cn } from '@/utils/classnames'
@@ -55,27 +54,28 @@ export default function Modal({
{!!title && (
{title}
)}
{!!description && (
-
+
{description}
)}
{closable
&& (
- {
e.stopPropagation()
onClose()
}
}
+ data-testid="modal-close-button"
/>
)}
diff --git a/web/app/components/base/modal/modal.spec.tsx b/web/app/components/base/modal/modal.spec.tsx
new file mode 100644
index 0000000000..df2c3bd15d
--- /dev/null
+++ b/web/app/components/base/modal/modal.spec.tsx
@@ -0,0 +1,114 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import Modal from './modal'
+
+describe('Modal Component', () => {
+ const defaultProps = {
+ title: 'Test Modal',
+ onClose: vi.fn(),
+ onConfirm: vi.fn(),
+ onCancel: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('Render', () => {
+ it('renders correctly with title and children', () => {
+ render(
+
+ Child Content
+ ,
+ )
+
+ expect(screen.getByText('Test Modal')).toBeInTheDocument()
+ expect(screen.getByTestId('modal-child')).toBeInTheDocument()
+ expect(screen.getByText(/cancel/i)).toBeInTheDocument()
+ expect(screen.getByText(/save/i)).toBeInTheDocument()
+ })
+
+ it('renders subTitle when provided', () => {
+ render(
)
+ expect(screen.getByText('Test Subtitle')).toBeInTheDocument()
+ })
+
+ it('renders and handles extra button', () => {
+ const onExtraClick = vi.fn()
+ render(
+
,
+ )
+
+ const extraBtn = screen.getByText('Extra Action')
+ expect(extraBtn).toBeInTheDocument()
+ fireEvent.click(extraBtn)
+ expect(onExtraClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('renders footerSlot and bottomSlot', () => {
+ render(
+
Footer}
+ bottomSlot={
Bottom
}
+ />,
+ )
+
+ expect(screen.getByTestId('footer-slot')).toBeInTheDocument()
+ expect(screen.getByTestId('bottom-slot')).toBeInTheDocument()
+ })
+ })
+
+ describe('Interactions', () => {
+ it('calls onClose when close icon is clicked', () => {
+ render(
)
+ const closeIcon = screen.getByTestId('close-icon').parentElement
+ fireEvent.click(closeIcon!)
+ expect(defaultProps.onClose).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls onConfirm when confirm button is clicked', () => {
+ render(
)
+ fireEvent.click(screen.getByText(/confirm/i))
+ expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls onCancel when cancel button is clicked', () => {
+ render(
)
+ fireEvent.click(screen.getByText('Cancel Me'))
+ expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
+ })
+
+ it('handles clickOutsideNotClose logic', () => {
+ const onClose = vi.fn()
+ const { rerender } = render(
)
+
+ fireEvent.click(screen.getByRole('tooltip'))
+ expect(onClose).toHaveBeenCalledTimes(1)
+
+ onClose.mockClear()
+ rerender(
)
+ fireEvent.click(screen.getByRole('tooltip'))
+ expect(onClose).not.toHaveBeenCalled()
+ })
+
+ it('prevents propagation on internal container click', () => {
+ const onClose = vi.fn()
+ render(
)
+ fireEvent.click(screen.getByText('Test Modal'))
+ expect(onClose).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Props', () => {
+ it('disables buttons when disabled prop is true', () => {
+ render(
)
+ expect(screen.getByText(/cancel/i).closest('button')).toBeDisabled()
+ expect(screen.getByText(/save/i).closest('button')).toBeDisabled()
+ })
+ })
+})
diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx
index 0061cdf7a0..3ad08e2493 100644
--- a/web/app/components/base/modal/modal.tsx
+++ b/web/app/components/base/modal/modal.tsx
@@ -1,5 +1,4 @@
import type { ButtonProps } from '@/app/components/base/button'
-import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -69,11 +68,11 @@ const Modal = ({
)}
onClick={e => e.stopPropagation()}
>
-
+
{title}
{
subTitle && (
-
+
{subTitle}
)
@@ -82,7 +81,7 @@ const Modal = ({
className="absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg"
onClick={onClose}
>
-
+
{
diff --git a/web/app/components/base/popover/index.spec.tsx b/web/app/components/base/popover/index.spec.tsx
new file mode 100644
index 0000000000..f90a024bcf
--- /dev/null
+++ b/web/app/components/base/popover/index.spec.tsx
@@ -0,0 +1,238 @@
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import CustomPopover from '.'
+
+const CloseButtonContent = ({ onClick }: { onClick?: () => void }) => (
+
+)
+
+describe('CustomPopover', () => {
+ const defaultProps = {
+ btnElement:
Trigger,
+ htmlContent:
Popover Content
,
+ }
+
+ beforeEach(() => {
+ vi.useFakeTimers()
+ })
+
+ afterEach(() => {
+ if (vi.isFakeTimers?.())
+ vi.clearAllTimers()
+ vi.restoreAllMocks()
+ vi.useRealTimers()
+ })
+
+ describe('Rendering', () => {
+ it('should render the trigger element', () => {
+ render(
)
+ expect(screen.getByTestId('trigger')).toBeInTheDocument()
+ })
+
+ it('should render string as htmlContent', async () => {
+ render(
)
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('trigger'))
+ })
+ expect(screen.getByText('String Content')).toBeInTheDocument()
+ })
+ })
+
+ describe('Interactions', () => {
+ it('should toggle when clicking the button', async () => {
+ vi.useRealTimers()
+ const user = userEvent.setup()
+ render(
)
+ const trigger = screen.getByTestId('trigger')
+
+ await user.click(trigger)
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+
+ await user.click(trigger)
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+ })
+ })
+
+ it('should open on hover when trigger is "hover" (default)', async () => {
+ render(
)
+
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+
+ const triggerContainer = screen.getByTestId('trigger').closest('div')
+ if (!triggerContainer)
+ throw new Error('Trigger container not found')
+
+ await act(async () => {
+ fireEvent.mouseEnter(triggerContainer)
+ })
+
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+ })
+
+ it('should close after delay on mouse leave when trigger is "hover"', async () => {
+ vi.useRealTimers()
+ const user = userEvent.setup()
+ render(
)
+
+ const trigger = screen.getByTestId('trigger')
+
+ await user.hover(trigger)
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+
+ await user.unhover(trigger)
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+ }, { timeout: 2000 })
+ })
+
+ it('should stay open when hovering over the popover content', async () => {
+ vi.useRealTimers()
+ const user = userEvent.setup()
+ render(
)
+
+ const trigger = screen.getByTestId('trigger')
+ await user.hover(trigger)
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+
+ // Leave trigger but enter content
+ await user.unhover(trigger)
+ const content = screen.getByTestId('content')
+ await user.hover(content)
+
+ // Wait for the timeout duration
+ await act(async () => {
+ await new Promise(resolve => setTimeout(resolve, 200))
+ })
+
+ // Should still be open because we are hovering the content
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+
+ // Now leave content
+ await user.unhover(content)
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+ }, { timeout: 2000 })
+ })
+
+ it('should cancel close timeout when re-entering during hover delay', async () => {
+ render(
)
+
+ const triggerContainer = screen.getByTestId('trigger').closest('div')
+ if (!triggerContainer)
+ throw new Error('Trigger container not found')
+
+ await act(async () => {
+ fireEvent.mouseEnter(triggerContainer)
+ })
+
+ await act(async () => {
+ fireEvent.mouseLeave(triggerContainer!)
+ })
+
+ await act(async () => {
+ vi.advanceTimersByTime(50) // Halfway through timeout
+ fireEvent.mouseEnter(triggerContainer!)
+ })
+
+ await act(async () => {
+ vi.advanceTimersByTime(1000) // Much longer than the original timeout
+ })
+
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+ })
+
+ it('should not open when disabled', async () => {
+ render(
)
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('trigger'))
+ })
+
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+ })
+
+ it('should pass close function to htmlContent when manualClose is true', async () => {
+ vi.useRealTimers()
+
+ render(
+
}
+ trigger="click"
+ manualClose={true}
+ />,
+ )
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('trigger'))
+ })
+
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('content'))
+ })
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+ })
+ })
+
+ it('should not close when mouse leaves while already closed', async () => {
+ render(
)
+ const triggerContainer = screen.getByTestId('trigger').closest('div')
+ if (!triggerContainer)
+ throw new Error('Trigger container not found')
+
+ await act(async () => {
+ fireEvent.mouseLeave(triggerContainer)
+ })
+
+ await act(async () => {
+ vi.runAllTimers()
+ })
+
+ expect(screen.queryByTestId('content')).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Props', () => {
+ it('should apply custom class names', async () => {
+ render(
+
,
+ )
+
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('trigger'))
+ })
+
+ expect(document.querySelector('.wrapper-class')).toBeInTheDocument()
+ expect(document.querySelector('.popup-inner-class')).toBeInTheDocument()
+
+ const button = screen.getByTestId('trigger').parentElement
+ expect(button).toHaveClass('btn-class')
+ })
+
+ it('should handle btnClassName as a function', () => {
+ render(
+
open ? 'btn-open' : 'btn-closed'}
+ />,
+ )
+
+ const button = screen.getByTestId('trigger').parentElement
+ expect(button).toHaveClass('btn-closed')
+ })
+ })
+})
diff --git a/web/app/components/base/progress-bar/progress-circle.spec.tsx b/web/app/components/base/progress-bar/progress-circle.spec.tsx
new file mode 100644
index 0000000000..9acc525d90
--- /dev/null
+++ b/web/app/components/base/progress-bar/progress-circle.spec.tsx
@@ -0,0 +1,89 @@
+import { render } from '@testing-library/react'
+import ProgressCircle from './progress-circle'
+
+const extractLargeArcFlag = (pathData: string): string => {
+ const afterA = pathData.slice(pathData.indexOf('A') + 1)
+ const tokens = afterA.replace(/,/g, ' ').trim().split(/\s+/)
+ // Arc syntax: A rx ry x-axis-rotation large-arc-flag sweep-flag x y
+ return tokens[3]
+}
+
+describe('ProgressCircle', () => {
+ describe('Render', () => {
+ it('renders an SVG with default props', () => {
+ const { container } = render()
+
+ const svg = container.querySelector('svg')
+ const circle = container.querySelector('circle')
+ const path = container.querySelector('path')
+
+ expect(svg).toBeInTheDocument()
+ expect(circle).toBeInTheDocument()
+ expect(path).toBeInTheDocument()
+ })
+ })
+
+ describe('Props', () => {
+ it('applies correct size and viewBox when size is provided', () => {
+ const size = 24
+ const strokeWidth = 2
+
+ const { container } = render(
+ ,
+ )
+
+ const svg = container.querySelector('svg') as SVGElement
+
+ expect(svg).toHaveAttribute('width', String(size + strokeWidth))
+ expect(svg).toHaveAttribute('height', String(size + strokeWidth))
+ expect(svg).toHaveAttribute(
+ 'viewBox',
+ `0 0 ${size + strokeWidth} ${size + strokeWidth}`,
+ )
+ })
+
+ it('applies custom stroke and fill classes to the circle', () => {
+ const { container } = render(
+ ,
+ )
+ const circle = container.querySelector('circle')!
+ expect(circle!).toHaveClass('stroke-red-500')
+ expect(circle!).toHaveClass('fill-red-100')
+ })
+
+ it('applies custom sector fill color to the path', () => {
+ const { container } = render(
+ ,
+ )
+ const path = container.querySelector('path')!
+ expect(path!).toHaveClass('fill-blue-500')
+ })
+
+ it('uses large arc flag when percentage is greater than 50', () => {
+ const { container } = render()
+ const path = container.querySelector('path')!
+ const d = path.getAttribute('d') || ''
+ expect(d).toContain('A')
+ expect(extractLargeArcFlag(d)).toBe('1')
+ })
+
+ it('uses small arc flag when percentage is 50 or less', () => {
+ const { container } = render()
+ const path = container.querySelector('path')!
+ const d = path.getAttribute('d') || ''
+ expect(d).toContain('A')
+ expect(extractLargeArcFlag(d)).toBe('0')
+ })
+
+ it('uses small arc flag when percentage is exactly 50', () => {
+ const { container } = render()
+ const path = container.querySelector('path')!
+ const d = path.getAttribute('d') || ''
+ expect(d).toContain('A')
+ expect(extractLargeArcFlag(d)).toBe('0')
+ })
+ })
+})
diff --git a/web/app/components/base/prompt-log-modal/card.spec.tsx b/web/app/components/base/prompt-log-modal/card.spec.tsx
new file mode 100644
index 0000000000..500e9db941
--- /dev/null
+++ b/web/app/components/base/prompt-log-modal/card.spec.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from '@testing-library/react'
+import Card from './card'
+
+describe('PromptLogModal Card', () => {
+ it('renders single log entry correctly', () => {
+ const log = [{ role: 'user', text: 'Single entry text' }]
+ render()
+
+ expect(screen.getByText('Single entry text')).toBeInTheDocument()
+ expect(screen.queryByText('USER')).not.toBeInTheDocument()
+ })
+
+ it('renders multiple log entries correctly', () => {
+ const log = [
+ { role: 'user', text: 'Message 1' },
+ { role: 'assistant', text: 'Message 2' },
+ ]
+ render()
+
+ expect(screen.getByText('USER')).toBeInTheDocument()
+ expect(screen.getByText('ASSISTANT')).toBeInTheDocument()
+ expect(screen.getByText('Message 1')).toBeInTheDocument()
+ expect(screen.getByText('Message 2')).toBeInTheDocument()
+ })
+})
diff --git a/web/app/components/base/prompt-log-modal/index.spec.tsx b/web/app/components/base/prompt-log-modal/index.spec.tsx
new file mode 100644
index 0000000000..c04e668026
--- /dev/null
+++ b/web/app/components/base/prompt-log-modal/index.spec.tsx
@@ -0,0 +1,60 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import PromptLogModal from '.'
+
+describe('PromptLogModal', () => {
+ const defaultProps = {
+ width: 1000,
+ onCancel: vi.fn(),
+ currentLogItem: {
+ id: '1',
+ content: 'test',
+ log: [{ role: 'user', text: 'Hello' }],
+ } as Parameters[0]['currentLogItem'],
+ }
+
+ describe('Render', () => {
+ it('renders correctly when currentLogItem is provided', () => {
+ render()
+ expect(screen.getByText('PROMPT LOG')).toBeInTheDocument()
+ expect(screen.getByText('Hello')).toBeInTheDocument()
+ })
+
+ it('returns null when currentLogItem is missing', () => {
+ const { container } = render()
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders copy feedback when log length is 1', () => {
+ render()
+ expect(screen.getByTestId('close-btn-container')).toBeInTheDocument()
+ })
+ })
+
+ describe('Interactions', () => {
+ it('calls onCancel when close button is clicked', () => {
+ render()
+ const closeBtn = screen.getByTestId('close-btn')
+ expect(closeBtn).toBeInTheDocument()
+ fireEvent.click(closeBtn)
+ expect(defaultProps.onCancel).toHaveBeenCalled()
+ })
+
+ it('calls onCancel when clicking outside', async () => {
+ const user = userEvent.setup()
+ const onCancel = vi.fn()
+ render(
+ ,
+ )
+
+ await waitFor(() => {
+ expect(screen.getByTestId('close-btn')).toBeInTheDocument()
+ })
+
+ await user.click(screen.getByTestId('outside'))
+ })
+ })
+})
diff --git a/web/app/components/base/prompt-log-modal/index.tsx b/web/app/components/base/prompt-log-modal/index.tsx
index aab75d4989..bd29782bd0 100644
--- a/web/app/components/base/prompt-log-modal/index.tsx
+++ b/web/app/components/base/prompt-log-modal/index.tsx
@@ -1,6 +1,5 @@
import type { FC } from 'react'
import type { IChatItem } from '@/app/components/base/chat/chat/type'
-import { RiCloseLine } from '@remixicon/react'
import { useClickAway } from 'ahooks'
import { useEffect, useRef, useState } from 'react'
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
@@ -57,8 +56,9 @@ const PromptLogModal: FC = ({
-
+
diff --git a/web/app/components/base/qrcode/index.spec.tsx b/web/app/components/base/qrcode/index.spec.tsx
new file mode 100644
index 0000000000..3e1d5ff6d9
--- /dev/null
+++ b/web/app/components/base/qrcode/index.spec.tsx
@@ -0,0 +1,94 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { downloadUrl } from '@/utils/download'
+import ShareQRCode from '.'
+
+vi.mock('@/utils/download', () => ({
+ downloadUrl: vi.fn(),
+}))
+
+describe('ShareQRCode', () => {
+ const content = 'https://example.com'
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('Rendering', () => {
+ it('renders correctly', () => {
+ render(