mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
refactor(web): replace emoji-mart with self-hosted Frimousse picker (#42469)
This commit is contained in:
1 parent
6d88df5d7c
commit
228541c15d
39 files changed
+2376
-472
No files matched your search
Generated
+22
-24
@@ -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'
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
+1
-1
@@ -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 () => {
|
||||
|
||||
+5
-2
@@ -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()
|
||||
@@ -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()
|
||||
@@ -41,7 +41,7 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({
|
||||
<div>
|
||||
<input placeholder="Search emojis..." />
|
||||
<button type="button" onClick={() => {}}>
|
||||
<em-emoji />
|
||||
<span>😀</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -256,7 +256,7 @@ describe('DuplicateAppModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
})
|
||||
const emojiButton = document.querySelector('em-emoji')?.closest('button')
|
||||
const emojiButton = screen.getByText('😀').closest('button')
|
||||
expect(emojiButton).toBeTruthy()
|
||||
await user.click(emojiButton!)
|
||||
await user.click(screen.getByRole('button', { name: '#E4FBCC', hidden: true }))
|
||||
|
||||
@@ -52,7 +52,7 @@ const APIKeyInfoPanel: FC = () => {
|
||||
isCloud ? 'flex h-8 items-center space-x-1' : 'mb-6 leading-8',
|
||||
)}
|
||||
>
|
||||
{isCloud && <em-emoji id="😀" />}
|
||||
{isCloud && <span>😀</span>}
|
||||
{isCloud ? (
|
||||
<div>
|
||||
{t(($) => $['apiKeyInfo.cloud.trial.title'], {
|
||||
|
||||
@@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { mockEmojiData } from '@/test/emoji-picker'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import SwitchAppModal from '../index'
|
||||
|
||||
@@ -295,13 +296,13 @@ describe('SwitchAppModal', () => {
|
||||
|
||||
await user.click(screen.getByText('open-icon-picker'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||
await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: 'app.switchStart' }))
|
||||
|
||||
@@ -325,13 +326,13 @@ describe('SwitchAppModal', () => {
|
||||
|
||||
await user.click(screen.getByText('open-icon-picker'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
await user.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()
|
||||
|
||||
await user.click(screen.getByText('app.removeOriginal'))
|
||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeInTheDocument()
|
||||
@@ -398,3 +399,5 @@ describe('SwitchAppModal', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mockEmojiData()
|
||||
@@ -544,10 +544,10 @@ describe('AppCard', () => {
|
||||
it('should render app icon', () => {
|
||||
// AppIcon component renders the emoji icon from app data
|
||||
const { container } = render(<AppCard app={mockApp} />)
|
||||
const emojiIcon = container.querySelector(`em-emoji[id="${mockApp.icon}"]`)
|
||||
const emojiIcon = screen.getByText(mockApp.icon!)
|
||||
const imageIcon = container.querySelector('img')
|
||||
expect(emojiIcon || imageIcon).toBeTruthy()
|
||||
expect(emojiIcon?.parentElement).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(emojiIcon).toHaveAttribute('aria-hidden', 'true')
|
||||
})
|
||||
|
||||
it('should treat a redundant image icon as decorative', () => {
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('ImportFromMarketplaceTemplateModal', () => {
|
||||
|
||||
expect(screen.getByText('Human Input: Writing Assistant')).toBeInTheDocument()
|
||||
expect(screen.queryByText('technologist')).not.toBeInTheDocument()
|
||||
expect(document.querySelector('em-emoji')?.parentElement).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(screen.getByText('🧑💻')).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: /marketplace\.template\.modalTitle/ }),
|
||||
).toBeInTheDocument()
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import data from '@emoji-mart/data'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { init } from 'emoji-mart'
|
||||
|
||||
init({ data })
|
||||
import { resolveEmoji } from '@/utils/emoji'
|
||||
|
||||
type AnswerIconProps = {
|
||||
iconType?: AppIconType | null
|
||||
@@ -32,10 +29,8 @@ const AnswerIcon: FC<AnswerIconProps> = ({ iconType, icon, background, imageUrl
|
||||
<div className={wrapperClassName} style={{ background: background || '#D5F5F6' }}>
|
||||
{isValidImageIcon ? (
|
||||
<img src={imageUrl} className="size-full rounded-full" alt="answer icon" />
|
||||
) : icon && icon !== '' ? (
|
||||
<em-emoji id={icon} />
|
||||
) : (
|
||||
<em-emoji id="🤖" />
|
||||
resolveEmoji(icon)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Area } from 'react-easy-crop'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { mockEmojiData } from '@/test/emoji-picker'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import AppIconPicker from '../index'
|
||||
import 'vitest-canvas-mock'
|
||||
@@ -109,10 +110,6 @@ vi.mock('../../image-uploader/hooks', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/emoji', () => ({
|
||||
searchEmoji: vi.fn().mockResolvedValue(['grinning', 'sunglasses']),
|
||||
}))
|
||||
|
||||
describe('AppIconPicker', () => {
|
||||
const originalCreateElement = document.createElement.bind(document)
|
||||
const originalCreateObjectURL = globalThis.URL.createObjectURL
|
||||
@@ -205,11 +202,11 @@ describe('AppIconPicker', () => {
|
||||
it('should call onSelect with emoji data after emoji selection', async () => {
|
||||
const { onSelect } = renderPicker()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('em-emoji')?.closest('button'))!.toBeInTheDocument()
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByRole('gridcell', { name: 'Grinning face' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const firstEmoji = document.querySelector('em-emoji')?.closest('button')
|
||||
const firstEmoji = await screen.findByRole('gridcell', { name: 'Grinning face' })
|
||||
if (!firstEmoji) throw new Error('Could not find emoji option')
|
||||
|
||||
await userEvent.click(firstEmoji)
|
||||
@@ -371,3 +368,5 @@ describe('AppIconPicker', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mockEmojiData()
|
||||
@@ -1,25 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import data from '@emoji-mart/data'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiEditLine } from '@remixicon/react'
|
||||
import { useHover } from 'ahooks'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { init } from 'emoji-mart'
|
||||
import * as React from 'react'
|
||||
import { useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
init({ data })
|
||||
|
||||
const subscribeHydrationState = () => () => {}
|
||||
|
||||
const useIsHydrated = () =>
|
||||
useSyncExternalStore(
|
||||
subscribeHydrationState,
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
import { useRef } from 'react'
|
||||
import { resolveEmoji } from '@/utils/emoji'
|
||||
|
||||
type AppIconProps = {
|
||||
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
|
||||
@@ -113,9 +101,7 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
}) => {
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
const isDecorative = decorative && !onClick
|
||||
const emojiIcon = icon && icon !== '' ? icon : '🤖'
|
||||
const isHydrated = useIsHydrated()
|
||||
const Icon = isHydrated ? <em-emoji key={emojiIcon} id={emojiIcon} /> : emojiIcon
|
||||
const Icon = resolveEmoji(icon)
|
||||
const wrapperRef = useRef<HTMLSpanElement>(null)
|
||||
const isHovering = useHover(wrapperRef)
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
'use client'
|
||||
import type { EmojiMartData } from '@emoji-mart/data'
|
||||
import data from '@emoji-mart/data'
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group'
|
||||
import { init } from 'emoji-mart'
|
||||
import { InputGroup, InputGroupAddon } from '@langgenius/dify-ui/input-group'
|
||||
import { EmojiPicker } from 'frimousse'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { searchEmoji } from '@/utils/emoji'
|
||||
import { resolveEmoji } from '@/utils/emoji'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { backgroundColors, defaultEmojiBackground } from './constants'
|
||||
|
||||
init({ data })
|
||||
|
||||
type IEmojiPickerInnerProps = {
|
||||
emoji?: string
|
||||
background?: string
|
||||
@@ -21,15 +17,37 @@ type IEmojiPickerInnerProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const listComponents: NonNullable<React.ComponentProps<typeof EmojiPicker.List>['components']> = {
|
||||
CategoryHeader: ({ category, ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
className="bg-components-panel-bg px-3 pb-1 system-xs-medium-uppercase text-text-primary"
|
||||
>
|
||||
{category.label}
|
||||
</div>
|
||||
),
|
||||
Row: ({ children, ...props }) => (
|
||||
<div {...props} className="scroll-my-1 gap-1 px-3 pb-1">
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Emoji: ({ emoji, ...props }) => (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="flex h-10 min-w-0 flex-1 items-center justify-center rounded-lg text-xl focus-visible:outline-2 focus-visible:outline-offset-2 data-[active]:bg-state-base-hover"
|
||||
>
|
||||
{emoji.emoji}
|
||||
</button>
|
||||
),
|
||||
}
|
||||
|
||||
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<string[]>([])
|
||||
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 (
|
||||
<div className={cn(className, 'flex flex-col')}>
|
||||
<div className="flex w-full flex-col items-center px-3 pb-2">
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
type="search"
|
||||
aria-label={t(($) => $['operation.search'], { ns: 'common' })}
|
||||
placeholder="Search emojis..."
|
||||
onValueChange={async (value) => {
|
||||
if (value === '') {
|
||||
setIsSearching(false)
|
||||
} else {
|
||||
setIsSearching(true)
|
||||
const emojis = await searchEmoji(value)
|
||||
setSearchedEmojis(emojis)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<InputGroupAddon className="ps-3 pe-2">
|
||||
<MagnifyingGlassIcon className="size-5 text-text-quaternary" aria-hidden="true" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<Divider className="my-3" />
|
||||
|
||||
<div className="max-h-50 w-full overflow-x-hidden overflow-y-auto px-3">
|
||||
{isSearching && (
|
||||
<>
|
||||
<div key="category-search" className="flex flex-col">
|
||||
<p className="mb-1 system-xs-medium-uppercase text-text-primary">Search</p>
|
||||
<div className="grid size-full grid-cols-8 gap-1">
|
||||
{searchedEmojis.map((emoji: string, index: number) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={`emoji-search-${index}`}
|
||||
aria-label={emoji}
|
||||
className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
||||
onClick={() => {
|
||||
handleEmojiSelect(emoji)
|
||||
}}
|
||||
>
|
||||
<span className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||
<em-emoji id={emoji} />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{categories.map((category, index: number) => {
|
||||
return (
|
||||
<div key={`category-${index}`} className="flex flex-col">
|
||||
<p className="mb-1 system-xs-medium-uppercase text-text-primary">{category.id}</p>
|
||||
<div className="grid size-full grid-cols-8 gap-1">
|
||||
{category.emojis.map((emoji, index: number) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={`emoji-${index}`}
|
||||
aria-label={emoji}
|
||||
className="inline-flex size-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
||||
onClick={() => {
|
||||
handleEmojiSelect(emoji)
|
||||
}}
|
||||
>
|
||||
<span className="flex size-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||
<em-emoji id={emoji} />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className={cn(className, 'flex min-h-0 w-full min-w-0 flex-col')}>
|
||||
<EmojiPicker.Root
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
locale="en"
|
||||
columns={8}
|
||||
emojibaseUrl={`${basePath}/emoji/emojibase-17.0.0`}
|
||||
onEmojiSelect={({ emoji }) => handleEmojiSelect(emoji)}
|
||||
>
|
||||
<div className="px-3 pb-2">
|
||||
<InputGroup>
|
||||
<EmojiPicker.Search
|
||||
aria-label={t(($) => $['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"
|
||||
/>
|
||||
<InputGroupAddon className="ps-3 pe-2">
|
||||
<span
|
||||
className="i-heroicons-magnifying-glass size-5 text-text-quaternary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<Divider className="my-3" />
|
||||
<EmojiPicker.Viewport className="relative h-50 min-h-0 w-full overflow-x-hidden overflow-y-auto">
|
||||
<EmojiPicker.Loading className="block p-3 text-text-tertiary">
|
||||
{t(($) => $.loading, { ns: 'common' })}
|
||||
</EmojiPicker.Loading>
|
||||
<EmojiPicker.Empty className="block p-3 text-text-tertiary">
|
||||
{t(($) => $.noData, { ns: 'common' })}
|
||||
</EmojiPicker.Empty>
|
||||
<EmojiPicker.List components={listComponents} />
|
||||
</EmojiPicker.Viewport>
|
||||
</EmojiPicker.Root>
|
||||
|
||||
{/* Color Select */}
|
||||
<div className={cn('flex items-center justify-between p-3 pb-0')}>
|
||||
<div className={cn('flex shrink-0 items-center justify-between p-3 pb-0')}>
|
||||
<p id={styleColorsLabelId} className="mb-2 system-xs-medium-uppercase text-text-primary">
|
||||
Choose Style
|
||||
</p>
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showStyleColors && (
|
||||
<div className="grid w-full grid-cols-8 gap-1 px-3">
|
||||
<div className="grid w-full shrink-0 grid-cols-8 gap-1 px-3">
|
||||
{backgroundColors.map((color) => {
|
||||
return (
|
||||
<button
|
||||
@@ -156,10 +129,10 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick
|
||||
key={color}
|
||||
aria-label={color}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
'cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
'border-none bg-transparent p-0',
|
||||
'ring-components-input-border-hover ring-offset-1 hover:ring-1',
|
||||
'inline-flex size-10 items-center justify-center rounded-lg',
|
||||
'inline-flex h-10 w-full min-w-0 items-center justify-center rounded-lg',
|
||||
color === selectedBackground ? 'ring-1 ring-components-input-border-hover' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
@@ -167,10 +140,12 @@ function EmojiPickerInner({ emoji, background, onSelect, className }: IEmojiPick
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('flex size-8 items-center justify-center rounded-lg p-1')}
|
||||
className={cn(
|
||||
'flex size-8 max-w-full items-center justify-center rounded-lg p-1 text-xl',
|
||||
)}
|
||||
style={{ background: color }}
|
||||
>
|
||||
{selectedEmoji !== '' && <em-emoji id={selectedEmoji} />}
|
||||
{selectedEmoji}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
<div style={{ width: 380 }}>
|
||||
<EmojiPickerInner onSelect={onSelect} />
|
||||
</div>,
|
||||
)
|
||||
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(
|
||||
<AppIconPicker
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
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),
|
||||
})
|
||||
})
|
||||
@@ -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(<EmojiPickerInner onSelect={onSelect} />)
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
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(<EmojiPickerInner emoji="rabbit" background="#E4FBCC" onSelect={mockOnSelect} />)
|
||||
|
||||
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(<EmojiPickerInner onSelect={onSelect} />)
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
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(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
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(<EmojiPickerInner emoji="rabbit" onSelect={onSelect} />)
|
||||
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(),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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(<EmojiPicker open onOpenChange={mockOnOpenChange} />)
|
||||
})
|
||||
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(<EmojiPicker open onOpenChange={mockOnOpenChange} onSelect={mockOnSelect} />)
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
+12
-9
@@ -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(<EditPipelineInfo {...defaultProps} />)
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open picker when icon is clicked', () => {
|
||||
render(<EditPipelineInfo {...defaultProps} />)
|
||||
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(<EditPipelineInfo {...defaultProps} pipeline={createImagePipelineTemplate()} />)
|
||||
|
||||
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()
|
||||
@@ -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 (
|
||||
<div>
|
||||
<input placeholder="Search emojis..." />
|
||||
<input placeholder="common.operation.search" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="#E4FBCC"
|
||||
@@ -910,7 +911,7 @@ describe('RenameDatasetModal', () => {
|
||||
// Initially picker should not be visible
|
||||
// Initially picker should not be visible
|
||||
// Initially picker should not be visible
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
|
||||
const appIcon = screen.getByTestId('app-icon')
|
||||
await act(async () => {
|
||||
@@ -918,7 +919,7 @@ describe('RenameDatasetModal', () => {
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -928,12 +929,12 @@ describe('RenameDatasetModal', () => {
|
||||
|
||||
await user.click(screen.getByTestId('app-icon'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||
await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Save and verify new icon is used
|
||||
@@ -963,12 +964,12 @@ describe('RenameDatasetModal', () => {
|
||||
|
||||
await user.click(screen.getByTestId('app-icon'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: '#E0F2FE' }))
|
||||
await user.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const saveButton = screen.getByText('common.operation.save')
|
||||
@@ -1002,11 +1003,11 @@ describe('RenameDatasetModal', () => {
|
||||
|
||||
const user = userEvent.setup()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: /iconPicker\.cancel/ }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Save and verify original icon is preserved
|
||||
@@ -1363,3 +1364,5 @@ describe('RenameDatasetModal', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mockEmojiData()
|
||||
@@ -5,6 +5,7 @@ import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { mockEmojiData } from '@/test/emoji-picker'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import CreateAppModal from '../index'
|
||||
|
||||
@@ -28,16 +29,6 @@ const triggerHotkey = (hotkey: string) => {
|
||||
registration?.handler()
|
||||
}
|
||||
|
||||
vi.mock('emoji-mart', () => ({
|
||||
init: vi.fn(),
|
||||
SearchIndex: { search: vi.fn().mockResolvedValue([]) },
|
||||
}))
|
||||
vi.mock('@emoji-mart/data', () => ({
|
||||
default: {
|
||||
categories: [{ id: 'people', emojis: ['😀'] }],
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({}),
|
||||
}))
|
||||
@@ -332,35 +323,28 @@ describe('CreateAppModal', () => {
|
||||
})
|
||||
|
||||
it('should update icon payload when selecting emoji and confirming', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { onConfirm } = await setup({
|
||||
appIconType: 'image',
|
||||
appIcon: 'file-123',
|
||||
appIconUrl: 'https://example.com/icon.png',
|
||||
})
|
||||
const { onConfirm } = await setup({
|
||||
appIconType: 'image',
|
||||
appIcon: 'file-123',
|
||||
appIconUrl: 'https://example.com/icon.png',
|
||||
})
|
||||
|
||||
const pickerDialog = openAppIconPicker()
|
||||
const pickerDialog = openAppIconPicker()
|
||||
|
||||
fireEvent.click(within(pickerDialog).getByRole('button', { name: '😀' }))
|
||||
fireEvent.click(await within(pickerDialog).findByRole('gridcell', { name: 'Grinning face' }))
|
||||
|
||||
fireEvent.click(within(pickerDialog).getByRole('button', { name: 'app.iconPicker.ok' }))
|
||||
fireEvent.click(within(pickerDialog).getByRole('button', { name: 'app.iconPicker.ok' }))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.create/ }))
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(300)
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.create/ }))
|
||||
await waitFor(() => expect(onConfirm).toHaveBeenCalledTimes(1))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
const payload = onConfirm.mock.calls[0]![0]
|
||||
expect(payload).toMatchObject({
|
||||
icon_type: 'emoji',
|
||||
icon: '😀',
|
||||
icon_background: '#FFEAD5',
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
const payload = onConfirm.mock.calls[0]![0]
|
||||
expect(payload).toMatchObject({
|
||||
icon_type: 'emoji',
|
||||
icon: '😀',
|
||||
icon_background: '#FFEAD5',
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow changing only the background for the current emoji icon', async () => {
|
||||
@@ -558,3 +542,5 @@ it('edits an existing app without waiting for application quota data', async ()
|
||||
await userEvent.setup().click(save)
|
||||
await waitFor(() => expect(onConfirm).toHaveBeenCalledOnce())
|
||||
})
|
||||
|
||||
mockEmojiData()
|
||||
+8
-5
@@ -1,6 +1,7 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { mockEmojiData } from '@/test/emoji-picker'
|
||||
import PublishAsKnowledgePipelineModal from '../publish-as-knowledge-pipeline-modal'
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
@@ -157,11 +158,11 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should show icon picker when app icon clicked', async () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('common.operation.search')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getIconButton())
|
||||
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update icon when emoji style is selected', async () => {
|
||||
@@ -172,7 +173,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -182,7 +183,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
fireEvent.click(getIconButton())
|
||||
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
|
||||
|
||||
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close icon picker when cancel is clicked', async () => {
|
||||
@@ -192,7 +193,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -215,3 +216,5 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith('Trimmed Name', expect.any(Object), 'Some desc')
|
||||
})
|
||||
})
|
||||
|
||||
mockEmojiData()
|
||||
@@ -4,6 +4,7 @@ import { zSsoProtocol } from '@dify/contracts/api/console/system-features/zod.ge
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { mockEmojiData } from '@/test/emoji-picker'
|
||||
import MCPModal from '../modal'
|
||||
|
||||
// Mock the service API
|
||||
@@ -689,7 +690,7 @@ describe('MCPModal', () => {
|
||||
fireEvent.click(appIconContainer)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('Search emojis...'))!.toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('common.operation.search'))!.toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -706,14 +707,14 @@ describe('MCPModal', () => {
|
||||
fireEvent.click(appIconContainer)
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -730,13 +731,13 @@ describe('MCPModal', () => {
|
||||
fireEvent.click(appIconContainer)
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -898,3 +899,5 @@ describe('MCPModal', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mockEmojiData()
|
||||
@@ -127,15 +127,15 @@ describe('AgentDetailSection', () => {
|
||||
})
|
||||
|
||||
it('renders the current agent avatar, name, and role', () => {
|
||||
const { container } = renderAgentDetailSection()
|
||||
renderAgentDetailSection()
|
||||
const agentName = screen.getByText('Research Agent')
|
||||
const agentAvatar = container.querySelector('em-emoji')?.parentElement
|
||||
const agentAvatar = screen.getByText('🧪')
|
||||
|
||||
expect(agentName).toBeInTheDocument()
|
||||
expect(screen.getByText('Research Assistant')).toBeInTheDocument()
|
||||
expect(screen.queryByText('agent')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('agentV2.agentDetail.title')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('em-emoji')).toHaveAttribute('id', '🧪')
|
||||
expect(agentAvatar).toHaveTextContent('🧪')
|
||||
expect(agentAvatar).toHaveClass('h-10', 'w-10', 'rounded-full')
|
||||
})
|
||||
|
||||
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
import './types/i18n'
|
||||
import './types/jsx'
|
||||
import './types/assets'
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useAppFavicon } from './use-app-favicon'
|
||||
|
||||
vi.mock('@/utils/emoji', () => ({
|
||||
searchEmoji: vi.fn(async () => '🤖'),
|
||||
}))
|
||||
|
||||
const firstAppIcon = 'https://example.com/app-one.png'
|
||||
const secondAppIcon = 'https://example.com/app-two.png'
|
||||
|
||||
@@ -76,7 +72,7 @@ describe('useAppFavicon', () => {
|
||||
|
||||
it('renders emoji icons as SVG favicons', async () => {
|
||||
renderHook(() =>
|
||||
useAppFavicon({ icon_type: 'emoji', icon: 'robot', icon_background: '#ffffff' }),
|
||||
useAppFavicon({ icon_type: 'emoji', icon: 'robot_face', icon_background: '#ffffff' }),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { useEffect } from 'react'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { searchEmoji } from '@/utils/emoji'
|
||||
import { resolveEmoji } from '@/utils/emoji'
|
||||
|
||||
type UseAppFaviconOptions = {
|
||||
enable?: boolean
|
||||
@@ -27,11 +27,11 @@ export function useAppFavicon(options: UseAppFaviconOptions) {
|
||||
} else if (icon_type === 'link') {
|
||||
href = icon
|
||||
} else if (icon_type !== 'emoji' || icon) {
|
||||
const emoji = icon ? await searchEmoji(icon) : '🤖'
|
||||
const emoji = resolveEmoji(icon)
|
||||
href =
|
||||
'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22>' +
|
||||
`<rect width=%22100%25%22 height=%22100%25%22 fill=%22${encodeURIComponent(icon_background || appDefaultIconBackground)}%22 rx=%2230%22 ry=%2230%22 />` +
|
||||
`<text x=%2212.5%22 y=%221em%22 font-size=%2275%22>${emoji}</text>` +
|
||||
`<text x=%2212.5%22 y=%221em%22 font-size=%2275%22>${emoji.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')}</text>` +
|
||||
'</svg>'
|
||||
type = 'image/svg+xml'
|
||||
}
|
||||
|
||||
+1
-2
@@ -38,7 +38,6 @@
|
||||
"@amplitude/analytics-browser": "catalog:",
|
||||
"@amplitude/plugin-session-replay-browser": "catalog:",
|
||||
"@base-ui/react": "catalog:",
|
||||
"@emoji-mart/data": "catalog:",
|
||||
"@floating-ui/react": "catalog:",
|
||||
"@formatjs/intl-localematcher": "catalog:",
|
||||
"@heroicons/react": "catalog:",
|
||||
@@ -80,10 +79,10 @@
|
||||
"embla-carousel-autoplay": "catalog:",
|
||||
"embla-carousel-fade": "catalog:",
|
||||
"embla-carousel-react": "catalog:",
|
||||
"emoji-mart": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"fast-deep-equal": "catalog:",
|
||||
"foxact": "catalog:",
|
||||
"frimousse": "catalog:",
|
||||
"fuse.js": "catalog:",
|
||||
"hast-util-to-jsx-runtime": "catalog:",
|
||||
"html-entities": "catalog:",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Missive.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Self-hosted emoji data
|
||||
|
||||
`emojibase-17.0.0/en/{data,messages}.json` are vendored files from
|
||||
`emojibase-data@17.0.0` (MIT; license included). Frimousse uses only these two
|
||||
files. A final newline is added where needed to satisfy EditorConfig; JSON contents
|
||||
are unchanged. Keep `locale="en"` to preserve the existing English search vocabulary.
|
||||
The URL includes the deployment base path and never falls back to a CDN.
|
||||
|
||||
To update, extract these files and LICENSE from the corresponding npm tarball,
|
||||
use a new versioned directory, and update the picker URL together.
|
||||
|
||||
`web/utils/emoji-legacy.json` is the frozen compatibility map generated from
|
||||
`@emoji-mart/data@1.2.1/sets/15/native.json`: each `emojis` key maps to its first
|
||||
skin's `native` value, followed by every `aliases` key mapped through its target.
|
||||
It is retained independently of picker data updates so persisted IDs keep working.
|
||||
Emoji Mart data is MIT licensed: https://github.com/missive/emoji-mart.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2019 Miles Johnson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
[{"label":"regional indicator A","hexcode":"1F1E6","emoji":"🇦","text":"","type":1,"version":0},{"label":"regional indicator B","hexcode":"1F1E7","emoji":"🇧","text":"","type":1,"version":0},{"label":"regional indicator C","hexcode":"1F1E8","emoji":"🇨","text":"","type":1,"version":0},{"label":"regional indicator D","hexcode":"1F1E9","emoji":"🇩","text":"","type":1,"version":0},{"label":"regional indicator E","hexcode":"1F1EA","emoji":"🇪","text":"","type":1,"version":0},{"label":"regional indicator F","hexcode":"1F1EB","emoji":"🇫","text":"","type":1,"version":0},{"label":"regional indicator G","hexcode":"1F1EC","emoji":"🇬","text":"","type":1,"version":0},{"label":"regional indicator H","hexcode":"1F1ED","emoji":"🇭","text":"","type":1,"version":0},{"label":"regional indicator I","hexcode":"1F1EE","emoji":"🇮","text":"","type":1,"version":0},{"label":"regional indicator J","hexcode":"1F1EF","emoji":"🇯","text":"","type":1,"version":0},{"label":"regional indicator K","hexcode":"1F1F0","emoji":"🇰","text":"","type":1,"version":0},{"label":"regional indicator L","hexcode":"1F1F1","emoji":"🇱","text":"","type":1,"version":0},{"label":"regional indicator M","hexcode":"1F1F2","emoji":"🇲","text":"","type":1,"version":0},{"label":"regional indicator N","hexcode":"1F1F3","emoji":"🇳","text":"","type":1,"version":0},{"label":"regional indicator O","hexcode":"1F1F4","emoji":"🇴","text":"","type":1,"version":0},{"label":"regional indicator P","hexcode":"1F1F5","emoji":"🇵","text":"","type":1,"version":0},{"label":"regional indicator Q","hexcode":"1F1F6","emoji":"🇶","text":"","type":1,"version":0},{"label":"regional indicator R","hexcode":"1F1F7","emoji":"🇷","text":"","type":1,"version":0},{"label":"regional indicator S","hexcode":"1F1F8","emoji":"🇸","text":"","type":1,"version":0},{"label":"regional indicator T","hexcode":"1F1F9","emoji":"🇹","text":"","type":1,"version":0},{"label":"regional indicator U","hexcode":"1F1FA","emoji":"🇺","text":"","type":1,"version":0},{"label":"regional indicator V","hexcode":"1F1FB","emoji":"🇻","text":"","type":1,"version":0},{"label":"regional indicator W","hexcode":"1F1FC","emoji":"🇼","text":"","type":1,"version":0},{"label":"regional indicator X","hexcode":"1F1FD","emoji":"🇽","text":"","type":1,"version":0},{"label":"regional indicator Y","hexcode":"1F1FE","emoji":"🇾","text":"","type":1,"version":0},{"label":"regional indicator Z","hexcode":"1F1FF","emoji":"🇿","text":"","type":1,"version":0},{"label":"grinning face","hexcode":"1F600","tags":["cheerful","cheery","face","grin","grinning","happy","laugh","nice","smile","smiling","teeth"],"emoji":"😀","text":"","type":1,"order":1,"group":0,"subgroup":0,"version":1},{"label":"grinning face with big eyes","hexcode":"1F603","tags":["awesome","big","eyes","face","grin","grinning","happy","mouth","open","smile","smiling","teeth","yay"],"emoji":"😃","text":"","type":1,"order":2,"group":0,"subgroup":0,"version":0.6},{"label":"grinning face with smiling eyes","hexcode":"1F604","tags":["eye","eyes","face","grin","grinning","happy","laugh","lol","mouth","open","smile","smiling"],"emoji":"😄","text":"","type":1,"order":3,"group":0,"subgroup":0,"version":0.6,"emoticon":":D"},{"label":"beaming face with smiling eyes","hexcode":"1F601","tags":["beaming","eye","eyes","face","grin","grinning","happy","nice","smile","smiling","teeth"],"emoji":"😁","text":"","type":1,"order":4,"group":0,"subgroup":0,"version":0.6},{"label":"grinning squinting face","hexcode":"1F606","tags":["closed","eyes","face","grinning","haha","hahaha","happy","laugh","lol","mouth","open","rofl","smile","smiling","squinting"],"emoji":"😆","text":"","type":1,"order":5,"group":0,"subgroup":0,"version":0.6,"emoticon":["xD","XD"]},{"label":"grinning face with sweat","hexcode":"1F605","tags":["cold","dejected","excited","face","grinning","mouth","nervous","open","smile","smiling","stress","stressed","sweat"],"emoji":"😅","text":"","type":1,"order":6,"group":0,"subgroup":0,"version":0.6},{"label":"rolling on the floor laughing","hexcode":"1F923","tags":["crying","face","floor","funny","haha","happy","hehe","hilarious","joy","laugh","lmao","lol","rofl","roflmao","rolling","tear"],"emoji":"🤣","text":"","type":1,"order":7,"group":0,"subgroup":0,"version":3,"emoticon":":'D"},{"label":"face with tears of joy","hexcode":"1F602","tags":["crying","face","feels","funny","haha","happy","hehe","hilarious","joy","laugh","lmao","lol","rofl","roflmao","tear"],"emoji":"😂","text":"","type":1,"order":8,"group":0,"subgroup":0,"version":0.6,"emoticon":":')"},{"label":"slightly smiling face","hexcode":"1F642","tags":["face","happy","slightly","smile","smiling"],"emoji":"🙂","text":"","type":1,"order":9,"group":0,"subgroup":0,"version":1,"emoticon":":)"},{"label":"upside-down face","hexcode":"1F643","tags":["face","hehe","smile","upside-down"],"emoji":"🙃","text":"","type":1,"order":10,"group":0,"subgroup":0,"version":1},{"laLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"groups":[{"key":"smileys-emotion","message":"smileys & emotion","order":0},{"key":"people-body","message":"people & body","order":1},{"key":"component","message":"components","order":2},{"key":"animals-nature","message":"animals & nature","order":3},{"key":"food-drink","message":"food & drink","order":4},{"key":"travel-places","message":"travel & places","order":5},{"key":"activities","message":"activities","order":6},{"key":"objects","message":"objects","order":7},{"key":"symbols","message":"symbols","order":8},{"key":"flags","message":"flags","order":9}],"skinTones":[{"key":"dark","message":"dark skin tone"},{"key":"light","message":"light skin tone"},{"key":"medium","message":"medium skin tone"},{"key":"medium-dark","message":"medium-dark skin tone"},{"key":"medium-light","message":"medium-light skin tone"}],"subgroups":[{"key":"face-smiling","message":"smiling","order":0},{"key":"face-affection","message":"affectionate","order":1},{"key":"face-tongue","message":"with tongue","order":2},{"key":"face-hand","message":"with hands","order":3},{"key":"face-neutral-skeptical","message":"neutral / skeptical","order":4},{"key":"face-sleepy","message":"sleepy","order":5},{"key":"face-unwell","message":"unwell","order":6},{"key":"face-hat","message":"with hats","order":7},{"key":"face-glasses","message":"with glasses","order":8},{"key":"face-concerned","message":"concerned","order":9},{"key":"face-negative","message":"negative","order":10},{"key":"face-costume","message":"costumed & creatures","order":11},{"key":"cat-face","message":"cat faces","order":12},{"key":"monkey-face","message":"monkey faces","order":13},{"key":"heart","message":"hearts","order":14},{"key":"emotion","message":"emotions","order":15},{"key":"hand-fingers-open","message":"fingers open","order":16},{"key":"hand-fingers-partial","message":"hand signs","order":17},{"key":"hand-single-finger","message":"finger pointing","order":18},{"key":"hand-fingers-closed","message":"fingers closed","order":19},{"key":"hands","message":"hands","order":20},{"key":"hand-prop","message":"hand props","order":21},{"key":"body-parts","message":"body parts","order":22},{"key":"person","message":"people","order":23},{"key":"person-gesture","message":"gestures","order":24},{"key":"person-role","message":"roles & careers","order":25},{"key":"person-fantasy","message":"fantasy","order":26},{"key":"person-activity","message":"activities","order":27},{"key":"person-sport","message":"athletics","order":28},{"key":"person-resting","message":"resting","order":29},{"key":"family","message":"family","order":30},{"key":"person-symbol","message":"people symbols","order":31},{"key":"skin-tone","message":"skin tones","order":32},{"key":"hair-style","message":"hair styles","order":33},{"key":"animal-mammal","message":"mammals","order":34},{"key":"animal-bird","message":"birds","order":35},{"key":"animal-amphibian","message":"amphibians","order":36},{"key":"animal-reptile","message":"reptiles","order":37},{"key":"animal-marine","message":"marine life","order":38},{"key":"animal-bug","message":"bugs","order":39},{"key":"plant-flower","message":"flowers","order":40},{"key":"plant-other","message":"other plants","order":41},{"key":"food-fruit","message":"fruit","order":42},{"key":"food-vegetable","message":"vegetables","order":43},{"key":"food-prepared","message":"cooked / prepared","order":44},{"key":"food-asian","message":"asian","order":45},{"key":"food-marine","message":"seafood","order":46},{"key":"food-sweet","message":"sweets & candy","order":46},{"key":"drink","message":"drink","order":47},{"key":"dishware","message":"dishware","order":48},{"key":"place-map","message":"globes & maps","order":49},{"key":"place-geographic","message":"geographic locations","order":50},{"key":"place-building","message":"buildings","order":51},{"key":"place-religious","message":"religious buildings","order":52},{"key":"place-other","message":"other places","order":53},{"key":"transport-ground","message":"ground transportation","order":54},{"key":"transport-water","message":"water transportation","order":55},{"key":"transport-air","message":"air transportation","order":56},{"key":"hotel","message":"hotel","order":57},{"key":"time","message":"time","order":58},{"key":"sky-weather","message":"weather","order":59},{"key":"event","message":"events & holidays","order":60},{"key":"award-medal","message":"award medals","order":61},{"key":"sport","message":"sports","order":62},{"key":"game","message":"games & hobbies","order":63},{"key":"arts-crafts","message":"arts & crafts","order":64},{"key":"clothing","message":"clothing","order":65},{"key":"sound","message":"sound","order":66},{"key":"music","message":"music","order":67},{"key":"musical-instrument","message":"musical instruments","order":68},{"key":"phone","message":"phone","order":69},{"key":"computer","message":"computer","order":70},{"key":"light-video","message":"light, film & video","order":71},{"key":"book-paper","message":"books & paper","ordLine truncated
|
||||
@@ -0,0 +1,22 @@
|
||||
import data from '@/public/emoji/emojibase-17.0.0/en/data.json'
|
||||
import messages from '@/public/emoji/emojibase-17.0.0/en/messages.json'
|
||||
|
||||
/** Exercise the real picker with the same data served in production, without network. */
|
||||
export function mockEmojiData() {
|
||||
beforeEach(() => {
|
||||
// oxlint-disable-next-line no-restricted-globals -- Reset Frimousse's own cache to exercise its data loader.
|
||||
localStorage.removeItem('frimousse/data/en')
|
||||
sessionStorage.removeItem('frimousse/metadata')
|
||||
sessionStorage.setItem(
|
||||
'frimousse/metadata',
|
||||
JSON.stringify({ emojiVersion: 17, countryFlags: true }),
|
||||
)
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const url = String(input)
|
||||
if (!/^\/.*emoji\/emojibase-17\.0\.0\/en\/(?:data|messages)\.json$/.test(url))
|
||||
throw new Error(`Unexpected emoji data URL: ${url}`)
|
||||
return new Response(JSON.stringify(url.endsWith('/messages.json') ? messages : data))
|
||||
})
|
||||
})
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
}
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// TypeScript type definitions for custom JSX elements
|
||||
// Custom JSX elements for emoji-mart web components
|
||||
|
||||
import 'react'
|
||||
|
||||
declare module 'react' {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'em-emoji': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { resolveEmoji } from '../emoji'
|
||||
|
||||
describe('persisted emoji compatibility', () => {
|
||||
it.each([
|
||||
['rabbit', '🐰'],
|
||||
['robot_face', '🤖'],
|
||||
['satisfied', '😆'],
|
||||
[':+1:', '👍'],
|
||||
['🐻', '🐻'],
|
||||
['👩🏽💻', '👩🏽💻'],
|
||||
['unknown-id', 'unknown-id'],
|
||||
['', '🤖'],
|
||||
[undefined, '🤖'],
|
||||
])('resolves %s to %s', (value, expected) => {
|
||||
expect(resolveEmoji(value)).toBe(expected)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
+7
-9
@@ -1,11 +1,9 @@
|
||||
import type { Emoji } from '@emoji-mart/data'
|
||||
import { SearchIndex } from 'emoji-mart'
|
||||
import legacyEmojis from './emoji-legacy.json'
|
||||
|
||||
export async function searchEmoji(value: string) {
|
||||
const emojis: Emoji[] = (await SearchIndex.search(value)) || []
|
||||
|
||||
const results = emojis.map((emoji) => {
|
||||
return emoji.skins[0]!.native
|
||||
})
|
||||
return results
|
||||
/** Resolve persisted emoji-mart IDs (including aliases) without loading picker data. */
|
||||
export function resolveEmoji(value?: string | null): string {
|
||||
if (!value) return '🤖'
|
||||
const id = value.replace(/^:|:$/g, '')
|
||||
if (Object.hasOwn(legacyEmojis, id)) return legacyEmojis[id as keyof typeof legacyEmojis]
|
||||
return value
|
||||
}
|
||||
@@ -68,10 +68,6 @@ export default defineConfig(({ command, mode, isPreview }) => {
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
ssr: {
|
||||
// SyntaxError: Named export not found. The requested module is a CommonJS module, which may not support all module.exports as named exports
|
||||
noExternal: ['emoji-mart'],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
|
||||
Reference in new issue
Block a user