diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d762e4b8a9..9653c66478b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,9 +185,6 @@ catalogs: '@egoist/tailwindcss-icons': specifier: 1.9.2 version: 1.9.2 - '@emoji-mart/data': - specifier: 1.2.1 - version: 1.2.1 '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.8.1 version: 4.8.1 @@ -464,9 +461,6 @@ catalogs: embla-carousel-react: specifier: 8.6.0 version: 8.6.0 - emoji-mart: - specifier: 5.6.0 - version: 5.6.0 es-toolkit: specifier: 1.52.0 version: 1.52.0 @@ -536,6 +530,9 @@ catalogs: foxact: specifier: 0.3.11 version: 0.3.11 + frimousse: + specifier: 0.3.0 + version: 0.3.0 fuse.js: specifier: 7.5.0 version: 7.5.0 @@ -1360,9 +1357,6 @@ importers: '@base-ui/react': specifier: 'catalog:' version: 1.8.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@emoji-mart/data': - specifier: 'catalog:' - version: 1.2.1 '@floating-ui/react': specifier: 'catalog:' version: 0.27.20(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -1486,9 +1480,6 @@ importers: embla-carousel-react: specifier: 'catalog:' version: 8.6.0(react@19.3.0) - emoji-mart: - specifier: 'catalog:' - version: 5.6.0 es-toolkit: specifier: 'catalog:' version: 1.52.0 @@ -1498,6 +1489,9 @@ importers: foxact: specifier: 'catalog:' version: 0.3.11(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + frimousse: + specifier: 'catalog:' + version: 0.3.0(@typescript/typescript6@6.0.2)(react@19.3.0) fuse.js: specifier: 'catalog:' version: 7.5.0 @@ -2177,9 +2171,6 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@emoji-mart/data@1.2.1': - resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} - '@es-joy/jsdoccomment@0.92.0': resolution: {integrity: sha512-WivZqV5jPTYDQJgIMp0hTsyQESKYNY6yCJr4b0l84tYtQntkAMkc6zARh2vlRQyeg4aYHPUkCUDpvr2IWAiGpQ==} engines: {node: ^22.22.2 || >=24.15.0} @@ -6346,9 +6337,6 @@ packages: embla-carousel@8.6.0: resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} - emoji-mart@5.6.0: - resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} - emoji-regex-xs@2.0.1: resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} engines: {node: '>=10.0.0'} @@ -6810,6 +6798,15 @@ packages: react-dom: optional: true + frimousse@0.3.0: + resolution: {integrity: sha512-kO6LMoKY/cLAYEhXXtqLRaLIE6L/DagpFPrUZaLv3LsUa1/8Iza3HhwZcgN8eZ+weXnhv69eoclNUPohcCa/IQ==} + peerDependencies: + react: ^18 || ^19 + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -10193,8 +10190,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emoji-mart/data@1.2.1': {} - '@es-joy/jsdoccomment@0.92.0': dependencies: '@types/estree': 1.0.9 @@ -13962,8 +13957,6 @@ snapshots: embla-carousel@8.6.0: {} - emoji-mart@5.6.0: {} - emoji-regex-xs@2.0.1: {} empathic@2.0.1: {} @@ -14584,6 +14577,12 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) + frimousse@0.3.0(@typescript/typescript6@6.0.2)(react@19.3.0): + dependencies: + react: 19.3.0 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' + fs-constants@1.0.0: optional: true @@ -18102,7 +18101,6 @@ time: '@chromatic-com/storybook@5.3.1': '2026-09-02T13:43:27.595Z' '@cucumber/cucumber@13.2.1': '2026-08-04T07:20:28.681Z' '@egoist/tailwindcss-icons@1.9.2': '2026-01-31T10:48:44.594Z' - '@emoji-mart/data@1.2.1': '2024-04-25T15:36:14.556Z' '@eslint-community/eslint-plugin-eslint-comments@4.8.1': '2026-09-12T22:48:47.148Z' '@eslint-react/eslint-plugin@5.19.1': '2026-09-14T17:56:33.726Z' '@eslint/markdown@8.0.3': '2026-07-01T13:28:44.339Z' @@ -18196,7 +18194,6 @@ time: embla-carousel-autoplay@8.6.0: '2025-04-04T17:37:46.303Z' embla-carousel-fade@8.6.0: '2025-04-04T17:37:50.278Z' embla-carousel-react@8.6.0: '2025-04-04T17:37:53.976Z' - emoji-mart@5.6.0: '2024-04-25T14:22:21.440Z' es-toolkit@1.52.0: '2026-08-28T07:22:17.687Z' eslint-config-flat-gitignore@2.4.0: '2026-08-27T07:24:43.063Z' eslint-markdown@0.17.0: '2026-09-16T06:20:36.564Z' @@ -18220,6 +18217,7 @@ time: eventsource-parser@4.1.1: '2026-09-15T18:30:29.195Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' foxact@0.3.11: '2026-09-05T12:25:54.102Z' + frimousse@0.3.0: '2025-07-15T10:44:32.958Z' fuse.js@7.5.0: '2026-07-13T17:23:36.705Z' happy-dom@20.14.5: '2026-09-12T00:07:47.688Z' hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e6160a92379..04ff09c4ba0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -68,7 +68,6 @@ catalog: '@chromatic-com/storybook': 5.3.1 '@cucumber/cucumber': 13.2.1 '@egoist/tailwindcss-icons': 1.9.2 - '@emoji-mart/data': 1.2.1 '@eslint-community/eslint-plugin-eslint-comments': 4.8.1 '@eslint-react/eslint-plugin': 5.19.1 '@eslint/markdown': 8.0.3 @@ -162,7 +161,6 @@ catalog: embla-carousel-autoplay: 8.6.0 embla-carousel-fade: 8.6.0 embla-carousel-react: 8.6.0 - emoji-mart: 5.6.0 es-toolkit: 1.52.0 eslint: 10.10.0 eslint-config-flat-gitignore: 2.4.0 @@ -186,6 +184,7 @@ catalog: eventsource-parser: 4.1.1 fast-deep-equal: 3.1.3 foxact: 0.3.11 + frimousse: 0.3.0 fuse.js: 7.5.0 happy-dom: 20.14.5 hast-util-to-jsx-runtime: 2.3.6 diff --git a/vite.config.ts b/vite.config.ts index 8d47f7528bb..dee3d7eccca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,6 +40,8 @@ const generatedIgnores = [ 'web/public/embed.min.js', 'web/public/pdf.worker.min.mjs', 'web/public/vs/**', + // Vendored Emojibase JSON is served verbatim. + 'web/public/emoji/emojibase-*/**', ] const formatterUnstableInputs = ['web/app/components/develop/template/*.mdx'] diff --git a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx index acb21be3f6a..48424a0322c 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx @@ -218,7 +218,7 @@ describe('dataset-config/card-item', () => { const iconElement = nameElement.parentElement?.firstElementChild as HTMLElement expect(iconElement)!.toHaveStyle({ background: '#FFF4ED' }) - expect(iconElement.querySelector('em-emoji'))!.toHaveAttribute('id', '๐Ÿ“™') + expect(iconElement).toHaveTextContent('๐Ÿ“™') }) it('should apply mask overlay on mobile when drawer is open', async () => { diff --git a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx index 4b5ca839428..5169868b53a 100644 --- a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx +++ b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal.spec.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { mockEmojiData } from '@/test/emoji-picker' import ExternalDataToolModal from '../external-data-tool-modal' const mockToastError = vi.fn() @@ -131,9 +132,9 @@ describe('ExternalDataToolModal', () => { fireEvent.click(screen.getByText('pick-extension')) fireEvent.click(screen.getByText('open-emoji-picker')) await waitFor(() => { - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) - const emojiButton = document.querySelector('em-emoji')?.closest('button') + const emojiButton = await screen.findByRole('gridcell', { name: 'Grinning face' }) expect(emojiButton).toBeTruthy() fireEvent.click(emojiButton!) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) @@ -228,3 +229,5 @@ describe('ExternalDataToolModal', () => { expect(mockOnCancel).toHaveBeenCalled() }) }) + +mockEmojiData() diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index 3bbe0d7c8a1..68af8670fe4 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -4,6 +4,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { useRouter } from '@/next/navigation' import { renderWithConsoleQuery } from '@/test/console/query-data' +import { mockEmojiData } from '@/test/emoji-picker' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { trackCreateApp } from '@/utils/create-app-tracking' @@ -265,12 +266,12 @@ describe('CreateAppModal', () => { fireEvent.click(screen.getByText('app.types.chatbot')) fireEvent.click(screen.getByText('open-icon-picker')) await waitFor(() => { - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) fireEvent.change(screen.getByPlaceholderText('app.newApp.appNamePlaceholder'), { target: { value: 'Keyboard App' }, @@ -309,14 +310,14 @@ describe('CreateAppModal', () => { fireEvent.click(screen.getByText('open-icon-picker')) await waitFor(() => { - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() ahooksMocks.keyPressHandlers.at(-1)?.() @@ -379,3 +380,5 @@ describe('CreateAppModal', () => { }) }) }) + +mockEmojiData() diff --git a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx index 1f5adc98622..f36ee285ac1 100644 --- a/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/duplicate-modal/__tests__/index.spec.tsx @@ -41,7 +41,7 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({
+ ), +} + function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPickerInnerProps) { const { t } = useTranslation() - const { categories } = data as EmojiMartData - const [selectedEmoji, setSelectedEmoji] = useState(emoji || '') + const [selectedEmoji, setSelectedEmoji] = useState(() => (emoji ? resolveEmoji(emoji) : '')) const [selectedBackground, setSelectedBackground] = useState(background || defaultEmojiBackground) const [showStyleColors, setShowStyleColors] = useState(!!emoji) - const [searchedEmojis, setSearchedEmojis] = useState([]) - const [isSearching, setIsSearching] = useState(false) const styleColorsLabelId = React.useId() const handleEmojiSelect = (emoji: string) => { @@ -44,88 +62,43 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick } return ( -
-
- - $['operation.search'], { ns: 'common' })} - placeholder="Search emojis..." - onValueChange={async (value) => { - if (value === '') { - setIsSearching(false) - } else { - setIsSearching(true) - const emojis = await searchEmoji(value) - setSearchedEmojis(emojis) - } - }} - /> - - - -
- - -
- {isSearching && ( - <> -
-

Search

-
- {searchedEmojis.map((emoji: string, index: number) => { - return ( - - ) - })} -
-
- - )} - - {categories.map((category, index: number) => { - return ( -
-

{category.id}

-
- {category.emojis.map((emoji, index: number) => { - return ( - - ) - })} -
-
- ) - })} -
+
+ handleEmojiSelect(emoji)} + > +
+ + $['operation.search'], { ns: 'common' })} + placeholder={t(($) => $['operation.search'], { ns: 'common' })} + className="h-8 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-hover" + /> + + + +
+ + + + {t(($) => $.loading, { ns: 'common' })} + + + {t(($) => $.noData, { ns: 'common' })} + + + +
{/* Color Select */} -
+

