# Mocking Guide for Dify Frontend Tests

## ⚠️ Important: What NOT to Mock

### DO NOT Mock Base Components or dify-ui Primitives

**Never mock components from `@/app/components/base/` or from `@langgenius/dify-ui/*`** such as:

- Legacy base (`@/app/components/base/*`): `Loading`, `Spinner`, `Input`, `Badge`, `Tag`
- dify-ui primitives (`@langgenius/dify-ui/*`): `Button`, `Tooltip`, `Dialog`, `Popover`, `DropdownMenu`, `ContextMenu`, `Select`, `AlertDialog`, `Toast`

**Why?**

- These components have their own dedicated tests
- Mocking them creates false positives (tests pass but real integration fails)
- Using real components tests actual integration behavior

```typescript
// ❌ WRONG: Don't mock base components or dify-ui primitives
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
vi.mock('@langgenius/dify-ui/button', () => ({ Button: ({ children }: any) => <button>{children}</button> }))

// ✅ CORRECT: Import and use the real components
import Loading from '@/app/components/base/loading'
import { Button } from '@langgenius/dify-ui/button'
// They will render normally in tests
```

### What TO Mock

Only mock these categories:

1. **API services** (`@/service/*`) - Network calls
1. **Complex context providers** - When setup is too difficult
1. **Third-party libraries with side effects** - `next/navigation`, external SDKs
1. **i18n** - Always mock to return keys

### Zustand Stores - DO NOT Mock Manually

**Zustand is globally mocked** in `web/vitest.setup.ts`. Use real stores with `setState()`:

```typescript
// ✅ CORRECT: Use real store, set test state
import { useAppStore } from '@/app/components/app/store'

useAppStore.setState({ appDetail: { id: 'test', name: 'Test' } })
render(<MyComponent />)

// ❌ WRONG: Don't mock the store module
vi.mock('@/app/components/app/store', () => ({ ... }))
```

See [Zustand Store Testing](#zustand-store-testing) section for full details.

## Mock Placement

| Location | Purpose |
|----------|---------|
| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `next/image`, `zustand`) |
| `web/__mocks__/zustand.ts` | Zustand mock implementation (auto-resets stores after each test) |
| `web/__mocks__/` | Reusable mock factories shared across multiple test files |
| Test file | Test-specific mocks, inline with `vi.mock()` |

Modules are not mocked automatically. Use `vi.mock` in test files, or add global mocks in `web/vitest.setup.ts`.

**Note**: Zustand is special - it's globally mocked but you should NOT mock store modules manually. See [Zustand Store Testing](#zustand-store-testing).

## Essential Mocks

### 1. i18n (Auto-loaded via Global Mock)

A global mock is defined in `web/vitest.setup.ts` and is auto-loaded by Vitest setup.

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
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
const mockPush = vi.fn()
const mockReplace = vi.fn()

vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: mockPush,
    replace: mockReplace,
    back: vi.fn(),
    prefetch: vi.fn(),
  }),
  usePathname: () => '/current-path',
  useSearchParams: () => new URLSearchParams('?key=value'),
}))

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

  it('should navigate on click', () => {
    render(<Component />)
    fireEvent.click(screen.getByRole('button'))
    expect(mockPush).toHaveBeenCalledWith('/expected-path')
  })
})
```

### 2.1 `nuqs` Query State (Preferred: Testing Adapter)

For tests that validate URL query behavior, use `NuqsTestingAdapter` instead of mocking `nuqs` directly.

```typescript
import { renderHookWithNuqs } from '@/test/nuqs-testing'

it('should sync query to URL with push history', async () => {
  const { result, onUrlUpdate } = renderHookWithNuqs(() => useMyQueryState(), {
    searchParams: '?page=1',
  })

  act(() => {
    result.current.setQuery({ page: 2 })
  })

  await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
  const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
  expect(update.options.history).toBe('push')
  expect(update.searchParams.get('page')).toBe('2')
})
```

Use direct `vi.mock('nuqs')` only when URL synchronization is intentionally out of scope.

### 3. Portal Components (with Shared State)

```typescript
// ⚠️ Important: Use shared state for components that depend on each other
let mockPortalOpenState = false

vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
  PortalToFollowElem: ({ children, open, ...props }: any) => {
    mockPortalOpenState = open || false  // Update shared state
    return <div data-testid="portal" data-open={open}>{children}</div>
  },
  PortalToFollowElemContent: ({ children }: any) => {
    // ✅ Matches actual: returns null when portal is closed
    if (!mockPortalOpenState) return null
    return <div data-testid="portal-content">{children}</div>
  },
  PortalToFollowElemTrigger: ({ children }: any) => (
    <div data-testid="portal-trigger">{children}</div>
  ),
}))

