@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
305 lines (247 loc) • 10.7 kB
text/typescript
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { fluxSchnellParamsSchema } from '@/config/paramsSchemas/fal/flux-schnell';
import { ModelParamsSchema, RuntimeImageGenParams } from '@/libs/standard-parameters/meta-schema';
import { useImageStore } from '@/store/image';
import { AIImageModelCard } from '@/types/aiModel';
import { useGenerationConfigParam } from './hooks';
// Mock external dependencies
vi.mock('@/store/aiInfra', () => ({
aiProviderSelectors: {
enabledImageModelList: vi.fn(() => [
{
id: 'fal',
name: 'Fal',
children: [
{
id: 'flux/schnell',
displayName: 'FLUX.1 Schnell',
type: 'image',
parameters: fluxSchnellParamsSchema,
releasedAt: '2024-08-01',
} as AIImageModelCard,
],
},
]),
},
getAiInfraStoreState: vi.fn(() => ({})),
}));
const testModelSchema: ModelParamsSchema = {
prompt: { default: '', description: 'The text prompt for image generation' },
width: { default: 1024, min: 512, max: 2048, step: 64, description: 'Image width' },
height: { default: 768, min: 256, max: 1536, step: 32, description: 'Image height' },
steps: { default: 20, min: 1, max: 50, description: 'Number of inference steps' },
seed: { default: null, min: 0, max: 2147483647, description: 'Random seed' },
cfg: { default: 7.5, min: 1, max: 20, step: 0.5, description: 'CFG scale' },
aspectRatio: {
default: '16:9',
enum: ['1:1', '16:9', '4:3', '9:16'],
description: 'Aspect ratio',
},
};
const testParameters: RuntimeImageGenParams = {
prompt: 'test prompt',
width: 1024,
height: 768,
steps: 25,
seed: 12345,
cfg: 8.0,
aspectRatio: '16:9',
};
describe('useGenerationConfigParam', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset store state before each test
useImageStore.setState({
parametersSchema: testModelSchema,
parameters: testParameters,
});
});
describe('value and setValue', () => {
it('should return current parameter value', () => {
const { result } = renderHook(() => useGenerationConfigParam('width'));
expect(result.current.value).toBe(1024);
});
it('should return current string parameter value', () => {
const { result } = renderHook(() => useGenerationConfigParam('prompt'));
expect(result.current.value).toBe('test prompt');
});
it('should return null for null values', () => {
useImageStore.setState({
parameters: { ...testParameters, seed: null },
});
const { result } = renderHook(() => useGenerationConfigParam('seed'));
expect(result.current.value).toBeNull();
});
it('should update parameter value using setValue', () => {
const { result } = renderHook(() => useGenerationConfigParam('width'));
act(() => {
result.current.setValue(2048);
});
const updatedParameters = useImageStore.getState().parameters;
expect(updatedParameters?.width).toBe(2048);
});
it('should update string parameter value using setValue', () => {
const { result } = renderHook(() => useGenerationConfigParam('prompt'));
act(() => {
result.current.setValue('new test prompt');
});
const updatedParameters = useImageStore.getState().parameters;
expect(updatedParameters?.prompt).toBe('new test prompt');
});
it('should handle array parameter updates', () => {
const { result } = renderHook(() => useGenerationConfigParam('imageUrls'));
act(() => {
result.current.setValue(['image1.jpg', 'image2.jpg']);
});
const updatedParameters = useImageStore.getState().parameters;
expect(updatedParameters?.imageUrls).toEqual(['image1.jpg', 'image2.jpg']);
});
});
describe('parameter constraints', () => {
it('should return min and max constraints for numeric parameter', () => {
const { result } = renderHook(() => useGenerationConfigParam('width'));
expect(result.current.min).toBe(512);
expect(result.current.max).toBe(2048);
expect(result.current.step).toBe(64);
expect(result.current.description).toBe('Image width');
expect(result.current.enumValues).toBeUndefined();
});
it('should return step constraint for decimal parameter', () => {
const { result } = renderHook(() => useGenerationConfigParam('cfg'));
expect(result.current.min).toBe(1);
expect(result.current.max).toBe(20);
expect(result.current.step).toBe(0.5);
expect(result.current.description).toBe('CFG scale');
});
it('should return enum values for enum parameter', () => {
const { result } = renderHook(() => useGenerationConfigParam('aspectRatio'));
expect(result.current.enumValues).toEqual(['1:1', '16:9', '4:3', '9:16']);
expect(result.current.description).toBe('Aspect ratio');
expect(result.current.min).toBeUndefined();
expect(result.current.max).toBeUndefined();
expect(result.current.step).toBeUndefined();
});
it('should return undefined constraints for parameter without constraints', () => {
const { result } = renderHook(() => useGenerationConfigParam('prompt'));
expect(result.current.min).toBeUndefined();
expect(result.current.max).toBeUndefined();
expect(result.current.step).toBeUndefined();
expect(result.current.enumValues).toBeUndefined();
expect(result.current.description).toBe('The text prompt for image generation');
});
it('should handle seed parameter with large range', () => {
const { result } = renderHook(() => useGenerationConfigParam('seed'));
expect(result.current.min).toBe(0);
expect(result.current.max).toBe(2147483647);
expect(result.current.step).toBeUndefined();
expect(result.current.description).toBe('Random seed');
});
});
describe('edge cases', () => {
it('should handle undefined parameters', () => {
useImageStore.setState({ parameters: undefined });
const { result } = renderHook(() => useGenerationConfigParam('width'));
expect(result.current.value).toBeUndefined();
});
it('should handle undefined parametersSchema', () => {
useImageStore.setState({ parametersSchema: undefined });
const { result } = renderHook(() => useGenerationConfigParam('width'));
expect(result.current.min).toBeUndefined();
expect(result.current.max).toBeUndefined();
expect(result.current.step).toBeUndefined();
expect(result.current.description).toBeUndefined();
expect(result.current.enumValues).toBeUndefined();
});
it('should handle parameter not in current parametersSchema', () => {
useImageStore.setState({
parametersSchema: { prompt: { default: '' } }, // Only prompt defined
});
const { result } = renderHook(() => useGenerationConfigParam('width'));
expect(result.current.min).toBeUndefined();
expect(result.current.max).toBeUndefined();
expect(result.current.step).toBeUndefined();
expect(result.current.enumValues).toBeUndefined();
});
});
describe('flux/schnell real-world parameters', () => {
it('should work with actual flux/schnell parameters', () => {
useImageStore.setState({
parametersSchema: fluxSchnellParamsSchema,
parameters: {
prompt: 'A beautiful landscape',
width: 1024,
height: 1024,
steps: 4,
seed: null,
},
});
// Test prompt parameter
const { result: promptResult } = renderHook(() => useGenerationConfigParam('prompt'));
expect(promptResult.current.value).toBe('A beautiful landscape');
// flux-schnell's prompt parameter doesn't have description
// Test width parameter
const { result: widthResult } = renderHook(() => useGenerationConfigParam('width'));
expect(widthResult.current.value).toBe(1024);
expect(widthResult.current.min).toBeDefined();
expect(widthResult.current.max).toBeDefined();
// Test steps parameter
const { result: stepsResult } = renderHook(() => useGenerationConfigParam('steps'));
expect(stepsResult.current.value).toBe(4);
expect(stepsResult.current.min).toBeDefined();
expect(stepsResult.current.max).toBeDefined();
// Test seed parameter
const { result: seedResult } = renderHook(() => useGenerationConfigParam('seed'));
expect(seedResult.current.value).toBeNull();
});
it('should update flux/schnell parameters correctly', () => {
useImageStore.setState({
parametersSchema: fluxSchnellParamsSchema,
parameters: {
prompt: 'original prompt',
width: 512,
height: 512,
steps: 1,
seed: null,
},
});
const { result: promptResult } = renderHook(() => useGenerationConfigParam('prompt'));
const { result: widthResult } = renderHook(() => useGenerationConfigParam('width'));
// Update prompt
act(() => {
promptResult.current.setValue('updated prompt');
});
// Update width
act(() => {
widthResult.current.setValue(1024);
});
const updatedParameters = useImageStore.getState().parameters;
expect(updatedParameters?.prompt).toBe('updated prompt');
expect(updatedParameters?.width).toBe(1024);
});
});
describe('setValue callback stability', () => {
it('should maintain setValue callback reference when value changes', () => {
const { result, rerender } = renderHook(() => useGenerationConfigParam('width'));
const initialSetValue = result.current.setValue;
// Change the parameter value
act(() => {
result.current.setValue(2048);
});
rerender();
// setValue callback should remain the same reference
expect(result.current.setValue).toBe(initialSetValue);
});
it('should maintain setValue callback reference when other parameters change', () => {
const { result: widthResult } = renderHook(() => useGenerationConfigParam('width'));
const { result: promptResult } = renderHook(() => useGenerationConfigParam('prompt'));
const initialWidthSetValue = widthResult.current.setValue;
// Change a different parameter
act(() => {
promptResult.current.setValue('new prompt');
});
// setValue callback for width should remain the same reference
expect(widthResult.current.setValue).toBe(initialWidthSetValue);
});
});
});