Choose Style

@@ -134,7 +107,7 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick type="button" aria-labelledby={styleColorsLabelId} aria-expanded="true" - className="i-heroicons-chevron-down size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary" + className="i-heroicons-chevron-down size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary focus-visible:outline-2 focus-visible:outline-offset-2" onClick={() => setShowStyleColors(!showStyleColors)} /> ) : ( @@ -142,13 +115,13 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick type="button" aria-labelledby={styleColorsLabelId} aria-expanded="false" - className="i-heroicons-chevron-up size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary" + className="i-heroicons-chevron-up size-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary focus-visible:outline-2 focus-visible:outline-offset-2" onClick={() => setShowStyleColors(!showStyleColors)} /> )}
{showStyleColors && ( -
+
{backgroundColors.map((color) => { return ( ) diff --git a/web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx b/web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx new file mode 100644 index 00000000000..500ec454afe --- /dev/null +++ b/web/app/components/base/emoji-picker/__tests__/Inner.browser.spec.tsx @@ -0,0 +1,83 @@ +import { page, userEvent } from 'vite-plus/test/browser' +import { render } from 'vitest-browser-react' +import AppIconPicker from '../../app-icon-picker' +import { backgroundColors } from '../constants' +import EmojiPickerInner from '../Inner' + +// Real layout is needed to verify virtualized rows remain reachable using the keyboard. +it('selects a searched emoji with the keyboard and keeps style controls usable', async () => { + const onSelect = vi.fn() + await render( +
+ +
, + ) + const search = page.getByRole('searchbox') + await search.fill('rabbit face') + await expect.element(page.getByRole('gridcell', { name: 'Rabbit face' })).toBeVisible() + await userEvent.keyboard('{Enter}') + await expect.poll(() => onSelect.mock.lastCall).toEqual(['๐Ÿฐ', '#FFEAD5']) + await page.getByRole('button', { name: '#E4FBCC' }).click() + expect(onSelect).toHaveBeenLastCalledWith('๐Ÿฐ', '#E4FBCC') + await search.fill('') + await expect + .element(page.getByRole('gridcell', { name: 'Grinning face', exact: true })) + .toBeVisible() + await search.click() + await userEvent.keyboard( + '{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{Enter}', + ) + expect(onSelect).toHaveBeenCalledTimes(3) +}) + +// The production dialog is narrower than the old fixed-width eight-column grid. +// Verify every edge option is visible and clickable, rather than hiding overflow. +it('fits emoji and style options inside the app icon dialog', async () => { + const originalFontSize = document.documentElement.style.fontSize + document.documentElement.style.fontSize = '16px' + onTestFinished(() => { + document.documentElement.style.fontSize = originalFontSize + }) + const onSelect = vi.fn() + await render( + {}} + onSelect={onSelect} + initialEmoji={{ icon: 'robot_face' }} + />, + ) + const dialog = page.getByRole('dialog') + const firstEmoji = page.getByRole('gridcell', { name: 'Grinning face', exact: true }) + await expect.element(firstEmoji).toBeVisible() + const bounds = dialog.element().getBoundingClientRect() + const options = [ + ...dialog.element().querySelectorAll('[role="row"] [role="gridcell"]'), + ...backgroundColors.map((color) => + page.getByRole('button', { name: color, exact: true }).element(), + ), + ] + for (const option of options) { + const rect = option.getBoundingClientRect() + expect(rect.left).toBeGreaterThanOrEqual(bounds.left) + expect(rect.right).toBeLessThanOrEqual(bounds.right) + } + const footerTop = page + .getByRole('button', { name: 'app.iconPicker.ok', exact: true }) + .element() + .getBoundingClientRect().top + for (const color of backgroundColors) { + expect( + page.getByRole('button', { name: color, exact: true }).element().getBoundingClientRect() + .bottom, + ).toBeLessThanOrEqual(footerTop) + } + await page.getByRole('gridcell', { name: 'Face with tears of joy', exact: true }).click() + await page.getByRole('button', { name: backgroundColors.at(-1)!, exact: true }).click() + await page.getByRole('button', { name: 'app.iconPicker.ok', exact: true }).click() + expect(onSelect).toHaveBeenCalledWith({ + type: 'emoji', + icon: '๐Ÿ˜‚', + background: backgroundColors.at(-1), + }) +}) diff --git a/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx b/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx index c5321def681..74b67300aca 100644 --- a/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx +++ b/web/app/components/base/emoji-picker/__tests__/Inner.spec.tsx @@ -1,181 +1,55 @@ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { mockEmojiData } from '@/test/emoji-picker' import EmojiPickerInner from '../Inner' -vi.mock('@emoji-mart/data', () => ({ - default: { - categories: [ - { - id: 'nature', - emojis: ['rabbit', 'bear'], - }, - { - id: 'food', - emojis: ['apple', 'orange'], - }, - ], - }, -})) +vi.mock('@/utils/var', () => ({ basePath: '/console' })) -vi.mock('emoji-mart', () => ({ - init: vi.fn(), -})) - -vi.mock('@/utils/emoji', () => ({ - searchEmoji: vi.fn().mockResolvedValue(['dog', 'cat']), -})) +mockEmojiData() describe('EmojiPickerInner', () => { - const mockOnSelect = vi.fn() - - beforeEach(() => { - vi.clearAllMocks() - // Define the custom element to avoid "Unknown custom element" warnings - if (!customElements.get('em-emoji')) { - customElements.define( - 'em-emoji', - class extends HTMLElement { - static get observedAttributes() { - return ['id'] - } - }, - ) - } + it('selects Unicode from self-hosted data and preserves the chosen background', async () => { + const onSelect = vi.fn() + render() + fireEvent.click(await screen.findByRole('gridcell', { name: 'Grinning face' })) + expect(onSelect).toHaveBeenLastCalledWith('๐Ÿ˜€', '#FFEAD5') + fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) + expect(onSelect).toHaveBeenLastCalledWith('๐Ÿ˜€', '#E4FBCC') + expect(fetch).toHaveBeenCalledTimes(2) + expect(fetch).toHaveBeenCalledWith( + '/console/emoji/emojibase-17.0.0/en/data.json', + expect.any(Object), + ) + expect(fetch).toHaveBeenCalledWith( + '/console/emoji/emojibase-17.0.0/en/messages.json', + expect.any(Object), + ) }) - describe('Rendering', () => { - it('renders initial categories and emojis correctly', () => { - render() - - expect(screen.getByText('nature'))!.toBeInTheDocument() - expect(screen.getByText('food'))!.toBeInTheDocument() - expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() - }) - - it('initializes selected emoji and background when provided', () => { - render() - - expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - expect(mockOnSelect).not.toHaveBeenCalled() - }) + it('filters search, selects a result, shows empty state and restores categories on clear', async () => { + const onSelect = vi.fn() + render() + await screen.findByRole('gridcell', { name: 'Grinning face' }) + const search = screen.getByRole('searchbox') + fireEvent.change(search, { target: { value: 'rabbit' } }) + fireEvent.click(await screen.findByRole('gridcell', { name: 'Rabbit face' })) + expect(onSelect).toHaveBeenLastCalledWith('๐Ÿฐ', '#FFEAD5') + fireEvent.change(search, { target: { value: 'zzzz-no-emoji' } }) + await screen.findByText('common.noData') + fireEvent.change(search, { target: { value: '' } }) + await screen.findByRole('gridcell', { name: 'Grinning face' }) }) - describe('User Interactions', () => { - it('calls searchEmoji and displays results when typing in search input', async () => { - render() - const searchInput = screen.getByPlaceholderText('Search emojis...') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: 'anim' } }) - }) - - await waitFor(() => { - expect(screen.getByText('Search'))!.toBeInTheDocument() - }) - - const searchSection = screen.getByText('Search').parentElement - expect(searchSection?.querySelectorAll('em-emoji').length).toBe(2) - }) - - it('updates selected emoji and calls onSelect when an emoji is clicked', async () => { - render() - const emojiButton = screen.getByRole('button', { name: 'rabbit' }) - - await act(async () => { - fireEvent.click(emojiButton) - }) - - expect(mockOnSelect).toHaveBeenCalledWith('rabbit', expect.any(String)) - }) - - it('toggles style colors display when clicking the chevron', async () => { - render() - - expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument() - - const toggleButton = screen.getByRole('button', { name: 'Choose Style' }) - expect(toggleButton)!.toBeInTheDocument() - - await act(async () => { - fireEvent.click(toggleButton!) - }) - - expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - const colorOptions = document.querySelectorAll('[style^="background:"]') - expect(colorOptions.length).toBeGreaterThan(0) - }) - - it('updates background color and calls onSelect when a color is clicked', async () => { - render() - - const toggleButton = screen.getByRole('button', { name: 'Choose Style' }) - await act(async () => { - fireEvent.click(toggleButton!) - }) - - const emojiButton = screen.getByRole('button', { name: 'rabbit' }) - await act(async () => { - fireEvent.click(emojiButton) - }) - - mockOnSelect.mockClear() - - const colorOptions = screen.getAllByRole('button', { name: /^#/ }) - await act(async () => { - fireEvent.click(colorOptions[1]!) - }) - - expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC') - }) - - it('updates selected emoji when clicking a search result', async () => { - render() - const searchInput = screen.getByPlaceholderText('Search emojis...') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: 'anim' } }) - }) - - await screen.findByText('Search') - - const searchEmoji = screen.getByRole('button', { name: 'dog' }) - await act(async () => { - fireEvent.click(searchEmoji) - }) - - expect(mockOnSelect).toHaveBeenCalledWith('dog', expect.any(String)) - }) - - it('toggles style colors display back and forth', async () => { - render() - - const toggleButton = screen.getByRole('button', { name: 'Choose Style' }) - - await act(async () => { - fireEvent.click(toggleButton!) - }) - expect(screen.getByText('Choose Style'))!.toBeInTheDocument() - - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'Choose Style' })) - }) - expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument() - }) - - it('clears search results when input is cleared', async () => { - render() - const searchInput = screen.getByPlaceholderText('Search emojis...') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: 'anim' } }) - }) - - await screen.findByText('Search') - - await act(async () => { - fireEvent.change(searchInput, { target: { value: '' } }) - }) - - expect(screen.queryByText('Search')).not.toBeInTheDocument() - }) + it('resolves a saved ID in style previews and only emits after a user action', async () => { + const onSelect = vi.fn() + render() + const color = screen.getByRole('button', { name: '#E4FBCC' }) + expect(color).toHaveTextContent('๐Ÿฐ') + expect(onSelect).not.toHaveBeenCalled() + fireEvent.click(color) + expect(onSelect).toHaveBeenCalledWith('๐Ÿฐ', '#E4FBCC') + fireEvent.click(screen.getByRole('button', { name: 'Choose Style' })) + await waitFor(() => + expect(screen.queryByRole('button', { name: '#E4FBCC' })).not.toBeInTheDocument(), + ) }) }) diff --git a/web/app/components/base/emoji-picker/__tests__/index.spec.tsx b/web/app/components/base/emoji-picker/__tests__/index.spec.tsx index 7a36b5a779f..b0d8d41a7c7 100644 --- a/web/app/components/base/emoji-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/emoji-picker/__tests__/index.spec.tsx @@ -1,28 +1,8 @@ import { act, fireEvent, render, screen } from '@testing-library/react' +import { mockEmojiData } from '@/test/emoji-picker' import EmojiPicker from '../index' -vi.mock('@emoji-mart/data', () => ({ - default: { - categories: [ - { - id: 'category1', - name: 'Category 1', - emojis: ['emoji1', 'emoji2'], - }, - ], - }, -})) - -vi.mock('emoji-mart', () => ({ - init: vi.fn(), - SearchIndex: { - search: vi.fn().mockResolvedValue([{ skins: [{ native: '๐Ÿ”' }] }]), - }, -})) - -vi.mock('@/utils/emoji', () => ({ - searchEmoji: vi.fn().mockResolvedValue(['๐Ÿ”']), -})) +mockEmojiData() describe('EmojiPicker', () => { const mockOnSelect = vi.fn() @@ -43,7 +23,7 @@ describe('EmojiPicker', () => { render() }) expect(screen.getByRole('dialog', { name: /Emoji/i }))!.toBeInTheDocument() - expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search'))!.toBeInTheDocument() expect(screen.getByText(/Cancel/i))!.toBeInTheDocument() expect(screen.getByText(/OK/i))!.toBeInTheDocument() }) @@ -63,9 +43,7 @@ describe('EmojiPicker', () => { render() }) - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: 'emoji1' })) - }) + fireEvent.click(await screen.findByRole('gridcell', { name: 'Grinning face' })) const okButton = screen.getByText(/OK/i) expect(okButton.closest('button')).not.toBeDisabled() diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx index de26e64e577..24c28a0071b 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx @@ -5,6 +5,7 @@ import { fireEvent, screen, render as testingLibraryRender, waitFor } from '@tes import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { ChunkingMode } from '@/models/datasets' +import { mockEmojiData } from '@/test/emoji-picker' import EditPipelineInfo from '../edit-pipeline-info' const render = (ui: ReactElement) => @@ -282,7 +283,7 @@ describe('EditPipelineInfo', () => { // Open icon picker fireEvent.click(getIconButton()) - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) it('should save correct icon_info when starting with image icon type', async () => { @@ -346,13 +347,13 @@ describe('EditPipelineInfo', () => { // Open picker fireEvent.click(getIconButton()) - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() // Close without selection - should revert to original image icon fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) }) @@ -370,7 +371,7 @@ describe('EditPipelineInfo', () => { // Open picker and select emoji fireEvent.click(getIconButton()) - const emojiButton = document.querySelector('em-emoji')?.closest('button') + const emojiButton = await screen.findByRole('gridcell', { name: 'Grinning face' }) expect(emojiButton).toBeTruthy() fireEvent.click(emojiButton!) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) @@ -407,14 +408,14 @@ describe('EditPipelineInfo', () => { describe('AppIconPicker', () => { it('should not show picker initially', () => { render() - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) it('should open picker when icon is clicked', () => { render() fireEvent.click(getIconButton()) - expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) it('should close picker and update icon when emoji style is selected', async () => { @@ -425,7 +426,7 @@ describe('EditPipelineInfo', () => { fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) }) @@ -445,7 +446,7 @@ describe('EditPipelineInfo', () => { fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ })) await waitFor(() => { - expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument() }) }) @@ -488,7 +489,7 @@ describe('EditPipelineInfo', () => { render() fireEvent.click(getIconButton()) - const emojiButton = document.querySelector('em-emoji')?.closest('button') + const emojiButton = await screen.findByRole('gridcell', { name: 'Grinning face' }) expect(emojiButton).toBeTruthy() fireEvent.click(emojiButton!) fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' })) @@ -577,3 +578,5 @@ describe('EditPipelineInfo', () => { }) }) }) + +mockEmojiData() diff --git a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx index 4257199d6f3..52ed523e6b8 100644 --- a/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/rename-modal/__tests__/index.spec.tsx @@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { mockEmojiData } from '@/test/emoji-picker' import RenameDatasetModal from '../index' const { mockToast } = vi.hoisted(() => { @@ -42,7 +43,7 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({ let selectedBackground = '#FFEAD5' return (
- +