describe('Component', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockPortalOpenState = false  // ✅ Reset shared state
  })
})
```

### 4. API Service Mocks

```typescript
import * as api from '@/service/api'

vi.mock('@/service/api')

const mockedApi = vi.mocked(api)

describe('Component', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    
    // Setup default mock implementation
    mockedApi.fetchData.mockResolvedValue({ data: [] })
  })

  it('should show data on success', async () => {
    mockedApi.fetchData.mockResolvedValue({ data: [{ id: 1 }] })
    
    render(<Component />)
    
    await waitFor(() => {
      expect(screen.getByText('1')).toBeInTheDocument()
    })
  })

  it('should show error on failure', async () => {
    mockedApi.fetchData.mockRejectedValue(new Error('Network error'))
    
    render(<Component />)
    
    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument()
    })
  })
})
```

### 5. HTTP Mocking with Nock

```typescript
import nock from 'nock'

const GITHUB_HOST = 'https://api.github.com'
const GITHUB_PATH = '/repos/owner/repo'

const mockGithubApi = (status: number, body: Record<string, unknown>, delayMs = 0) => {
  return nock(GITHUB_HOST)
    .get(GITHUB_PATH)
    .delay(delayMs)
    .reply(status, body)
}

describe('GithubComponent', () => {
  afterEach(() => {
    nock.cleanAll()
  })

  it('should display repo info', async () => {
    mockGithubApi(200, { name: 'dify', stars: 1000 })
    
    render(<GithubComponent />)
    
    await waitFor(() => {
      expect(screen.getByText('dify')).toBeInTheDocument()
    })
  })

  it('should handle API error', async () => {
    mockGithubApi(500, { message: 'Server error' })
    
    render(<GithubComponent />)
    
    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument()
    })
  })
})
```

### 6. Context Providers

```typescript
import { ProviderContext } from '@/context/provider-context'
import { createMockProviderContextValue, createMockPlan } from '@/__mocks__/provider-context'

describe('Component with Context', () => {
  it('should render for free plan', () => {
    const mockContext = createMockPlan('sandbox')
    
    render(
      <ProviderContext.Provider value={mockContext}>
        <Component />
      </ProviderContext.Provider>
    )
    
    expect(screen.getByText('Upgrade')).toBeInTheDocument()
  })

  it('should render for pro plan', () => {
    const mockContext = createMockPlan('professional')
    
    render(
      <ProviderContext.Provider value={mockContext}>
        <Component />
      </ProviderContext.Provider>
    )
    
    expect(screen.queryByText('Upgrade')).not.toBeInTheDocument()
  })
})
```

### 7. React Query

```typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: { retry: false },
    mutations: { retry: false },
  },
})

