Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions .claude/skills/frontend-testing/assets/component-test.template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,14 @@ import userEvent from '@testing-library/user-event'

// i18n (automatically mocked)
// WHY: Global mock in web/vitest.setup.ts is auto-loaded by Vitest setup
// No explicit mock needed - it returns translation keys as-is
// The global mock provides: useTranslation, Trans, useMixedTranslation, useGetLanguage
// No explicit mock needed for most tests
//
// Override only if custom translations are required:
// vi.mock('react-i18next', () => ({
// useTranslation: () => ({
// t: (key: string) => {
// const customTranslations: Record<string, string> = {
// 'my.custom.key': 'Custom Translation',
// }
// return customTranslations[key] || key
// },
// }),
// import { createReactI18nextMock } from '@/test/i18n-mock'
// vi.mock('react-i18next', () => createReactI18nextMock({
// 'my.custom.key': 'Custom Translation',
// 'button.save': 'Save',
// }))

// Router (if component uses useRouter, usePathname, useSearchParams)
Expand Down
28 changes: 17 additions & 11 deletions .claude/skills/frontend-testing/references/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,29 @@ Modules are not mocked automatically. Use `vi.mock` in test files, or add global
### 1. i18n (Auto-loaded via Global Mock)

A global mock is defined in `web/vitest.setup.ts` and is auto-loaded by Vitest setup.
**No explicit mock needed** for most tests - it returns translation keys as-is.

For tests requiring custom translations, override the mock:
The global mock provides:

- `useTranslation` - returns translation keys with namespace prefix
- `Trans` component - renders i18nKey and components
- `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`)
- `useGetLanguage` (from `@/context/i18n`) - returns `'en-US'`

**Default behavior**: Most tests should use the global mock (no local override needed).

**For custom translations**: Use the helper function from `@/test/i18n-mock`:

```typescript
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'my.custom.key': 'Custom translation',
}
return translations[key] || key
},
}),
import { createReactI18nextMock } from '@/test/i18n-mock'

vi.mock('react-i18next', () => createReactI18nextMock({
'my.custom.key': 'Custom translation',
'button.save': 'Save',
}))
```

**Avoid**: Manually defining `useTranslation` mocks that just return the key - the global mock already does this.

### 2. Next.js Router

