dify/web/utils/context.spec.ts

254 lines
8.3 KiB
TypeScript

/**
* 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()
})
})
})