const renderWithQueryClient = (ui: React.ReactElement) => {
  const queryClient = createTestQueryClient()
  return render(
    <QueryClientProvider client={queryClient}>
      {ui}
    </QueryClientProvider>
  )
}
```

## Mock Best Practices

### ✅ DO

1. **Use real base components and dify-ui primitives** - Import from `@/app/components/base/` or `@langgenius/dify-ui/*` directly
1. **Use real project components** - Prefer importing over mocking
1. **Use real Zustand stores** - Set test state via `store.setState()`
1. **Reset mocks in `beforeEach`**, not `afterEach`
1. **Match actual component behavior** in mocks (when mocking is necessary)
1. **Use factory functions** for complex mock data
1. **Import actual types** for type safety
1. **Reset shared mock state** in `beforeEach`

### ❌ DON'T

1. **Don't mock base components or dify-ui primitives** (`Loading`, `Input`, `Button`, `Tooltip`, `Dialog`, etc.)
1. **Don't mock Zustand store modules** - Use real stores with `setState()`
1. Don't mock components you can import directly
1. Don't create overly simplified mocks that miss conditional logic
1. Don't forget to clean up nock after each test
1. Don't use `any` types in mocks without necessity

### Mock Decision Tree

```
Need to use a component in test?
│
├─ Is it from @/app/components/base/* or @langgenius/dify-ui/*?
│  └─ YES → Import real component, DO NOT mock
│
├─ Is it a project component?
│  └─ YES → Prefer importing real component
│           Only mock if setup is extremely complex
│
├─ Is it an API service (@/service/*)?
│  └─ YES → Mock it
│
├─ Is it a third-party lib with side effects?
│  └─ YES → Mock it (next/navigation, external SDKs)
│
├─ Is it a Zustand store?
│  └─ YES → DO NOT mock the module!
│           Use real store + setState() to set test state
│           (Global mock handles auto-reset)
│
└─ Is it i18n?
   └─ YES → Uses shared mock (auto-loaded). Override only for custom translations
```

## Zustand Store Testing

### Global Zustand Mock (Auto-loaded)

Zustand is globally mocked in `web/vitest.setup.ts` following the [official Zustand testing guide](https://zustand.docs.pmnd.rs/guides/testing). The mock in `web/__mocks__/zustand.ts` provides:

- Real store behavior with `getState()`, `setState()`, `subscribe()` methods
- Automatic store reset after each test via `afterEach`
- Proper test isolation between tests

### ✅ Recommended: Use Real Stores (Official Best Practice)

**DO NOT mock store modules manually.** Import and use the real store, then use `setState()` to set test state:

```typescript
// ✅ CORRECT: Use real store with setState
import { useAppStore } from '@/app/components/app/store'

describe('MyComponent', () => {
  it('should render app details', () => {
    // Arrange: Set test state via setState
    useAppStore.setState({
      appDetail: {
        id: 'test-app',
        name: 'Test App',
        mode: 'chat',
      },
    })

    // Act
    render(<MyComponent />)

    // Assert
    expect(screen.getByText('Test App')).toBeInTheDocument()
    // Can also verify store state directly
    expect(useAppStore.getState().appDetail?.name).toBe('Test App')
  })

  // No cleanup needed - global mock auto-resets after each test
})
```

### ❌ Avoid: Manual Store Module Mocking

Manual mocking conflicts with the global Zustand mock and loses store functionality:

```typescript
// ❌ WRONG: Don't mock the store module
vi.mock('@/app/components/app/store', () => ({
  useStore: (selector) => mockSelector(selector),  // Missing getState, setState!
}))

// ❌ WRONG: This conflicts with global zustand mock
vi.mock('@/app/components/workflow/store', () => ({
  useWorkflowStore: vi.fn(() => mockState),
}))
```

**Problems with manual mocking:**

1. Loses `getState()`, `setState()`, `subscribe()` methods
1. Conflicts with global Zustand mock behavior
1. Requires manual maintenance of store API
1. Tests don't reflect actual store behavior

### When Manual Store Mocking is Necessary

In rare cases where the store has complex initialization or side effects, you can mock it, but ensure you provide the full store API:

```typescript
// If you MUST mock (rare), include full store API
const mockStore = {
  appDetail: { id: 'test', name: 'Test' },
  setAppDetail: vi.fn(),
}

vi.mock('@/app/components/app/store', () => ({
  useStore: Object.assign(
    (selector: (state: typeof mockStore) => unknown) => selector(mockStore),
    {
      getState: () => mockStore,
      setState: vi.fn(),
      subscribe: vi.fn(),
    },
  ),
}))
```

### Store Testing Decision Tree

```
Need to test a component using Zustand store?
│
├─ Can you use the real store?
│  └─ YES → Use real store + setState (RECOMMENDED)
│           useAppStore.setState({ ... })
│
├─ Does the store have complex initialization/side effects?
│  └─ YES → Consider mocking, but include full API
│           (getState, setState, subscribe)
│
└─ Are you testing the store itself (not a component)?
   └─ YES → Test store directly with getState/setState
            const store = useMyStore
            store.setState({ count: 0 })
            store.getState().increment()
            expect(store.getState().count).toBe(1)
```

### Example: Testing Store Actions

```typescript
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  it('should increment count', () => {
    // Initial state (auto-reset by global mock)
    expect(useCounterStore.getState().count).toBe(0)

    // Call action
    useCounterStore.getState().increment()

    // Verify state change
    expect(useCounterStore.getState().count).toBe(1)
  })

  it('should reset to initial state', () => {
    // Set some state
    useCounterStore.setState({ count: 100 })
    expect(useCounterStore.getState().count).toBe(100)

    // After this test, global mock will reset to initial state
  })
})
```

## Factory Function Pattern

```typescript
// __mocks__/data-factories.ts
import type { User, Project } from '@/types'

export const createMockUser = (overrides: Partial<User> = {}): User => ({
  id: 'user-1',
  name: 'Test User',
  email: 'test@example.com',
  role: 'member',
  createdAt: new Date().toISOString(),
  ...overrides,
})

export const createMockProject = (overrides: Partial<Project> = {}): Project => ({
  id: 'project-1',
  name: 'Test Project',
  description: 'A test project',
  owner: createMockUser(),
  members: [],
  createdAt: new Date().toISOString(),
  ...overrides,
})

// Usage in tests
it('should display project owner', () => {
  const project = createMockProject({
    owner: createMockUser({ name: 'John Doe' }),
  })
  
  render(<ProjectCard project={project} />)
  expect(screen.getByText('John Doe')).toBeInTheDocument()
})
```