```typescript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,6 @@ import * as React from 'react'
import { AgentStrategy } from '@/types/app'
import AgentSettingButton from './agent-setting-button'

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))

let latestAgentSettingProps: any
vi.mock('./agent/agent-setting', () => ({
default: (props: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ vi.mock('use-context-selector', async (importOriginal) => {
}
})

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))

const mockUseFeatures = vi.fn()
const mockUseFeaturesStore = vi.fn()
vi.mock('@/app/components/base/features/hooks', () => ({
Expand Down
26 changes: 7 additions & 19 deletions web/app/components/base/inline-delete-confirm/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import { cleanup, fireEvent, render } from '@testing-library/react'
import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import InlineDeleteConfirm from './index'

// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, defaultValueOrOptions?: string | { ns?: string }) => {
const translations: Record<string, string> = {
'operation.deleteConfirmTitle': 'Delete?',
'operation.yes': 'Yes',
'operation.no': 'No',
'operation.confirmAction': 'Please confirm your action.',
}
if (translations[key])
return translations[key]
// Handle case where second arg is default value string
if (typeof defaultValueOrOptions === 'string')
return defaultValueOrOptions
const prefix = defaultValueOrOptions?.ns ? `${defaultValueOrOptions.ns}.` : ''
return `${prefix}${key}`
},
}),
// Mock react-i18next with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
'operation.deleteConfirmTitle': 'Delete?',
'operation.yes': 'Yes',
'operation.no': 'No',
'operation.confirmAction': 'Please confirm your action.',
}))

afterEach(cleanup)
Expand Down
23 changes: 7 additions & 16 deletions web/app/components/base/input-with-copy/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import InputWithCopy from './index'

// Create a mock function that we can track using vi.hoisted
Expand All @@ -10,22 +11,12 @@ vi.mock('copy-to-clipboard', () => ({
default: mockCopyToClipboard,
}))

// Mock the i18n hook
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const translations: Record<string, string> = {
'operation.copy': 'Copy',
'operation.copied': 'Copied',
'overview.appInfo.embedded.copy': 'Copy',
'overview.appInfo.embedded.copied': 'Copied',
}
if (translations[key])
return translations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
// Mock the i18n hook with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
'operation.copy': 'Copy',
'operation.copied': 'Copied',
'overview.appInfo.embedded.copy': 'Copy',
'overview.appInfo.embedded.copied': 'Copied',
}))

// Mock es-toolkit/compat debounce
Expand Down
19 changes: 5 additions & 14 deletions web/app/components/base/input/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import Input, { inputVariants } from './index'

// Mock the i18n hook
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const translations: Record<string, string> = {
'operation.search': 'Search',
'placeholder.input': 'Please input',
}
if (translations[key])
return translations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
// Mock the i18n hook with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
'operation.search': 'Search',
'placeholder.input': 'Please input',
}))

describe('Input component', () => {
Expand Down
18 changes: 0 additions & 18 deletions web/app/components/billing/pricing/footer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import * as React from 'react'
import { CategoryEnum } from '.'
import Footer from './footer'

let mockTranslations: Record<string, string> = {}

vi.mock('next/link', () => ({
default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => (
<a href={href} className={className} target={target} data-testid="pricing-link">
Expand All @@ -13,25 +11,9 @@ vi.mock('next/link', () => ({
),
}))

vi.mock('react-i18next', async (importOriginal) => {
const actual = await importOriginal<typeof import('react-i18next')>()
return {
...actual,
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
if (mockTranslations[key])
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}
})

describe('Footer', () => {
beforeEach(() => {
vi.clearAllMocks()
mockTranslations = {}
})

// Rendering behavior
Expand Down
10 changes: 0 additions & 10 deletions web/app/components/datasets/create/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,6 @@ const IndexingTypeValues = {
// Mock External Dependencies
// ==========================================

// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))

// Mock next/link
vi.mock('next/link', () => {
return function MockLink({ children, href }: { children: React.ReactNode, href: string }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,6 @@ import Processing from './index'
// Mock External Dependencies
// ==========================================

// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))

// Mock useDocLink - returns a function that generates doc URLs
// Strips leading slash from path to match actual implementation behavior
vi.mock('@/context/i18n', () => ({
Expand Down
27 changes: 0 additions & 27 deletions web/app/components/plugins/card/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,6 @@ import Card from './index'
// Mock External Dependencies Only
// ================================

// Mock react-i18next (translation hook)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))

// Mock useMixedTranslation hook
vi.mock('../marketplace/hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
t: (key: string, options?: { ns?: string }) => {
const fullKey = options?.ns ? `${options.ns}.${key}` : key
const translations: Record<string, string> = {
'plugin.marketplace.partnerTip': 'Partner plugin',
'plugin.marketplace.verifiedTip': 'Verified plugin',
'plugin.installModal.installWarning': 'Install warning message',
}
return translations[fullKey] || key
},
}),
}))

// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))

// Mock useTheme hook
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: 'light' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,20 @@ vi.mock('@/context/app-context', () => ({
}),
}))

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
// Handle interpolation params (excluding ns)
const { ns: _ns, ...params } = options || {}
if (Object.keys(params).length > 0) {
return `${fullKey}:${JSON.stringify(params)}`
}
return fullKey
},
}),
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
<span data-testid="trans">
{i18nKey}
{components?.trustSource}
</span>
),
}))
vi.mock('react-i18next', async (importOriginal) => {
const actual = await importOriginal<typeof import('react-i18next')>()
const { createReactI18nextMock } = await import('@/test/i18n-mock')
return {
...actual,
...createReactI18nextMock(),
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
<span data-testid="trans">
{i18nKey}
{components?.trustSource}
</span>
),
}
})

vi.mock('../../../card', () => ({
default: ({ payload, titleLeft }: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,6 @@ vi.mock('@/service/plugins', () => ({
uploadFile: (...args: unknown[]) => mockUploadFile(...args),
}))

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
// Handle interpolation params (excluding ns)
const { ns: _ns, ...params } = options || {}
if (Object.keys(params).length > 0) {
return `${fullKey}:${JSON.stringify(params)}`
}
return fullKey
},
}),
}))

vi.mock('../../../card', () => ({
default: ({ payload, isLoading, loadingFileName }: {
payload: { name: string }
Expand Down
Loading
Loading