import { act, render, renderHook, screen } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { BaseProviders } from "@/test/test-utils"
import { useUserSettings } from "../../hooks/use-user-settings"
import { UserSettingsProvider } from "../../services/user-settings-provider"
// Создаем моковый объект для send
const mockSend = vi.fn()
// Создаем моковый объект для состояния
const mockState = {
context: {
activeTab: "media",
layoutMode: "default",
screenshotsPath: "public/screenshots",
playerScreenshotsPath: "public/media",
playerVolume: 100,
openAiApiKey: "",
claudeApiKey: "",
isBrowserVisible: true,
isTimelineVisible: true,
isOptionsVisible: true,
isLoaded: true,
previewSizes: {
MEDIA: 100,
TRANSITIONS: 100,
TEMPLATES: 100,
},
// GPU и производительность
gpuAccelerationEnabled: true,
preferredGpuEncoder: "h264",
maxConcurrentJobs: 4,
renderQuality: "high",
backgroundRenderingEnabled: true,
renderDelay: 0,
// Настройки прокси
proxyEnabled: false,
proxyType: "http",
proxyHost: "",
proxyPort: "",
proxyUsername: "",
proxyPassword: "",
// Настройки автосохранения
autoSaveEnabled: true,
autoSaveInterval: 300,
},
status: "active",
}
// Создаем мок состояния для AppProvider
const mockAppState = {
context: {
isConnected: true,
error: null,
projectState: null,
},
matches: vi.fn(() => false),
}
const mockAppSend = vi.fn()
// Счетчик вызовов useMachine для различения машин
let useMachineCallCount = 0
// Мокаем useMachine из @xstate/react
vi.mock("@xstate/react", () => ({
useMachine: vi.fn(() => {
useMachineCallCount++
// Первый вызов - это appMachine из AppProvider
if (useMachineCallCount === 1) {
return [mockAppState, mockAppSend]
}
// Последующие вызовы - это userSettingsMachine
return [mockState, mockSend]
}),
}))
// Мокаем userSettingsMachine
vi.mock("../../services/user-settings-machine", () => ({
userSettingsMachine: {
createMachine: vi.fn(),
},
}))
// Мокаем useAppSettings
vi.mock("@/features/app-state/hooks/use-app-settings", () => ({
useAppSettings: vi.fn(() => ({
settings: {},
state: {
context: {
isLoading: false,
currentProject: {
path: "/path/to/project",
},
},
},
send: vi.fn(),
updateUserSettings: vi.fn(),
})),
}))
// Мокаем AppSettingsProvider и useAppState
vi.mock("@/features/app-state", () => ({
AppSettingsProvider: ({ children }: any) => children,
useAppSettings: vi.fn(() => ({
settings: {},
state: {
context: {
isLoading: false,
currentProject: {
path: "/path/to/project",
},
},
},
send: vi.fn(),
updateUserSettings: vi.fn(),
})),
useAppState: vi.fn(() => ({
state: {
context: {
isConnected: true,
error: null,
projectState: null,
},
matches: vi.fn(() => false),
},
send: vi.fn(),
})),
}))
// Мокаем сохранение настроек
vi.mock("../../utils/user-settings-storage", () => ({
saveUserSettings: vi.fn(),
loadUserSettings: vi.fn(() => ({})),
}))
// Мокаем console.log и console.error
vi.spyOn(console, "log").mockImplementation(() => {})
vi.spyOn(console, "error").mockImplementation(() => {})
// Компонент-обертка для тестирования хука useUserSettings
const UserSettingsWrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
describe("UserSettingsProvider", () => {
beforeEach(() => {
vi.clearAllMocks()
// Сбрасываем счетчик вызовов useMachine
useMachineCallCount = 0
// Сбрасываем состояние мока перед каждым тестом
Object.assign(mockState.context, {
activeTab: "media",
layoutMode: "default",
screenshotsPath: "public/screenshots",
playerScreenshotsPath: "public/media",
playerVolume: 100,
openAiApiKey: "",
claudeApiKey: "",
isBrowserVisible: true,
isTimelineVisible: true,
isOptionsVisible: true,
isLoaded: true,
previewSizes: {
MEDIA: 100,
TRANSITIONS: 100,
TEMPLATES: 100,
},
// GPU и производительность
gpuAccelerationEnabled: true,
preferredGpuEncoder: "h264",
maxConcurrentJobs: 4,
renderQuality: "high",
backgroundRenderingEnabled: true,
renderDelay: 0,
// Настройки прокси
proxyEnabled: false,
proxyType: "http",
proxyHost: "",
proxyPort: "",
proxyUsername: "",
proxyPassword: "",
// Настройки автосохранения
autoSaveEnabled: true,
autoSaveInterval: 300,
})
})
it("should render children", () => {
render(
Test Child
,
)
expect(screen.getByTestId("test-child")).toBeInTheDocument()
})
it("should provide UserSettingsContext", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Проверяем, что контекст содержит ожидаемые свойства
expect(result.current).toBeDefined()
expect(result.current.activeTab).toBe("media")
expect(result.current.layoutMode).toBe("default")
expect(result.current.screenshotsPath).toBe("public/screenshots")
expect(result.current.playerScreenshotsPath).toBe("public/media")
expect(result.current.playerVolume).toBe(100)
expect(result.current.openAiApiKey).toBe("")
expect(result.current.claudeApiKey).toBe("")
expect(result.current.isBrowserVisible).toBe(true)
expect(result.current.isTimelineVisible).toBe(true)
expect(result.current.isOptionsVisible).toBe(true)
})
it("should provide methods for interacting with user settings", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Проверяем наличие всех методов
expect(result.current.handleTabChange).toBeDefined()
expect(typeof result.current.handleTabChange).toBe("function")
expect(result.current.handleLayoutChange).toBeDefined()
expect(typeof result.current.handleLayoutChange).toBe("function")
expect(result.current.handleScreenshotsPathChange).toBeDefined()
expect(typeof result.current.handleScreenshotsPathChange).toBe("function")
expect(result.current.handlePlayerScreenshotsPathChange).toBeDefined()
expect(typeof result.current.handlePlayerScreenshotsPathChange).toBe("function")
expect(result.current.handlePlayerVolumeChange).toBeDefined()
expect(typeof result.current.handlePlayerVolumeChange).toBe("function")
expect(result.current.handleAiApiKeyChange).toBeDefined()
expect(typeof result.current.handleAiApiKeyChange).toBe("function")
expect(result.current.handleClaudeApiKeyChange).toBeDefined()
expect(typeof result.current.handleClaudeApiKeyChange).toBe("function")
expect(result.current.toggleBrowserVisibility).toBeDefined()
expect(typeof result.current.toggleBrowserVisibility).toBe("function")
expect(result.current.toggleTimelineVisibility).toBeDefined()
expect(typeof result.current.toggleTimelineVisibility).toBe("function")
expect(result.current.toggleOptionsVisibility).toBeDefined()
expect(typeof result.current.toggleOptionsVisibility).toBe("function")
})
it("should throw error when useUserSettings is used outside of provider", () => {
// Проверяем, что хук выбрасывает ошибку, если используется вне провайдера
const consoleError = console.error
console.error = vi.fn() // Подавляем ошибки в консоли во время теста
expect(() => renderHook(() => useUserSettings())).toThrow(
"useUserSettings must be used within a UserSettingsProvider",
)
console.error = consoleError // Восстанавливаем console.error
})
it("should call send with UPDATE_ACTIVE_TAB event when handleTabChange is called", () => {
// Очищаем моковый объект перед тестом
mockSend.mockClear()
// Используем renderHook для тестирования хука useUserSettings
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Вызываем метод изменения вкладки
act(() => {
result.current.handleTabChange("music")
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_ACTIVE_TAB",
tab: "music",
})
})
it("should handle layout change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Изменяем макет
act(() => {
result.current.handleLayoutChange("vertical")
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_LAYOUT",
layoutMode: "vertical",
})
})
it("should handle screenshots path change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Изменяем путь скриншотов
act(() => {
result.current.handleScreenshotsPathChange("new/path")
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_SCREENSHOTS_PATH",
path: "new/path",
})
})
it("should handle player volume change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Изменяем громкость плеера
act(() => {
result.current.handlePlayerVolumeChange(75)
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PLAYER_VOLUME",
volume: 75,
})
})
it("should handle timeline visibility toggle", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Переключаем видимость временной шкалы
act(() => {
result.current.toggleTimelineVisibility()
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "TOGGLE_TIMELINE_VISIBILITY",
})
})
it("should handle options visibility toggle", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Переключаем видимость опций
act(() => {
result.current.toggleOptionsVisibility()
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "TOGGLE_OPTIONS_VISIBILITY",
})
})
it("should handle Claude API key change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Изменяем Claude API ключ
act(() => {
result.current.handleClaudeApiKeyChange("claude-test-key")
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_CLAUDE_API_KEY",
apiKey: "claude-test-key",
})
})
it("should handle AI API key change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Изменяем API ключ
act(() => {
result.current.handleAiApiKeyChange("test-api-key")
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_OPENAI_API_KEY",
apiKey: "test-api-key",
})
})
it("should handle player screenshots path change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Изменяем путь скриншотов плеера
act(() => {
result.current.handlePlayerScreenshotsPathChange("new/player/path")
})
// Проверяем, что send был вызван с правильными параметрами
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PLAYER_SCREENSHOTS_PATH",
path: "new/player/path",
})
})
describe("handleTabChange", () => {
it("should handle invalid tab values", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
// Вызываем с недопустимым значением
act(() => {
result.current.handleTabChange("invalid-tab")
})
// Проверяем, что send не был вызван
expect(mockSend).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith("Invalid tab value:", "invalid-tab")
})
it("should update tab and save settings for valid values", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleTabChange("effects")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_ACTIVE_TAB",
tab: "effects",
})
})
})
describe("handleLayoutChange", () => {
it("should handle invalid layout values", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleLayoutChange("invalid-layout" as any)
})
expect(mockSend).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith("Invalid layout value:", "invalid-layout")
})
it("should update layout and save settings for valid values", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleLayoutChange("chat")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_LAYOUT",
layoutMode: "chat",
})
})
})
describe("GPU and Performance Settings", () => {
it("should handle GPU acceleration change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleGpuAccelerationChange(false)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_GPU_ACCELERATION",
enabled: false,
})
})
it("should handle preferred GPU encoder change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handlePreferredGpuEncoderChange("h265")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PREFERRED_GPU_ENCODER",
encoder: "h265",
})
})
it("should handle max concurrent jobs change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleMaxConcurrentJobsChange(8)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_MAX_CONCURRENT_JOBS",
jobs: 8,
})
})
it("should handle render quality change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleRenderQualityChange("medium")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_RENDER_QUALITY",
quality: "medium",
})
})
it("should handle background rendering change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleBackgroundRenderingChange(false)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_BACKGROUND_RENDERING",
enabled: false,
})
})
it("should handle render delay change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleRenderDelayChange(500)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_RENDER_DELAY",
delay: 500,
})
})
})
describe("Proxy Settings", () => {
it("should handle proxy enabled change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleProxyEnabledChange(true)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PROXY_ENABLED",
enabled: true,
})
})
it("should handle proxy type change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleProxyTypeChange("socks5")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PROXY_TYPE",
proxyType: "socks5",
})
})
it("should handle proxy host change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleProxyHostChange("proxy.example.com")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PROXY_HOST",
host: "proxy.example.com",
})
})
it("should handle proxy port change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleProxyPortChange("8080")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PROXY_PORT",
port: "8080",
})
})
it("should handle proxy username change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleProxyUsernameChange("user123")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PROXY_USERNAME",
username: "user123",
})
})
it("should handle proxy password change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleProxyPasswordChange("password123")
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_PROXY_PASSWORD",
password: "password123",
})
})
})
describe("AutoSave Settings", () => {
it("should handle auto save enabled change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleAutoSaveEnabledChange(false)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_AUTO_SAVE_ENABLED",
enabled: false,
})
})
it("should handle auto save interval change", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.handleAutoSaveIntervalChange(600)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_AUTO_SAVE_INTERVAL",
interval: 600,
})
})
})
describe("toggleBrowserVisibility", () => {
it("should toggle browser visibility", () => {
mockSend.mockClear()
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
act(() => {
result.current.toggleBrowserVisibility()
})
expect(mockSend).toHaveBeenCalledWith({
type: "TOGGLE_BROWSER_VISIBILITY",
})
})
})
describe("Additional GPU and Performance properties in context", () => {
it("should provide all GPU and performance properties", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
expect(result.current.gpuAccelerationEnabled).toBe(true)
expect(result.current.preferredGpuEncoder).toBe("h264")
expect(result.current.maxConcurrentJobs).toBe(4)
expect(result.current.renderQuality).toBe("high")
expect(result.current.backgroundRenderingEnabled).toBe(true)
expect(result.current.renderDelay).toBe(0)
})
})
describe("Additional proxy properties in context", () => {
it("should provide all proxy properties", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
expect(result.current.proxyEnabled).toBe(false)
expect(result.current.proxyType).toBe("http")
expect(result.current.proxyHost).toBe("")
expect(result.current.proxyPort).toBe("")
expect(result.current.proxyUsername).toBe("")
expect(result.current.proxyPassword).toBe("")
})
})
describe("Additional autosave properties in context", () => {
it("should provide all autosave properties", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
expect(result.current.autoSaveEnabled).toBe(true)
expect(result.current.autoSaveInterval).toBe(300)
})
})
describe("All handler methods should be defined", () => {
it("should provide all GPU handler methods", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
expect(result.current.handleGpuAccelerationChange).toBeDefined()
expect(typeof result.current.handleGpuAccelerationChange).toBe("function")
expect(result.current.handlePreferredGpuEncoderChange).toBeDefined()
expect(typeof result.current.handlePreferredGpuEncoderChange).toBe("function")
expect(result.current.handleMaxConcurrentJobsChange).toBeDefined()
expect(typeof result.current.handleMaxConcurrentJobsChange).toBe("function")
expect(result.current.handleRenderQualityChange).toBeDefined()
expect(typeof result.current.handleRenderQualityChange).toBe("function")
expect(result.current.handleBackgroundRenderingChange).toBeDefined()
expect(typeof result.current.handleBackgroundRenderingChange).toBe("function")
expect(result.current.handleRenderDelayChange).toBeDefined()
expect(typeof result.current.handleRenderDelayChange).toBe("function")
})
it("should provide all proxy handler methods", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
expect(result.current.handleProxyEnabledChange).toBeDefined()
expect(typeof result.current.handleProxyEnabledChange).toBe("function")
expect(result.current.handleProxyTypeChange).toBeDefined()
expect(typeof result.current.handleProxyTypeChange).toBe("function")
expect(result.current.handleProxyHostChange).toBeDefined()
expect(typeof result.current.handleProxyHostChange).toBe("function")
expect(result.current.handleProxyPortChange).toBeDefined()
expect(typeof result.current.handleProxyPortChange).toBe("function")
expect(result.current.handleProxyUsernameChange).toBeDefined()
expect(typeof result.current.handleProxyUsernameChange).toBe("function")
expect(result.current.handleProxyPasswordChange).toBeDefined()
expect(typeof result.current.handleProxyPasswordChange).toBe("function")
})
it("should provide all autosave handler methods", () => {
const { result } = renderHook(() => useUserSettings(), {
wrapper: UserSettingsWrapper,
})
expect(result.current.handleAutoSaveEnabledChange).toBeDefined()
expect(typeof result.current.handleAutoSaveEnabledChange).toBe("function")
expect(result.current.handleAutoSaveIntervalChange).toBeDefined()
expect(typeof result.current.handleAutoSaveIntervalChange).toBe("function")
})
})
})