@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.
352 lines (285 loc) • 10.3 kB
text/typescript
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fluxSchnellParamsSchema } from '@/config/aiModels/fal';
import {
ModelParamsSchema,
RuntimeImageGenParams,
extractDefaultValues,
} from '@/libs/standard-parameters/index';
import { useImageStore } from '@/store/image';
import { AIImageModelCard } from '@/types/aiModel';
// 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,
],
},
{
id: 'custom-provider',
name: 'Custom Provider',
children: [
{
id: 'custom-model',
displayName: 'Custom Model',
type: 'image',
parameters: {
prompt: { default: '' },
width: { default: 1024, min: 256, max: 2048, step: 64 },
height: { default: 1024, min: 256, max: 2048, step: 64 },
steps: { default: 20, min: 1, max: 50 },
} as ModelParamsSchema,
releasedAt: '2024-01-01',
} as AIImageModelCard,
],
},
]),
},
getAiInfraStoreState: vi.fn(() => ({})),
}));
const fluxSchnellDefaultValues = extractDefaultValues(fluxSchnellParamsSchema);
const customModelSchema: ModelParamsSchema = {
prompt: { default: '' },
width: { default: 1024, min: 256, max: 2048, step: 64 },
height: { default: 1024, min: 256, max: 2048, step: 64 },
steps: { default: 20, min: 1, max: 50 },
};
beforeEach(() => {
vi.clearAllMocks();
// Reset store state
useImageStore.setState({
model: 'initial-model',
provider: 'initial-provider',
imageNum: 1,
parameters: {
prompt: 'initial prompt',
width: 512,
height: 512,
} as RuntimeImageGenParams,
parametersSchema: {
prompt: { default: '' },
width: { default: 512, min: 256, max: 1024 },
height: { default: 512, min: 256, max: 1024 },
},
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('GenerationConfigAction', () => {
describe('setParamOnInput', () => {
it('should update a single parameter in the parameters object', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setParamOnInput('prompt', 'new test prompt');
});
expect(result.current.parameters).toMatchObject({
prompt: 'new test prompt',
width: 512,
height: 512,
});
});
it('should update numeric parameters correctly', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setParamOnInput('width', 2048);
});
expect(result.current.parameters).toMatchObject({
prompt: 'initial prompt',
width: 2048,
height: 512,
});
});
it('should handle null values correctly', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setParamOnInput('seed', null);
});
expect(result.current.parameters?.seed).toBeNull();
});
it('should handle array values correctly', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setParamOnInput('imageUrls', ['test1.jpg', 'test2.jpg']);
});
expect(result.current.parameters?.imageUrls).toEqual(['test1.jpg', 'test2.jpg']);
});
});
describe('setModelAndProviderOnSelect', () => {
it('should set model, provider, parameters and parametersSchema for flux/schnell', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setModelAndProviderOnSelect('flux/schnell', 'fal');
});
expect(result.current.model).toBe('flux/schnell');
expect(result.current.provider).toBe('fal');
expect(result.current.parameters).toEqual(fluxSchnellDefaultValues);
expect(result.current.parametersSchema).toEqual(fluxSchnellParamsSchema);
});
it('should handle model selection with custom parameters', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setModelAndProviderOnSelect('custom-model', 'custom-provider');
});
const expectedParams = extractDefaultValues(customModelSchema);
expect(result.current.model).toBe('custom-model');
expect(result.current.provider).toBe('custom-provider');
expect(result.current.parameters).toEqual(expectedParams);
expect(result.current.parametersSchema).toEqual(customModelSchema);
});
it('should replace all previous parameters with new model defaults', async () => {
const { result } = renderHook(() => useImageStore());
// First set some custom parameters
act(() => {
result.current.setParamOnInput('prompt', 'custom prompt');
result.current.setParamOnInput('steps', 50);
});
// Then switch model
act(() => {
result.current.setModelAndProviderOnSelect('flux/schnell', 'fal');
});
// Should completely replace parameters with model defaults
expect(result.current.parameters).toEqual(fluxSchnellDefaultValues);
expect(result.current.parameters?.prompt).toBe(''); // Default value, not 'custom prompt'
});
});
describe('setImageNum', () => {
it('should update the imageNum value', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setImageNum(4);
});
expect(result.current.imageNum).toBe(4);
});
it('should handle different imageNum values', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setImageNum(8);
});
expect(result.current.imageNum).toBe(8);
act(() => {
result.current.setImageNum(1);
});
expect(result.current.imageNum).toBe(1);
});
it('should handle edge case values', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.setImageNum(0);
});
expect(result.current.imageNum).toBe(0);
});
});
describe('reuseSettings', () => {
it('should set model, provider and merge settings with default values', async () => {
const { result } = renderHook(() => useImageStore());
const customSettings: Partial<RuntimeImageGenParams> = {
prompt: 'custom prompt',
steps: 8,
seed: 54321,
};
act(() => {
result.current.reuseSettings('flux/schnell', 'fal', customSettings);
});
expect(result.current.model).toBe('flux/schnell');
expect(result.current.provider).toBe('fal');
expect(result.current.parameters).toEqual({
...fluxSchnellDefaultValues,
...customSettings,
});
expect(result.current.parametersSchema).toEqual(fluxSchnellParamsSchema);
});
it('should override default values with provided settings', async () => {
const { result } = renderHook(() => useImageStore());
const customSettings: Partial<RuntimeImageGenParams> = {
width: 1536,
height: 1536,
};
act(() => {
result.current.reuseSettings('flux/schnell', 'fal', customSettings);
});
expect(result.current.parameters).toEqual({
...fluxSchnellDefaultValues,
width: 1536,
height: 1536,
});
});
it('should handle empty settings object', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.reuseSettings('flux/schnell', 'fal', {});
});
expect(result.current.parameters).toEqual(fluxSchnellDefaultValues);
});
it('should handle partial settings with null values', async () => {
const { result } = renderHook(() => useImageStore());
const customSettings: Partial<RuntimeImageGenParams> = {
seed: null,
imageUrl: null,
};
act(() => {
result.current.reuseSettings('flux/schnell', 'fal', customSettings);
});
expect(result.current.parameters?.seed).toBeNull();
expect(result.current.parameters?.imageUrl).toBeNull();
});
});
describe('reuseSeed', () => {
it('should update only the seed parameter', async () => {
const { result } = renderHook(() => useImageStore());
const newSeed = 98765;
act(() => {
result.current.reuseSeed(newSeed);
});
expect(result.current.parameters).toMatchObject({
prompt: 'initial prompt',
width: 512,
height: 512,
seed: newSeed,
});
});
it('should preserve other parameters when updating seed', async () => {
const { result } = renderHook(() => useImageStore());
// First set some parameters
act(() => {
result.current.setParamOnInput('prompt', 'test prompt');
result.current.setParamOnInput('width', 1024);
});
const newSeed = 11111;
act(() => {
result.current.reuseSeed(newSeed);
});
expect(result.current.parameters).toMatchObject({
prompt: 'test prompt',
width: 1024,
height: 512,
seed: newSeed,
});
});
it('should handle seed value of 0', async () => {
const { result } = renderHook(() => useImageStore());
act(() => {
result.current.reuseSeed(0);
});
expect(result.current.parameters?.seed).toBe(0);
});
it('should handle large seed values within range', async () => {
const { result } = renderHook(() => useImageStore());
const largeSeed = 2147483647; // MAX_SEED
act(() => {
result.current.reuseSeed(largeSeed);
});
expect(result.current.parameters?.seed).toBe(largeSeed);
});
});
});