diff --git a/web/app/components/base/auto-height-textarea/index.spec.tsx b/web/app/components/base/auto-height-textarea/index.spec.tsx index 2eab1ba82e..f6ac0670df 100644 --- a/web/app/components/base/auto-height-textarea/index.spec.tsx +++ b/web/app/components/base/auto-height-textarea/index.spec.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vitest' import { sleep } from '@/utils' import AutoHeightTextarea from './index' @@ -18,8 +17,8 @@ describe('AutoHeightTextarea', () => { describe('Rendering', () => { it('should render without crashing', () => { - render() - const textarea = document.querySelector('textarea') + const { container } = render() + const textarea = container.querySelector('textarea') expect(textarea).toBeInTheDocument() }) @@ -37,26 +36,26 @@ describe('AutoHeightTextarea', () => { describe('Props', () => { it('should apply custom className to textarea', () => { - render() - const textarea = document.querySelector('textarea') + const { container } = render() + const textarea = container.querySelector('textarea') expect(textarea).toHaveClass('custom-class') }) it('should apply custom wrapperClassName to wrapper div', () => { - render() - const wrapper = document.querySelector('div.relative') + const { container } = render() + const wrapper = container.querySelector('div.relative') expect(wrapper).toHaveClass('wrapper-class') }) it('should apply minHeight and maxHeight styles to hidden div', () => { - render() - const hiddenDiv = document.querySelector('div.invisible') + const { container } = render() + const hiddenDiv = container.querySelector('div.invisible') expect(hiddenDiv).toHaveStyle({ minHeight: '50px', maxHeight: '200px' }) }) it('should use default minHeight and maxHeight when not provided', () => { - render() - const hiddenDiv = document.querySelector('div.invisible') + const { container } = render() + const hiddenDiv = container.querySelector('div.invisible') expect(hiddenDiv).toHaveStyle({ minHeight: '36px', maxHeight: '96px' }) }) @@ -64,6 +63,7 @@ describe('AutoHeightTextarea', () => { const focusSpy = vi.spyOn(HTMLTextAreaElement.prototype, 'focus') render() expect(focusSpy).toHaveBeenCalled() + focusSpy.mockRestore() }) }) @@ -122,7 +122,7 @@ describe('AutoHeightTextarea', () => { it('should handle newlines in value', () => { const textWithNewlines = 'line1\nline2\nline3' render() - const textarea = document.querySelector('textarea') + const textarea = screen.getByRole('textbox') expect(textarea).toHaveValue(textWithNewlines) }) diff --git a/web/app/components/base/button/add-button.spec.tsx b/web/app/components/base/button/add-button.spec.tsx index 658c032bb7..ad27753211 100644 --- a/web/app/components/base/button/add-button.spec.tsx +++ b/web/app/components/base/button/add-button.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render } from '@testing-library/react' +import { fireEvent, render, screen } from '@testing-library/react' import AddButton from './add-button' describe('AddButton', () => { @@ -9,9 +9,9 @@ describe('AddButton', () => { }) it('should render an add icon', () => { - const { container } = render() - const svg = container.querySelector('span') - expect(svg).toBeInTheDocument() + render() + const iconSpan = screen.getByTestId('add-button').querySelector('span') + expect(iconSpan).toBeInTheDocument() }) }) diff --git a/web/app/components/base/button/add-button.tsx b/web/app/components/base/button/add-button.tsx index 50a39ffe7c..92f9ae6800 100644 --- a/web/app/components/base/button/add-button.tsx +++ b/web/app/components/base/button/add-button.tsx @@ -13,7 +13,7 @@ const AddButton: FC = ({ onClick, }) => { return ( -
+
) diff --git a/web/app/components/base/button/sync-button.spec.tsx b/web/app/components/base/button/sync-button.spec.tsx index eeaf60d46e..8876229c28 100644 --- a/web/app/components/base/button/sync-button.spec.tsx +++ b/web/app/components/base/button/sync-button.spec.tsx @@ -13,9 +13,9 @@ describe('SyncButton', () => { }) it('should render a refresh icon', () => { - const { container } = render() - const svg = container.querySelector('span') - expect(svg).toBeInTheDocument() + render() + const iconSpan = screen.getByTestId('sync-button').querySelector('span') + expect(iconSpan).toBeInTheDocument() }) }) @@ -38,7 +38,7 @@ describe('SyncButton', () => { it('should call onClick when clicked', () => { const onClick = vi.fn() render() - const clickableDiv = screen.getByTestId('sync-button')! + const clickableDiv = screen.getByTestId('sync-button') fireEvent.click(clickableDiv) expect(onClick).toHaveBeenCalledTimes(1) }) @@ -46,7 +46,7 @@ describe('SyncButton', () => { it('should call onClick multiple times on repeated clicks', () => { const onClick = vi.fn() render() - const clickableDiv = screen.getByTestId('sync-button')! + const clickableDiv = screen.getByTestId('sync-button') fireEvent.click(clickableDiv) fireEvent.click(clickableDiv) fireEvent.click(clickableDiv) diff --git a/web/app/components/base/carousel/index.spec.tsx b/web/app/components/base/carousel/index.spec.tsx index 06434a51aa..6bce414ee7 100644 --- a/web/app/components/base/carousel/index.spec.tsx +++ b/web/app/components/base/carousel/index.spec.tsx @@ -1,7 +1,6 @@ import type { Mock } from 'vitest' import { act, fireEvent, render, screen } from '@testing-library/react' import useEmblaCarousel from 'embla-carousel-react' -import { beforeEach, describe, expect, it, vi } from 'vitest' import { Carousel, useCarousel } from './index' vi.mock('embla-carousel-react', () => ({ @@ -52,7 +51,9 @@ const createMockEmblaApi = (): MockEmblaApi => ({ }) const emitEmblaEvent = (event: EmblaEventName, api: MockEmblaApi | undefined = mockApi) => { - listeners[event].forEach(callback => callback(api)) + listeners[event].forEach((callback) => { + callback(api) + }) } const renderCarouselWithControls = (orientation: 'horizontal' | 'vertical' = 'horizontal') => { @@ -60,6 +61,8 @@ const renderCarouselWithControls = (orientation: 'horizontal' | 'vertical' = 'ho Slide 1 + Slide 2 + Slide 3 Prev Next @@ -68,6 +71,13 @@ const renderCarouselWithControls = (orientation: 'horizontal' | 'vertical' = 'ho ) } +const mockPlugin = () => ({ + name: 'mock', + options: {}, + init: vi.fn(), + destroy: vi.fn(), +}) + describe('Carousel', () => { beforeEach(() => { vi.clearAllMocks() @@ -90,22 +100,25 @@ describe('Carousel', () => { expect(screen.getByRole('region')).toHaveAttribute('aria-roledescription', 'carousel') expect(screen.getByTestId('carousel-content')).toHaveClass('flex') - expect(screen.getByRole('group')).toHaveAttribute('aria-roledescription', 'slide') + screen.getAllByRole('group').forEach((slide) => { + expect(slide).toHaveAttribute('aria-roledescription', 'slide') + }) }) }) // Props should be translated into Embla options and visible layout. describe('Props', () => { it('should configure embla with horizontal axis when orientation is omitted', () => { + const plugin = mockPlugin() render( - + , ) expect(mockedUseEmblaCarousel).toHaveBeenCalledWith( { loop: true, axis: 'x' }, - ['plugin-marker'], + [plugin], ) }) diff --git a/web/app/components/base/drawer/index.tsx b/web/app/components/base/drawer/index.tsx index a145f9a64d..ab01a4114d 100644 --- a/web/app/components/base/drawer/index.tsx +++ b/web/app/components/base/drawer/index.tsx @@ -80,7 +80,18 @@ export default function Drawer({ )} {showClose && ( - + { + if (e.key === 'Enter' || e.key === ' ') + onClose() + }} + role="button" + tabIndex={0} + aria-label={t('operation.close', { ns: 'common' })} + data-testid="close-icon" + /> )}
diff --git a/web/app/components/base/float-right-container/index.spec.tsx b/web/app/components/base/float-right-container/index.spec.tsx index 51713cc527..4cf87b189c 100644 --- a/web/app/components/base/float-right-container/index.spec.tsx +++ b/web/app/components/base/float-right-container/index.spec.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render, screen } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import FloatRightContainer from './index' describe('FloatRightContainer', () => { @@ -94,7 +94,47 @@ describe('FloatRightContainer', () => { const closeIcon = screen.getByTestId('close-icon') expect(closeIcon).toBeInTheDocument() - fireEvent.click(closeIcon!) + await userEvent.click(closeIcon) + + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('should call onClose when close is done using escape key', async () => { + const onClose = vi.fn() + render( + +
Closable content
+
, + ) + + const closeIcon = screen.getByTestId('close-icon') + closeIcon.focus() + await userEvent.keyboard('{Enter}') + + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('should call onClose when close is done using space key', async () => { + const onClose = vi.fn() + render( + +
Closable content
+
, + ) + + const closeIcon = screen.getByTestId('close-icon') + closeIcon.focus() + await userEvent.keyboard(' ') expect(onClose).toHaveBeenCalledTimes(1) }) @@ -129,8 +169,9 @@ describe('FloatRightContainer', () => { isOpen={true} onClose={vi.fn()} title="Empty mobile panel" - children={undefined} - />, + > + {undefined} + , ) expect(await screen.findByRole('dialog')).toBeInTheDocument() diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index c1b87efac3..285cef2018 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -1342,12 +1342,6 @@ }, "react/no-nested-component-definitions": { "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - }, - "tailwindcss/no-unnecessary-whitespace": { - "count": 1 } }, "app/components/base/button/add-button.stories.tsx": {