mirror of
https://github.com/langgenius/dify.git
synced 2026-04-27 19:27:23 +08:00
add @testing-library/user-event and create tests for external-knowledge-base/ (#29323)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
f722fdfa6d
commit
681c06186e
@ -0,0 +1,367 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import type { ExternalAPIItem } from '@/models/datasets'
|
||||||
|
import ExternalKnowledgeBaseConnector from './index'
|
||||||
|
import { createExternalKnowledgeBase } from '@/service/datasets'
|
||||||
|
|
||||||
|
// Mock next/navigation
|
||||||
|
const mockRouterBack = jest.fn()
|
||||||
|
const mockReplace = jest.fn()
|
||||||
|
jest.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
back: mockRouterBack,
|
||||||
|
replace: mockReplace,
|
||||||
|
push: jest.fn(),
|
||||||
|
refresh: jest.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 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 || ''}`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock toast context
|
||||||
|
const mockNotify = jest.fn()
|
||||||
|
jest.mock('@/app/components/base/toast', () => ({
|
||||||
|
useToastContext: () => ({
|
||||||
|
notify: mockNotify,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock modal context
|
||||||
|
jest.mock('@/context/modal-context', () => ({
|
||||||
|
useModalContext: () => ({
|
||||||
|
setShowExternalKnowledgeAPIModal: jest.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock API service
|
||||||
|
jest.mock('@/service/datasets', () => ({
|
||||||
|
createExternalKnowledgeBase: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Factory function to create mock ExternalAPIItem
|
||||||
|
const createMockExternalAPIItem = (overrides: Partial<ExternalAPIItem> = {}): ExternalAPIItem => ({
|
||||||
|
id: 'api-default',
|
||||||
|
tenant_id: 'tenant-1',
|
||||||
|
name: 'Default API',
|
||||||
|
description: 'Default API description',
|
||||||
|
settings: {
|
||||||
|
endpoint: 'https://api.example.com',
|
||||||
|
api_key: 'test-api-key',
|
||||||
|
},
|
||||||
|
dataset_bindings: [],
|
||||||
|
created_by: 'user-1',
|
||||||
|
created_at: '2024-01-01T00:00:00Z',
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Default mock API list
|
||||||
|
const createDefaultMockApiList = (): ExternalAPIItem[] => [
|
||||||
|
createMockExternalAPIItem({
|
||||||
|
id: 'api-1',
|
||||||
|
name: 'Test API 1',
|
||||||
|
settings: { endpoint: 'https://api1.example.com', api_key: 'key-1' },
|
||||||
|
}),
|
||||||
|
createMockExternalAPIItem({
|
||||||
|
id: 'api-2',
|
||||||
|
name: 'Test API 2',
|
||||||
|
settings: { endpoint: 'https://api2.example.com', api_key: 'key-2' },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
let mockExternalKnowledgeApiList: ExternalAPIItem[] = createDefaultMockApiList()
|
||||||
|
|
||||||
|
jest.mock('@/context/external-knowledge-api-context', () => ({
|
||||||
|
useExternalKnowledgeApi: () => ({
|
||||||
|
externalKnowledgeApiList: mockExternalKnowledgeApiList,
|
||||||
|
mutateExternalKnowledgeApis: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Suppress console.error helper
|
||||||
|
const suppressConsoleError = () => jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
||||||
|
|
||||||
|
// Helper to create a pending promise with external resolver
|
||||||
|
function createPendingPromise<T>() {
|
||||||
|
let resolve: (value: T) => void = jest.fn()
|
||||||
|
const promise = new Promise<T>((r) => {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
return { promise, resolve }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to fill required form fields and submit
|
||||||
|
async function fillFormAndSubmit(user: ReturnType<typeof userEvent.setup>) {
|
||||||
|
const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder')
|
||||||
|
const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder')
|
||||||
|
|
||||||
|
fireEvent.change(nameInput, { target: { value: 'Test Knowledge Base' } })
|
||||||
|
fireEvent.change(knowledgeIdInput, { target: { value: 'kb-123' } })
|
||||||
|
|
||||||
|
// Wait for button to be enabled
|
||||||
|
await waitFor(() => {
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
expect(connectButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
await user.click(connectButton!)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ExternalKnowledgeBaseConnector', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
mockExternalKnowledgeApiList = createDefaultMockApiList()
|
||||||
|
;(createExternalKnowledgeBase as jest.Mock).mockResolvedValue({ id: 'new-kb-id' })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests for rendering with real ExternalKnowledgeBaseCreate component
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render the create form with all required elements', () => {
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
// Verify main title and form elements
|
||||||
|
expect(screen.getByText('dataset.connectDataset')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('dataset.externalKnowledgeName')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('dataset.externalKnowledgeId')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('dataset.retrievalSettings')).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Verify buttons
|
||||||
|
expect(screen.getByText('dataset.externalKnowledgeForm.cancel')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('dataset.externalKnowledgeForm.connect')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render connect button disabled initially', () => {
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
expect(connectButton).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests for API success flow
|
||||||
|
describe('API Success Flow', () => {
|
||||||
|
it('should call API and show success notification when form is submitted', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
await fillFormAndSubmit(user)
|
||||||
|
|
||||||
|
// Verify API was called with form data
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(createExternalKnowledgeBase).toHaveBeenCalledWith({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
name: 'Test Knowledge Base',
|
||||||
|
external_knowledge_id: 'kb-123',
|
||||||
|
external_knowledge_api_id: 'api-1',
|
||||||
|
provider: 'external',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify success notification
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith({
|
||||||
|
type: 'success',
|
||||||
|
message: 'External Knowledge Base Connected Successfully',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify navigation back
|
||||||
|
expect(mockRouterBack).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include retrieval settings in API call', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
await fillFormAndSubmit(user)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(createExternalKnowledgeBase).toHaveBeenCalledWith({
|
||||||
|
body: expect.objectContaining({
|
||||||
|
external_retrieval_model: expect.objectContaining({
|
||||||
|
top_k: 4,
|
||||||
|
score_threshold: 0.5,
|
||||||
|
score_threshold_enabled: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests for API error flow
|
||||||
|
describe('API Error Flow', () => {
|
||||||
|
it('should show error notification when API fails', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const consoleErrorSpy = suppressConsoleError()
|
||||||
|
;(createExternalKnowledgeBase as jest.Mock).mockRejectedValue(new Error('Network Error'))
|
||||||
|
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
await fillFormAndSubmit(user)
|
||||||
|
|
||||||
|
// Verify error notification
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to connect External Knowledge Base',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify no navigation
|
||||||
|
expect(mockRouterBack).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
consoleErrorSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error notification when API returns invalid result', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const consoleErrorSpy = suppressConsoleError()
|
||||||
|
;(createExternalKnowledgeBase as jest.Mock).mockResolvedValue({})
|
||||||
|
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
await fillFormAndSubmit(user)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Failed to connect External Knowledge Base',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockRouterBack).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
consoleErrorSpy.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests for loading state
|
||||||
|
describe('Loading State', () => {
|
||||||
|
it('should show loading state during API call', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
// Create a promise that won't resolve immediately
|
||||||
|
const { promise, resolve: resolvePromise } = createPendingPromise<{ id: string }>()
|
||||||
|
;(createExternalKnowledgeBase as jest.Mock).mockReturnValue(promise)
|
||||||
|
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
// Fill form
|
||||||
|
const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder')
|
||||||
|
const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder')
|
||||||
|
fireEvent.change(nameInput, { target: { value: 'Test' } })
|
||||||
|
fireEvent.change(knowledgeIdInput, { target: { value: 'kb-1' } })
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
expect(connectButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Click connect
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
await user.click(connectButton!)
|
||||||
|
|
||||||
|
// Button should show loading (the real Button component has loading prop)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(createExternalKnowledgeBase).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resolve the promise
|
||||||
|
resolvePromise({ id: 'new-id' })
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith({
|
||||||
|
type: 'success',
|
||||||
|
message: 'External Knowledge Base Connected Successfully',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests for form validation (integration with real create component)
|
||||||
|
describe('Form Validation', () => {
|
||||||
|
it('should keep button disabled when only name is filled', () => {
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder')
|
||||||
|
fireEvent.change(nameInput, { target: { value: 'Test' } })
|
||||||
|
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
expect(connectButton).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep button disabled when only knowledge id is filled', () => {
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder')
|
||||||
|
fireEvent.change(knowledgeIdInput, { target: { value: 'kb-1' } })
|
||||||
|
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
expect(connectButton).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should enable button when all required fields are filled', async () => {
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder')
|
||||||
|
const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder')
|
||||||
|
|
||||||
|
fireEvent.change(nameInput, { target: { value: 'Test' } })
|
||||||
|
fireEvent.change(knowledgeIdInput, { target: { value: 'kb-1' } })
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button')
|
||||||
|
expect(connectButton).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tests for user interactions
|
||||||
|
describe('User Interactions', () => {
|
||||||
|
it('should allow typing in form fields', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder')
|
||||||
|
const descriptionInput = screen.getByPlaceholderText('dataset.externalKnowledgeDescriptionPlaceholder')
|
||||||
|
|
||||||
|
await user.type(nameInput, 'My Knowledge Base')
|
||||||
|
await user.type(descriptionInput, 'My Description')
|
||||||
|
|
||||||
|
expect((nameInput as HTMLInputElement).value).toBe('My Knowledge Base')
|
||||||
|
expect((descriptionInput as HTMLTextAreaElement).value).toBe('My Description')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle cancel button click', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const cancelButton = screen.getByText('dataset.externalKnowledgeForm.cancel').closest('button')
|
||||||
|
await user.click(cancelButton!)
|
||||||
|
|
||||||
|
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle back button click', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<ExternalKnowledgeBaseConnector />)
|
||||||
|
|
||||||
|
const buttons = screen.getAllByRole('button')
|
||||||
|
const backButton = buttons.find(btn => btn.classList.contains('rounded-full'))
|
||||||
|
await user.click(backButton!)
|
||||||
|
|
||||||
|
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,19 @@
|
|||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import { cleanup } from '@testing-library/react'
|
import { cleanup } from '@testing-library/react'
|
||||||
|
|
||||||
|
// Fix for @headlessui/react compatibility with happy-dom
|
||||||
|
// headlessui tries to set focus property which is read-only in happy-dom
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// Ensure window.focus is writable for headlessui
|
||||||
|
if (!Object.getOwnPropertyDescriptor(window, 'focus')?.writable) {
|
||||||
|
Object.defineProperty(window, 'focus', {
|
||||||
|
value: jest.fn(),
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup()
|
cleanup()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -168,6 +168,7 @@
|
|||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|||||||
3
web/pnpm-lock.yaml
generated
3
web/pnpm-lock.yaml
generated
@ -416,6 +416,9 @@ importers:
|
|||||||
'@testing-library/react':
|
'@testing-library/react':
|
||||||
specifier: ^16.3.0
|
specifier: ^16.3.0
|
||||||
version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
|
'@testing-library/user-event':
|
||||||
|
specifier: ^14.6.1
|
||||||
|
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: ^29.5.14
|
specifier: ^29.5.14
|
||||||
version: 29.5.14
|
version: 29.5.14
|
||||||
|
|||||||
@ -145,8 +145,17 @@ Treat component state as part of the public behavior: confirm the initial render
|
|||||||
- ✅ When creating lightweight provider stubs, mirror the real default values and surface helper builders (for example `createMockWorkflowContext`).
|
- ✅ When creating lightweight provider stubs, mirror the real default values and surface helper builders (for example `createMockWorkflowContext`).
|
||||||
- ✅ Reset shared stores (React context, Zustand, TanStack Query cache) between tests to avoid leaking state. Prefer helper factory functions over module-level singletons in specs.
|
- ✅ Reset shared stores (React context, Zustand, TanStack Query cache) between tests to avoid leaking state. Prefer helper factory functions over module-level singletons in specs.
|
||||||
- ✅ For hooks that read from context, use `renderHook` with a custom wrapper that supplies required providers.
|
- ✅ For hooks that read from context, use `renderHook` with a custom wrapper that supplies required providers.
|
||||||
|
- ✅ **Use factory functions for mock data**: Import actual types and create factory functions with complete defaults (see [Test Data Builders](#9-test-data-builders-anti-hardcoding) section).
|
||||||
|
- ✅ If it's need to mock some common context provider used across many components (for example, `ProviderContext`), put it in __mocks__/context(for example, `__mocks__/context/provider-context`). To dynamically control the mock behavior (for example, toggling plan type), use module-level variables to track state and change them(for example, `context/provider-context-mock.spec.tsx`).
|
||||||
|
- ✅ Use factory functions to create mock data with TypeScript types. This ensures type safety and makes tests more maintainable.
|
||||||
|
|
||||||
If it's need to mock some common context provider used across many components (for example, `ProviderContext`), put it in __mocks__/context(for example, `__mocks__/context/provider-context`). To dynamically control the mock behavior (for example, toggling plan type), use module-level variables to track state and change them(for example, `context/provier-context-mock.spec.tsx`).
|
**Rules**:
|
||||||
|
|
||||||
|
1. **Import actual types**: Always import types from the source (`@/models/`, `@/types/`, etc.) instead of defining inline types.
|
||||||
|
1. **Provide complete defaults**: Factory functions should return complete objects with all required fields filled with sensible defaults.
|
||||||
|
1. **Allow partial overrides**: Accept `Partial<T>` to enable flexible customization for specific test cases.
|
||||||
|
1. **Create list factories**: For array data, create a separate factory function that composes item factories.
|
||||||
|
1. **Reference**: See `__mocks__/provider-context.ts` for reusable context mock factories used across multiple test files.
|
||||||
|
|
||||||
### 4. Performance Optimization
|
### 4. Performance Optimization
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user