dify
This commit is contained in:
253
dify/web/utils/context.spec.ts
Normal file
253
dify/web/utils/context.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Test suite for React context creation utilities
|
||||
*
|
||||
* This module provides helper functions to create React contexts with better type safety
|
||||
* and automatic error handling when context is used outside of its provider.
|
||||
*
|
||||
* Two variants are provided:
|
||||
* - createCtx: Standard React context using useContext/createContext
|
||||
* - createSelectorCtx: Context with selector support using use-context-selector library
|
||||
*/
|
||||
import React from 'react'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { createCtx, createSelectorCtx } from './context'
|
||||
|
||||
describe('Context Utilities', () => {
|
||||
describe('createCtx', () => {
|
||||
/**
|
||||
* Test that createCtx creates a valid context with provider and hook
|
||||
* The function should return a tuple with [Provider, useContextValue, Context]
|
||||
* plus named properties for easier access
|
||||
*/
|
||||
it('should create context with provider and hook', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext, Context] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
expect(Provider).toBeDefined()
|
||||
expect(useTestContext).toBeDefined()
|
||||
expect(Context).toBeDefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the context hook returns the provided value correctly
|
||||
* when used within the context provider
|
||||
*/
|
||||
it('should provide and consume context value', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
const testValue = { value: 'test-value' }
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value: testValue }, children)
|
||||
|
||||
const { result } = renderHook(() => useTestContext(), { wrapper })
|
||||
|
||||
expect(result.current).toEqual(testValue)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that accessing context outside of provider throws an error
|
||||
* This ensures developers are notified when they forget to wrap components
|
||||
*/
|
||||
it('should throw error when used outside provider', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
// Suppress console.error for this test
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTestContext())
|
||||
}).toThrow('No Test context found.')
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that context works with default values
|
||||
* When a default value is provided, it should be accessible without a provider
|
||||
*/
|
||||
it('should use default value when provided', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const defaultValue = { value: 'default' }
|
||||
const [, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTestContext())
|
||||
|
||||
expect(result.current).toEqual(defaultValue)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that the returned tuple has named properties for convenience
|
||||
* This allows destructuring or property access based on preference
|
||||
*/
|
||||
it('should expose named properties', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const result = createCtx<TestContextValue>({ name: 'Test' })
|
||||
|
||||
expect(result.provider).toBe(result[0])
|
||||
expect(result.useContextValue).toBe(result[1])
|
||||
expect(result.context).toBe(result[2])
|
||||
})
|
||||
|
||||
/**
|
||||
* Test context with complex data types
|
||||
* Ensures type safety is maintained with nested objects and arrays
|
||||
*/
|
||||
it('should handle complex context values', () => {
|
||||
type ComplexContext = {
|
||||
user: { id: string; name: string }
|
||||
settings: { theme: string; locale: string }
|
||||
actions: Array<() => void>
|
||||
}
|
||||
|
||||
const [Provider, useComplexContext] = createCtx<ComplexContext>({
|
||||
name: 'Complex',
|
||||
})
|
||||
|
||||
const complexValue: ComplexContext = {
|
||||
user: { id: '123', name: 'Test User' },
|
||||
settings: { theme: 'dark', locale: 'en-US' },
|
||||
actions: [
|
||||
() => { /* empty action 1 */ },
|
||||
() => { /* empty action 2 */ },
|
||||
],
|
||||
}
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value: complexValue }, children)
|
||||
|
||||
const { result } = renderHook(() => useComplexContext(), { wrapper })
|
||||
|
||||
expect(result.current).toEqual(complexValue)
|
||||
expect(result.current.user.id).toBe('123')
|
||||
expect(result.current.settings.theme).toBe('dark')
|
||||
expect(result.current.actions).toHaveLength(2)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that context updates propagate to consumers
|
||||
* When provider value changes, hooks should receive the new value
|
||||
*/
|
||||
it('should update when context value changes', () => {
|
||||
type TestContextValue = { count: number }
|
||||
const [Provider, useTestContext] = createCtx<TestContextValue>({
|
||||
name: 'Test',
|
||||
})
|
||||
|
||||
let value = { count: 0 }
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value }, children)
|
||||
|
||||
const { result, rerender } = renderHook(() => useTestContext(), { wrapper })
|
||||
|
||||
expect(result.current.count).toBe(0)
|
||||
|
||||
value = { count: 5 }
|
||||
rerender()
|
||||
|
||||
expect(result.current.count).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSelectorCtx', () => {
|
||||
/**
|
||||
* Test that createSelectorCtx creates a valid context with selector support
|
||||
* This variant uses use-context-selector for optimized re-renders
|
||||
*/
|
||||
it('should create selector context with provider and hook', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext, Context] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
})
|
||||
|
||||
expect(Provider).toBeDefined()
|
||||
expect(useTestContext).toBeDefined()
|
||||
expect(Context).toBeDefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that selector context provides and consumes values correctly
|
||||
* The API should be identical to createCtx for basic usage
|
||||
*/
|
||||
it('should provide and consume context value with selector', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [Provider, useTestContext] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
})
|
||||
|
||||
const testValue = { value: 'selector-test' }
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement(Provider, { value: testValue }, children)
|
||||
|
||||
const { result } = renderHook(() => useTestContext(), { wrapper })
|
||||
|
||||
expect(result.current).toEqual(testValue)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test error handling for selector context
|
||||
* Should throw error when used outside provider, same as createCtx
|
||||
*/
|
||||
it('should throw error when used outside provider', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [, useTestContext] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
})
|
||||
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTestContext())
|
||||
}).toThrow('No SelectorTest context found.')
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that selector context works with default values
|
||||
*/
|
||||
it('should use default value when provided', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const defaultValue = { value: 'selector-default' }
|
||||
const [, useTestContext] = createSelectorCtx<TestContextValue>({
|
||||
name: 'SelectorTest',
|
||||
defaultValue,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTestContext())
|
||||
|
||||
expect(result.current).toEqual(defaultValue)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Context without name', () => {
|
||||
/**
|
||||
* Test that contexts can be created without a name
|
||||
* The error message should use a generic fallback
|
||||
*/
|
||||
it('should create context without name and show generic error', () => {
|
||||
type TestContextValue = { value: string }
|
||||
const [, useTestContext] = createCtx<TestContextValue>()
|
||||
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => { /* suppress error */ })
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useTestContext())
|
||||
}).toThrow('No related context found.')
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user