From eeb5129a17fafb8a24bf638e04aca2c45f636c00 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:45:17 +0800 Subject: [PATCH] refactor: create shared react-i18next mock to reduce duplication (#29711) --- .claude/skills/frontend-testing/CHECKLIST.md | 2 +- .claude/skills/frontend-testing/SKILL.md | 1 + .../skills/frontend-testing/guides/mocking.md | 16 +++++++-- web/__mocks__/react-i18next.ts | 34 +++++++++++++++++++ web/__tests__/embedded-user-id-auth.test.tsx | 6 ---- .../goto-anything/command-selector.test.tsx | 6 ---- .../svg-attribute-error-reproduction.spec.tsx | 7 ---- .../csv-uploader.spec.tsx | 6 ---- .../base/operation-btn/index.spec.tsx | 6 ---- .../confirm-add-var/index.spec.tsx | 6 ---- .../conversation-history/edit-modal.spec.tsx | 6 ---- .../history-panel.spec.tsx | 6 ---- .../config-prompt/index.spec.tsx | 6 ---- .../config-var/config-select/index.spec.tsx | 6 ---- .../ctrl-btn-group/index.spec.tsx | 6 ---- .../debug-with-multiple-model/index.spec.tsx | 6 ---- .../app/workflow-log/detail.spec.tsx | 6 ---- .../app/workflow-log/filter.spec.tsx | 6 ---- .../app/workflow-log/index.spec.tsx | 7 ---- .../components/app/workflow-log/list.spec.tsx | 6 ---- .../workflow-log/trigger-by-display.spec.tsx | 6 ---- web/app/components/apps/app-card.spec.tsx | 7 ---- web/app/components/apps/empty.spec.tsx | 7 ---- web/app/components/apps/footer.spec.tsx | 7 ---- web/app/components/apps/index.spec.tsx | 7 ---- web/app/components/apps/list.spec.tsx | 7 ---- web/app/components/apps/new-app-card.spec.tsx | 7 ---- web/app/components/base/drawer/index.spec.tsx | 7 ---- .../base/form/components/label.spec.tsx | 6 ---- .../base/input-number/index.spec.tsx | 6 ---- .../billing/annotation-full/index.spec.tsx | 6 ---- .../billing/annotation-full/modal.spec.tsx | 6 ---- .../billing/plan-upgrade-modal/index.spec.tsx | 6 ---- .../connector/index.spec.tsx | 7 ---- .../create/index.spec.tsx | 7 ---- .../explore/app-card/index.spec.tsx | 6 ---- .../text-generation/no-data/index.spec.tsx | 6 ---- .../run-batch/res-download/index.spec.tsx | 6 ---- .../confirm-modal/index.spec.tsx | 7 ---- web/testing/testing.md | 11 ++++-- 40 files changed, 58 insertions(+), 228 deletions(-) create mode 100644 web/__mocks__/react-i18next.ts diff --git a/.claude/skills/frontend-testing/CHECKLIST.md b/.claude/skills/frontend-testing/CHECKLIST.md index 95e04aec3f..b960067264 100644 --- a/.claude/skills/frontend-testing/CHECKLIST.md +++ b/.claude/skills/frontend-testing/CHECKLIST.md @@ -76,7 +76,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen - [ ] **DO NOT mock base components** (`@/app/components/base/*`) - [ ] `jest.clearAllMocks()` in `beforeEach` (not `afterEach`) - [ ] Shared mock state reset in `beforeEach` -- [ ] i18n mock returns keys (not empty strings) +- [ ] i18n uses shared mock (auto-loaded); only override locally for custom translations - [ ] Router mocks match actual Next.js API - [ ] Mocks reflect actual component conditional behavior - [ ] Only mock: API services, complex context providers, third-party libs diff --git a/.claude/skills/frontend-testing/SKILL.md b/.claude/skills/frontend-testing/SKILL.md index dac604ac4b..06cb672141 100644 --- a/.claude/skills/frontend-testing/SKILL.md +++ b/.claude/skills/frontend-testing/SKILL.md @@ -318,3 +318,4 @@ For more detailed information, refer to: - `web/jest.config.ts` - Jest configuration - `web/jest.setup.ts` - Test environment setup - `web/testing/analyze-component.js` - Component analysis tool +- `web/__mocks__/react-i18next.ts` - Shared i18n mock (auto-loaded by Jest, no explicit mock needed; override locally only for custom translations) diff --git a/.claude/skills/frontend-testing/guides/mocking.md b/.claude/skills/frontend-testing/guides/mocking.md index 6b2c517cb6..bf0bd79690 100644 --- a/.claude/skills/frontend-testing/guides/mocking.md +++ b/.claude/skills/frontend-testing/guides/mocking.md @@ -46,12 +46,22 @@ Only mock these categories: ## Essential Mocks -### 1. i18n (Always Required) +### 1. i18n (Auto-loaded via Shared Mock) + +A shared mock is available at `web/__mocks__/react-i18next.ts` and is auto-loaded by Jest. +**No explicit mock needed** for most tests - it returns translation keys as-is. + +For tests requiring custom translations, override the mock: ```typescript jest.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string) => { + const translations: Record = { + 'my.custom.key': 'Custom translation', + } + return translations[key] || key + }, }), })) ``` @@ -313,7 +323,7 @@ Need to use a component in test? │ └─ YES → Mock it (next/navigation, external SDKs) │ └─ Is it i18n? - └─ YES → Mock to return keys + └─ YES → Uses shared mock (auto-loaded). Override only for custom translations ``` ## Factory Function Pattern diff --git a/web/__mocks__/react-i18next.ts b/web/__mocks__/react-i18next.ts new file mode 100644 index 0000000000..b0d22e0cc0 --- /dev/null +++ b/web/__mocks__/react-i18next.ts @@ -0,0 +1,34 @@ +/** + * Shared mock for react-i18next + * + * Jest automatically uses this mock when react-i18next is imported in tests. + * The default behavior returns the translation key as-is, which is suitable + * for most test scenarios. + * + * For tests that need custom translations, you can override with jest.mock(): + * + * @example + * jest.mock('react-i18next', () => ({ + * useTranslation: () => ({ + * t: (key: string) => { + * if (key === 'some.key') return 'Custom translation' + * return key + * }, + * }), + * })) + */ + +export const useTranslation = () => ({ + t: (key: string) => key, + i18n: { + language: 'en', + changeLanguage: jest.fn(), + }, +}) + +export const Trans = ({ children }: { children?: React.ReactNode }) => children + +export const initReactI18next = { + type: '3rdParty', + init: jest.fn(), +} diff --git a/web/__tests__/embedded-user-id-auth.test.tsx b/web/__tests__/embedded-user-id-auth.test.tsx index 5c3c3c943f..9d6734b120 100644 --- a/web/__tests__/embedded-user-id-auth.test.tsx +++ b/web/__tests__/embedded-user-id-auth.test.tsx @@ -4,12 +4,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import MailAndPasswordAuth from '@/app/(shareLayout)/webapp-signin/components/mail-and-password-auth' import CheckCode from '@/app/(shareLayout)/webapp-signin/check-code/page' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - const replaceMock = jest.fn() const backMock = jest.fn() diff --git a/web/__tests__/goto-anything/command-selector.test.tsx b/web/__tests__/goto-anything/command-selector.test.tsx index 6d4e045d49..e502c533bb 100644 --- a/web/__tests__/goto-anything/command-selector.test.tsx +++ b/web/__tests__/goto-anything/command-selector.test.tsx @@ -4,12 +4,6 @@ import '@testing-library/jest-dom' import CommandSelector from '../../app/components/goto-anything/command-selector' import type { ActionItem } from '../../app/components/goto-anything/actions/types' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('cmdk', () => ({ Command: { Group: ({ children, className }: any) =>
{children}
, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx index b1e915b2bf..374dbff203 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx @@ -3,13 +3,6 @@ import { render } from '@testing-library/react' import '@testing-library/jest-dom' import { OpikIconBig } from '@/app/components/base/icons/src/public/tracing' -// Mock dependencies to isolate the SVG rendering issue -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('SVG Attribute Error Reproduction', () => { // Capture console errors const originalError = console.error diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx index 91e1e9d8fe..d94295c31c 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx @@ -3,12 +3,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import CSVUploader, { type Props } from './csv-uploader' import { ToastContext } from '@/app/components/base/toast' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('CSVUploader', () => { const notify = jest.fn() const updateFile = jest.fn() diff --git a/web/app/components/app/configuration/base/operation-btn/index.spec.tsx b/web/app/components/app/configuration/base/operation-btn/index.spec.tsx index b504bdcfe7..615a1769e8 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.spec.tsx +++ b/web/app/components/app/configuration/base/operation-btn/index.spec.tsx @@ -1,12 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import OperationBtn from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('@remixicon/react', () => ({ RiAddLine: (props: { className?: string }) => ( diff --git a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx b/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx index 7ffafbb172..211b43c5ba 100644 --- a/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/confirm-add-var/index.spec.tsx @@ -2,12 +2,6 @@ import React from 'react' import { fireEvent, render, screen } from '@testing-library/react' import ConfirmAddVar from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('../../base/var-highlight', () => ({ __esModule: true, default: ({ name }: { name: string }) => {name}, diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx index 652f5409e8..2e75cd62ca 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.spec.tsx @@ -3,12 +3,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import EditModal from './edit-modal' import type { ConversationHistoriesRole } from '@/models/debug' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('@/app/components/base/modal', () => ({ __esModule: true, default: ({ children }: { children: React.ReactNode }) =>
{children}
, diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx index 61e361c057..c92bb48e4a 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.spec.tsx @@ -2,12 +2,6 @@ import React from 'react' import { render, screen } from '@testing-library/react' import HistoryPanel from './history-panel' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - const mockDocLink = jest.fn(() => 'doc-link') jest.mock('@/context/i18n', () => ({ useDocLink: () => mockDocLink, diff --git a/web/app/components/app/configuration/config-prompt/index.spec.tsx b/web/app/components/app/configuration/config-prompt/index.spec.tsx index b2098862da..37832cbdb3 100644 --- a/web/app/components/app/configuration/config-prompt/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/index.spec.tsx @@ -6,12 +6,6 @@ import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config' import { type PromptItem, PromptRole, type PromptVariable } from '@/models/debug' import { AppModeEnum, ModelModeType } from '@/types/app' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - type DebugConfiguration = { isAdvancedMode: boolean currentAdvancedPrompt: PromptItem | PromptItem[] diff --git a/web/app/components/app/configuration/config-var/config-select/index.spec.tsx b/web/app/components/app/configuration/config-var/config-select/index.spec.tsx index 18df318de3..eae3238532 100644 --- a/web/app/components/app/configuration/config-var/config-select/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-select/index.spec.tsx @@ -5,12 +5,6 @@ jest.mock('react-sortablejs', () => ({ ReactSortable: ({ children }: { children: React.ReactNode }) =>
{children}
, })) -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('ConfigSelect Component', () => { const defaultProps = { options: ['Option 1', 'Option 2'], diff --git a/web/app/components/app/configuration/ctrl-btn-group/index.spec.tsx b/web/app/components/app/configuration/ctrl-btn-group/index.spec.tsx index 89a99d2bfe..11cf438974 100644 --- a/web/app/components/app/configuration/ctrl-btn-group/index.spec.tsx +++ b/web/app/components/app/configuration/ctrl-btn-group/index.spec.tsx @@ -1,12 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import ContrlBtnGroup from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('ContrlBtnGroup', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/index.spec.tsx index 86e756d95c..140a6c2e6e 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/index.spec.tsx @@ -51,12 +51,6 @@ const mockFiles: FileEntity[] = [ }, ] -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('@/context/debug-configuration', () => ({ __esModule: true, useDebugConfigurationContext: () => mockUseDebugConfigurationContext(), diff --git a/web/app/components/app/workflow-log/detail.spec.tsx b/web/app/components/app/workflow-log/detail.spec.tsx index 48641307b9..b594be5f04 100644 --- a/web/app/components/app/workflow-log/detail.spec.tsx +++ b/web/app/components/app/workflow-log/detail.spec.tsx @@ -18,12 +18,6 @@ import type { App, AppIconType, AppModeEnum } from '@/types/app' // Mocks // ============================================================================ -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - const mockRouterPush = jest.fn() jest.mock('next/navigation', () => ({ useRouter: () => ({ diff --git a/web/app/components/app/workflow-log/filter.spec.tsx b/web/app/components/app/workflow-log/filter.spec.tsx index 416f0cd9d9..d7bec41224 100644 --- a/web/app/components/app/workflow-log/filter.spec.tsx +++ b/web/app/components/app/workflow-log/filter.spec.tsx @@ -16,12 +16,6 @@ import type { QueryParam } from './index' // Mocks // ============================================================================ -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - const mockTrackEvent = jest.fn() jest.mock('@/app/components/base/amplitude/utils', () => ({ trackEvent: (...args: unknown[]) => mockTrackEvent(...args), diff --git a/web/app/components/app/workflow-log/index.spec.tsx b/web/app/components/app/workflow-log/index.spec.tsx index b38c1e4d0f..e6d9f37949 100644 --- a/web/app/components/app/workflow-log/index.spec.tsx +++ b/web/app/components/app/workflow-log/index.spec.tsx @@ -49,13 +49,6 @@ jest.mock('next/navigation', () => ({ }), })) -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), - Trans: ({ children }: { children: React.ReactNode }) => <>{children}, -})) - jest.mock('next/link', () => ({ __esModule: true, default: ({ children, href }: { children: React.ReactNode; href: string }) => {children}, diff --git a/web/app/components/app/workflow-log/list.spec.tsx b/web/app/components/app/workflow-log/list.spec.tsx index 228b6ac465..be54dbc2f3 100644 --- a/web/app/components/app/workflow-log/list.spec.tsx +++ b/web/app/components/app/workflow-log/list.spec.tsx @@ -22,12 +22,6 @@ import { APP_PAGE_LIMIT } from '@/config' // Mocks // ============================================================================ -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - const mockRouterPush = jest.fn() jest.mock('next/navigation', () => ({ useRouter: () => ({ diff --git a/web/app/components/app/workflow-log/trigger-by-display.spec.tsx b/web/app/components/app/workflow-log/trigger-by-display.spec.tsx index a2110f14eb..6e95fc2f35 100644 --- a/web/app/components/app/workflow-log/trigger-by-display.spec.tsx +++ b/web/app/components/app/workflow-log/trigger-by-display.spec.tsx @@ -15,12 +15,6 @@ import { Theme } from '@/types/app' // Mocks // ============================================================================ -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - let mockTheme = Theme.light jest.mock('@/hooks/use-theme', () => ({ __esModule: true, diff --git a/web/app/components/apps/app-card.spec.tsx b/web/app/components/apps/app-card.spec.tsx index 5854820214..40aa66075d 100644 --- a/web/app/components/apps/app-card.spec.tsx +++ b/web/app/components/apps/app-card.spec.tsx @@ -3,13 +3,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { AppModeEnum } from '@/types/app' import { AccessMode } from '@/models/access-control' -// Mock react-i18next - return key as per testing skills -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock next/navigation const mockPush = jest.fn() jest.mock('next/navigation', () => ({ diff --git a/web/app/components/apps/empty.spec.tsx b/web/app/components/apps/empty.spec.tsx index d69c7b3e1b..8e7680958c 100644 --- a/web/app/components/apps/empty.spec.tsx +++ b/web/app/components/apps/empty.spec.tsx @@ -2,13 +2,6 @@ import React from 'react' import { render, screen } from '@testing-library/react' import Empty from './empty' -// Mock react-i18next - return key as per testing skills -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('Empty', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/web/app/components/apps/footer.spec.tsx b/web/app/components/apps/footer.spec.tsx index 4bf381bf4f..291f15a5eb 100644 --- a/web/app/components/apps/footer.spec.tsx +++ b/web/app/components/apps/footer.spec.tsx @@ -2,13 +2,6 @@ import React from 'react' import { render, screen } from '@testing-library/react' import Footer from './footer' -// Mock react-i18next - return key as per testing skills -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('Footer', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/web/app/components/apps/index.spec.tsx b/web/app/components/apps/index.spec.tsx index b5dafa5905..61783f91d8 100644 --- a/web/app/components/apps/index.spec.tsx +++ b/web/app/components/apps/index.spec.tsx @@ -1,13 +1,6 @@ import React from 'react' import { render, screen } from '@testing-library/react' -// Mock react-i18next - return key as per testing skills -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Track mock calls let documentTitleCalls: string[] = [] let educationInitCalls: number = 0 diff --git a/web/app/components/apps/list.spec.tsx b/web/app/components/apps/list.spec.tsx index f470d84f80..fe664a4a50 100644 --- a/web/app/components/apps/list.spec.tsx +++ b/web/app/components/apps/list.spec.tsx @@ -2,13 +2,6 @@ import React from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { AppModeEnum } from '@/types/app' -// Mock react-i18next - return key as per testing skills -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock next/navigation const mockReplace = jest.fn() const mockRouter = { replace: mockReplace } diff --git a/web/app/components/apps/new-app-card.spec.tsx b/web/app/components/apps/new-app-card.spec.tsx index fd75287f76..d0591db22a 100644 --- a/web/app/components/apps/new-app-card.spec.tsx +++ b/web/app/components/apps/new-app-card.spec.tsx @@ -1,13 +1,6 @@ import React from 'react' import { fireEvent, render, screen } from '@testing-library/react' -// Mock react-i18next - return key as per testing skills -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock next/navigation const mockReplace = jest.fn() jest.mock('next/navigation', () => ({ diff --git a/web/app/components/base/drawer/index.spec.tsx b/web/app/components/base/drawer/index.spec.tsx index 87289cd869..666bd501ac 100644 --- a/web/app/components/base/drawer/index.spec.tsx +++ b/web/app/components/base/drawer/index.spec.tsx @@ -6,13 +6,6 @@ import type { IDrawerProps } from './index' // Capture dialog onClose for testing let capturedDialogOnClose: (() => void) | null = null -// Mock react-i18next -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock @headlessui/react jest.mock('@headlessui/react', () => ({ Dialog: ({ children, open, onClose, className, unmount }: { diff --git a/web/app/components/base/form/components/label.spec.tsx b/web/app/components/base/form/components/label.spec.tsx index b2dc98a21e..12ab9e335b 100644 --- a/web/app/components/base/form/components/label.spec.tsx +++ b/web/app/components/base/form/components/label.spec.tsx @@ -1,12 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import Label from './label' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('Label Component', () => { const defaultProps = { htmlFor: 'test-input', diff --git a/web/app/components/base/input-number/index.spec.tsx b/web/app/components/base/input-number/index.spec.tsx index 75d19ecb6e..28db10e86c 100644 --- a/web/app/components/base/input-number/index.spec.tsx +++ b/web/app/components/base/input-number/index.spec.tsx @@ -1,12 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import { InputNumber } from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('InputNumber Component', () => { const defaultProps = { onChange: jest.fn(), diff --git a/web/app/components/billing/annotation-full/index.spec.tsx b/web/app/components/billing/annotation-full/index.spec.tsx index 77a0940f12..0caa6a0b57 100644 --- a/web/app/components/billing/annotation-full/index.spec.tsx +++ b/web/app/components/billing/annotation-full/index.spec.tsx @@ -1,12 +1,6 @@ import { render, screen } from '@testing-library/react' import AnnotationFull from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - let mockUsageProps: { className?: string } | null = null jest.mock('./usage', () => ({ __esModule: true, diff --git a/web/app/components/billing/annotation-full/modal.spec.tsx b/web/app/components/billing/annotation-full/modal.spec.tsx index da2b2041b0..150b2ced08 100644 --- a/web/app/components/billing/annotation-full/modal.spec.tsx +++ b/web/app/components/billing/annotation-full/modal.spec.tsx @@ -1,12 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import AnnotationFullModal from './modal' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - let mockUsageProps: { className?: string } | null = null jest.mock('./usage', () => ({ __esModule: true, diff --git a/web/app/components/billing/plan-upgrade-modal/index.spec.tsx b/web/app/components/billing/plan-upgrade-modal/index.spec.tsx index 324043d439..cd1be7cc6c 100644 --- a/web/app/components/billing/plan-upgrade-modal/index.spec.tsx +++ b/web/app/components/billing/plan-upgrade-modal/index.spec.tsx @@ -5,12 +5,6 @@ import PlanUpgradeModal from './index' const mockSetShowPricingModal = jest.fn() -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('@/app/components/base/modal', () => { const MockModal = ({ isShow, children }: { isShow: boolean; children: React.ReactNode }) => ( isShow ?
{children}
: null diff --git a/web/app/components/datasets/external-knowledge-base/connector/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/connector/index.spec.tsx index a6353a101c..667d701a92 100644 --- a/web/app/components/datasets/external-knowledge-base/connector/index.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/connector/index.spec.tsx @@ -16,13 +16,6 @@ jest.mock('next/navigation', () => ({ }), })) -// Mock react-i18next -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock useDocLink hook jest.mock('@/context/i18n', () => ({ useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path || ''}`, diff --git a/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx index c315743424..7dc6c77c82 100644 --- a/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx @@ -15,13 +15,6 @@ jest.mock('next/navigation', () => ({ }), })) -// Mock react-i18next -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Mock useDocLink hook jest.mock('@/context/i18n', () => ({ useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path || ''}`, diff --git a/web/app/components/explore/app-card/index.spec.tsx b/web/app/components/explore/app-card/index.spec.tsx index ee09a2ad26..4fffce6527 100644 --- a/web/app/components/explore/app-card/index.spec.tsx +++ b/web/app/components/explore/app-card/index.spec.tsx @@ -4,12 +4,6 @@ import AppCard, { type AppCardProps } from './index' import type { App } from '@/models/explore' import { AppModeEnum } from '@/types/app' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - jest.mock('@/app/components/base/app-icon', () => ({ __esModule: true, default: ({ children }: any) =>
{children}
, diff --git a/web/app/components/share/text-generation/no-data/index.spec.tsx b/web/app/components/share/text-generation/no-data/index.spec.tsx index 20a8485f4c..0e2a592e46 100644 --- a/web/app/components/share/text-generation/no-data/index.spec.tsx +++ b/web/app/components/share/text-generation/no-data/index.spec.tsx @@ -2,12 +2,6 @@ import React from 'react' import { render, screen } from '@testing-library/react' import NoData from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - describe('NoData', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/web/app/components/share/text-generation/run-batch/res-download/index.spec.tsx b/web/app/components/share/text-generation/run-batch/res-download/index.spec.tsx index 65acac8bb6..5660db1374 100644 --- a/web/app/components/share/text-generation/run-batch/res-download/index.spec.tsx +++ b/web/app/components/share/text-generation/run-batch/res-download/index.spec.tsx @@ -2,12 +2,6 @@ import React from 'react' import { render, screen } from '@testing-library/react' import ResDownload from './index' -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - const mockType = { Link: 'mock-link' } let capturedProps: Record | undefined diff --git a/web/app/components/tools/workflow-tool/confirm-modal/index.spec.tsx b/web/app/components/tools/workflow-tool/confirm-modal/index.spec.tsx index 37bc1539d7..c57a2891e3 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/index.spec.tsx @@ -3,13 +3,6 @@ import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import ConfirmModal from './index' -// Mock external dependencies as per guidelines -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - // Test utilities const defaultProps = { show: true, diff --git a/web/testing/testing.md b/web/testing/testing.md index a08a615e54..46c9d84b4d 100644 --- a/web/testing/testing.md +++ b/web/testing/testing.md @@ -326,12 +326,19 @@ describe('ComponentName', () => { ### General -1. **i18n**: Always return key +1. **i18n**: Uses shared mock at `web/__mocks__/react-i18next.ts` (auto-loaded by Jest) + + The shared mock returns translation keys as-is. For custom translations, override: ```typescript jest.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string) => { + const translations: Record = { + 'my.custom.key': 'Custom translation', + } + return translations[key] || key + }, }), })) ```