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
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same documentation issue - the comment states that the global mock provides useMixedTranslation and useGetLanguage, but these are from separate modules that are not mocked globally. Only useTranslation and Trans from react-i18next are provided by the global mock in web/vitest.setup.ts.

Suggested change
// The global mock provides: useTranslation, Trans, useMixedTranslation, useGetLanguage
// The global mock provides: useTranslation, Trans (other hooks like useMixedTranslation/useGetLanguage need explicit mocks)

Copilot uses AI. Check for mistakes.
// 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'`

Comment on lines +60 to +62
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue - the documentation states that the global mock provides useMixedTranslation and useGetLanguage, but these are from separate modules (@/app/components/plugins/marketplace/hooks and @/context/i18n) that are not mocked globally. Only useTranslation and Trans from react-i18next are provided by the global mock.

Suggested change
- `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`)
- `useGetLanguage` (from `@/context/i18n`) - returns `'en-US'`
Hooks from other modules such as:
- `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`)
- `useGetLanguage` (from `@/context/i18n`)
are **not** provided by this global mock. When your tests depend on them, mock those modules explicitly in the test file (using `vi.mock`) or via shared mocks under `web/__mocks__/`, following the testing guidelines.

Copilot uses AI. Check for mistakes.
**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
// ================================

Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file removed the mock for useMixedTranslation from ../marketplace/hooks, but the Card component being tested uses this hook (line 56 of index.tsx). Without a mock, the test will attempt to use the real implementation of useMixedTranslation, which depends on i18n.getFixedT and may not work correctly in the test environment. The removed mock should be restored, or a global mock for @/app/components/plugins/marketplace/hooks should be added to vitest.setup.ts.

Copilot uses AI. Check for mistakes.
// 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