Files
iofficeai--aionui/tests/unit/settings/SystemModalContent.dom.test.tsx
2026-07-13 13:20:22 +08:00

318 lines
11 KiB
TypeScript

/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ConfigProvider } from '@arco-design/web-react';
import { SWRConfig } from 'swr';
const { systemInfoMock, updateSystemInfoMock, restartMock, showOpenMock, messageInfoMock, configServiceMock } =
vi.hoisted(() => ({
systemInfoMock: vi.fn(),
updateSystemInfoMock: vi.fn(),
restartMock: vi.fn(),
showOpenMock: vi.fn(),
messageInfoMock: vi.fn(),
configServiceMock: {
get: vi.fn(() => undefined),
set: vi.fn(() => Promise.resolve()),
setLocal: vi.fn(),
},
}));
const clientBusinessSettingsMocks = vi.hoisted(() => ({
getClientBusinessSetting: vi.fn(),
setClientBusinessSetting: vi.fn(() => Promise.resolve()),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (k: string) => k, i18n: { language: 'en' } }),
}));
vi.mock('@/renderer/utils/platform', () => ({
isElectronDesktop: () => true,
}));
vi.mock('@/renderer/components/base/AionScrollArea', () => ({
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@/renderer/components/base/FeedbackButton', () => ({
default: () => <button type='button'>settings.oneClickFeedback</button>,
}));
vi.mock('@/renderer/components/settings/LanguageSwitcher', () => ({
default: () => <div>LanguageSwitcher</div>,
}));
vi.mock('@/renderer/components/settings/SettingsModal/contents/SystemModalContent/DevSettings', () => ({
default: () => <div>DevSettings</div>,
}));
vi.mock('@/renderer/services/clientBusinessSettings', () => ({
getClientBusinessSetting: clientBusinessSettingsMocks.getClientBusinessSetting,
setClientBusinessSetting: clientBusinessSettingsMocks.setClientBusinessSetting,
removeClientBusinessSetting: vi.fn(() => Promise.resolve()),
}));
vi.mock('@/common/config/configService', () => ({
configService: configServiceMock,
}));
vi.mock('@/common', () => ({
ipcBridge: {
application: {
systemInfo: { invoke: systemInfoMock },
updateSystemInfo: { invoke: updateSystemInfoMock },
restart: { invoke: restartMock },
getStartOnBootStatus: { invoke: vi.fn(() => Promise.resolve({ success: false })) },
getGpuStatus: { invoke: vi.fn(() => Promise.resolve({ success: false })) },
},
systemSettings: {
getCloseToTray: { invoke: vi.fn(() => Promise.resolve(false)) },
setCloseToTray: { invoke: vi.fn(() => Promise.resolve()) },
},
dialog: {
showOpen: { invoke: showOpenMock },
},
shell: {
openFolderWith: { invoke: vi.fn(() => Promise.resolve()) },
},
},
}));
vi.mock('@arco-design/web-react', async (importOriginal) => {
const actual = await importOriginal<typeof import('@arco-design/web-react')>();
return {
...actual,
Message: {
...actual.Message,
info: messageInfoMock,
},
Modal: {
...actual.Modal,
useModal: () => [
{
confirm: ({ onOk }: { onOk?: () => void }) => {
onOk?.();
},
},
null,
],
},
};
});
import SystemModalContent from '@/renderer/components/settings/SettingsModal/contents/SystemModalContent';
const defaultSystemInfo = {
cacheDir: '/cache',
workDir: '/work',
logDir: '/logs',
platform: 'darwin',
arch: 'arm64',
};
const renderContent = () =>
render(
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>
<ConfigProvider>
<SystemModalContent />
</ConfigProvider>
</SWRConfig>
);
describe('SystemModalContent directory settings', () => {
beforeEach(() => {
cleanup();
vi.clearAllMocks();
configServiceMock.get.mockImplementation(() => undefined);
configServiceMock.set.mockResolvedValue(undefined);
clientBusinessSettingsMocks.getClientBusinessSetting.mockImplementation(async (key: string) => {
if (key === 'acp.promptTimeout') return undefined;
if (key === 'acp.agentIdleTimeout') return undefined;
return undefined;
});
clientBusinessSettingsMocks.setClientBusinessSetting.mockResolvedValue(undefined);
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
systemInfoMock.mockResolvedValue(defaultSystemInfo);
updateSystemInfoMock.mockResolvedValue(undefined);
restartMock.mockResolvedValue({ restarted: true, manualRestartRequired: false });
showOpenMock.mockResolvedValue(['/new-logs']);
});
it('persists a selected log directory and restarts with the updated directory config', async () => {
const user = userEvent.setup();
const { container } = renderContent();
await screen.findByText('/logs');
const logDirItem = screen.getByText('settings.logDir').closest('.arco-form-item');
expect(logDirItem).not.toBeNull();
const pickButton = within(logDirItem as HTMLElement).getByRole('button');
await user.click(pickButton);
await waitFor(() => {
expect(updateSystemInfoMock).toHaveBeenCalledWith({
cacheDir: '/cache',
workDir: '/work',
logDir: '/new-logs',
});
});
expect(restartMock).toHaveBeenCalledTimes(1);
expect(container).toHaveTextContent('/new-logs');
});
it('shows the update failure reason when changing a directory fails', async () => {
const user = userEvent.setup();
updateSystemInfoMock.mockRejectedValueOnce(new Error('permission denied'));
showOpenMock.mockResolvedValueOnce(['/new-work']);
const { container } = renderContent();
await screen.findByText('/work');
const workDirItem = screen.getByText('settings.workDir').closest('.arco-form-item');
expect(workDirItem).not.toBeNull();
await user.click(within(workDirItem as HTMLElement).getByRole('button'));
await screen.findByText('permission denied');
expect(container).toHaveTextContent('/work');
expect(restartMock).not.toHaveBeenCalled();
});
it('opens the directory picker when clicking the work directory field body', async () => {
const user = userEvent.setup();
showOpenMock.mockResolvedValueOnce(['/new-work']);
renderContent();
await screen.findByText('/work');
const workDirItem = screen.getByText('settings.workDir').closest('.arco-form-item');
expect(workDirItem).not.toBeNull();
const fieldBody = (workDirItem as HTMLElement).querySelector('.aion-dir-input');
expect(fieldBody).not.toBeNull();
await user.click(fieldBody as HTMLElement);
await waitFor(() => {
expect(updateSystemInfoMock).toHaveBeenCalledWith({
cacheDir: '/cache',
workDir: '/new-work',
logDir: '/logs',
});
});
});
it('tells the user to restart manually when dev mode cannot relaunch automatically', async () => {
const user = userEvent.setup();
restartMock.mockResolvedValueOnce({ restarted: false, manualRestartRequired: true, reason: 'dev-mode' });
const { container } = renderContent();
await screen.findByText('/logs');
const logDirItem = screen.getByText('settings.logDir').closest('.arco-form-item');
expect(logDirItem).not.toBeNull();
await user.click(within(logDirItem as HTMLElement).getByRole('button'));
await waitFor(() => {
expect(updateSystemInfoMock).toHaveBeenCalledWith({
cacheDir: '/cache',
workDir: '/work',
logDir: '/new-logs',
});
});
expect(restartMock).toHaveBeenCalledTimes(1);
expect(messageInfoMock).toHaveBeenCalledWith('settings.restartManualRequired');
expect(container).toHaveTextContent('/new-logs');
});
it('shows field-specific tooltip text when hovering the folder action button', async () => {
const user = userEvent.setup();
renderContent();
await screen.findByText('/work');
const workDirItem = screen.getByText('settings.workDir').closest('.arco-form-item');
const logDirItem = screen.getByText('settings.logDir').closest('.arco-form-item');
expect(workDirItem).not.toBeNull();
expect(logDirItem).not.toBeNull();
const workDirButton = within(workDirItem as HTMLElement).getByRole('button');
const logDirButton = within(logDirItem as HTMLElement).getByRole('button');
await user.hover(workDirButton);
expect(await screen.findByText('settings.changeWorkDir')).toBeInTheDocument();
await user.unhover(workDirButton);
await user.hover(logDirButton);
expect(await screen.findByText('settings.changeLogDir')).toBeInTheDocument();
});
it('loads ACP timeouts from backend client settings', async () => {
clientBusinessSettingsMocks.getClientBusinessSetting.mockImplementation(async (key: string) => {
if (key === 'acp.promptTimeout') return 640;
if (key === 'acp.agentIdleTimeout') return 9;
return undefined;
});
renderContent();
expect(await screen.findByDisplayValue('640')).toBeInTheDocument();
expect(await screen.findByDisplayValue('9')).toBeInTheDocument();
});
it('does not fall back to legacy configService ACP timeout keys', async () => {
configServiceMock.get.mockImplementation((key: string) => {
if (key === 'acp.promptTimeout') return 777;
if (key === 'acp.agentIdleTimeout') return 13;
return undefined;
});
renderContent();
expect(await screen.findByDisplayValue('300')).toBeInTheDocument();
expect(await screen.findByDisplayValue('5')).toBeInTheDocument();
expect(configServiceMock.get).not.toHaveBeenCalledWith('acp.promptTimeout');
expect(configServiceMock.get).not.toHaveBeenCalledWith('acp.agentIdleTimeout');
});
it('persists ACP timeout changes through backend client settings', async () => {
const user = userEvent.setup();
renderContent();
const timeoutInputs = await screen.findAllByRole('spinbutton');
const promptTimeoutInput = timeoutInputs[0];
const agentIdleTimeoutInput = timeoutInputs[1];
await user.clear(promptTimeoutInput);
await user.type(promptTimeoutInput, '450');
fireEvent.blur(promptTimeoutInput);
await waitFor(() => {
expect(clientBusinessSettingsMocks.setClientBusinessSetting).toHaveBeenCalledWith('acp.promptTimeout', 450);
});
await user.clear(agentIdleTimeoutInput);
await user.type(agentIdleTimeoutInput, '7');
fireEvent.blur(agentIdleTimeoutInput);
await waitFor(() => {
expect(clientBusinessSettingsMocks.setClientBusinessSetting).toHaveBeenCalledWith('acp.agentIdleTimeout', 7);
});
});
});