import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { ThemeProvider } from '@mui/material/styles';
import { lightTheme } from '@services/theme/defaultTheme';
import { BehaviorSubject } from 'rxjs';
import { defaultPreferences } from '@services/preferences/defaultPreferences';
import type { IPreferences } from '@services/preferences/interface';
import { SupportedStorageServices } from '@services/types';
import type { IWorkspace } from '@services/workspaces/interface';
import { TidGiMiniWindow } from '../TidGiMiniWindow';
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
);
const mockWorkspaces: IWorkspace[] = [
{
id: 'workspace-1',
name: 'Test Workspace 1',
wikiFolderLocation: '/test/workspace1',
homeUrl: 'http://localhost:5212/',
port: 5212,
isSubWiki: false,
mainWikiToLink: null,
tagName: null,
lastUrl: null,
active: true,
hibernated: false,
order: 0,
disableNotifications: false,
backupOnInterval: false,
disableAudio: false,
enableHTTPAPI: false,
excludedPlugins: [],
gitUrl: null,
hibernateWhenUnused: false,
readOnlyMode: false,
storageService: SupportedStorageServices.local,
subWikiFolderName: 'subwiki',
syncOnInterval: false,
syncOnStartup: false,
tokenAuth: false,
transparentBackground: false,
userName: '',
picturePath: null,
},
{
id: 'workspace-2',
name: 'Test Workspace 2',
wikiFolderLocation: '/test/workspace2',
homeUrl: 'http://localhost:5213/',
port: 5213,
isSubWiki: false,
mainWikiToLink: null,
tagName: null,
lastUrl: null,
active: false,
hibernated: false,
order: 1,
disableNotifications: false,
backupOnInterval: false,
disableAudio: false,
enableHTTPAPI: false,
excludedPlugins: [],
gitUrl: null,
hibernateWhenUnused: false,
readOnlyMode: false,
storageService: SupportedStorageServices.local,
subWikiFolderName: 'subwiki',
syncOnInterval: false,
syncOnStartup: false,
tokenAuth: false,
transparentBackground: false,
userName: '',
picturePath: null,
},
];
// Reuse defaultPreferences to avoid duplication
const createMockPreference = (overrides: Partial = {}): IPreferences => ({
...defaultPreferences,
...overrides,
});
describe('TidGiMiniWindow Component', () => {
let preferenceSubject: BehaviorSubject;
beforeEach(() => {
vi.clearAllMocks();
preferenceSubject = new BehaviorSubject(
createMockPreference({ tidgiMiniWindow: false }),
);
Object.defineProperty(window.observables.preference, 'preference$', {
value: preferenceSubject.asObservable(),
writable: true,
});
Object.defineProperty(window.service.context, 'get', {
value: vi.fn().mockResolvedValue('win32'),
writable: true,
});
Object.defineProperty(window.service.workspace, 'getWorkspacesAsList', {
value: vi.fn().mockResolvedValue(mockWorkspaces),
writable: true,
});
Object.defineProperty(window.service.preference, 'set', {
value: vi.fn(async (key: string, value: unknown) => {
const currentPreference = preferenceSubject.value;
if (currentPreference) {
preferenceSubject.next({ ...currentPreference, [key]: value });
}
}),
writable: true,
});
Object.defineProperty(window.service.native, 'registerKeyboardShortcut', {
value: vi.fn().mockResolvedValue(undefined),
writable: true,
});
Object.defineProperty(window.service.native, 'unregisterKeyboardShortcut', {
value: vi.fn().mockResolvedValue(undefined),
writable: true,
});
});
const renderComponent = async () => {
const result = render(
,
);
// Wait for component to fully load and stabilize
await waitFor(() => {
expect(screen.queryByText('Loading')).not.toBeInTheDocument();
});
// Ensure component is fully rendered
await waitFor(() => {
expect(screen.getByText('Menu.TidGiMiniWindow')).toBeInTheDocument();
});
return result;
};
describe('Initial state and loading', () => {
it('should show loading state when preference is undefined', () => {
// Create a fresh BehaviorSubject with undefined for this specific test
const loadingPreferenceSubject = new BehaviorSubject(undefined);
Object.defineProperty(window.observables.preference, 'preference$', {
value: loadingPreferenceSubject.asObservable(),
writable: true,
configurable: true,
});
const { unmount } = render(
,
);
// Verify loading state is shown when preference is undefined
expect(screen.getByText('Loading')).toBeInTheDocument();
// Immediately unmount to prevent any async updates
unmount();
});
it('should render after loading with preferences', async () => {
await renderComponent();
expect(screen.getByText('Menu.TidGiMiniWindow')).toBeInTheDocument();
});
it('should load platform information from backend on mount', async () => {
await renderComponent();
expect(window.service.context.get).toHaveBeenCalledWith('platform');
});
it('should load workspace list from backend on mount', async () => {
await renderComponent();
expect(window.service.workspace.getWorkspacesAsList).toHaveBeenCalled();
});
it('should display correct attach to tidgi mini window text for Windows', async () => {
Object.defineProperty(window.service.context, 'get', {
value: vi.fn().mockResolvedValue('win32'),
writable: true,
});
await renderComponent();
expect(screen.getByText('Preference.AttachToTaskbar')).toBeInTheDocument();
});
it('should display correct attach to tidgi mini window text for macOS', async () => {
Object.defineProperty(window.service.context, 'get', {
value: vi.fn().mockResolvedValue('darwin'),
writable: true,
});
await renderComponent();
expect(screen.getByText('Preference.TidgiMiniWindow')).toBeInTheDocument();
});
});
describe('Attach to tidgi mini window toggle', () => {
it('should display attach to tidgi mini window switch with correct initial state', async () => {
preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
await renderComponent();
await waitFor(() => {
expect(screen.getAllByRole('switch')).toHaveLength(1);
});
const switches = screen.getAllByRole('switch');
const attachSwitch = switches[0];
expect(attachSwitch).not.toBeChecked();
});
it('should call backend API when attach to tidgi mini window is toggled', async () => {
const user = userEvent.setup();
preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
await renderComponent();
await waitFor(() => {
expect(screen.getAllByRole('switch')).toHaveLength(1);
});
const switches = screen.getAllByRole('switch');
const attachSwitch = switches[0];
await user.click(attachSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindow', true);
});
});
it('should toggle attach to tidgi mini window setting', async () => {
const user = userEvent.setup();
preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
await renderComponent();
await waitFor(() => {
expect(screen.getAllByRole('switch')).toHaveLength(1);
});
const switches = screen.getAllByRole('switch');
const attachSwitch = switches[0];
await user.click(attachSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindow', true);
});
});
});
describe('Conditional settings visibility', () => {
it('should hide additional settings when tidgiMiniWindow is false', async () => {
preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
await renderComponent();
expect(screen.queryByText('Preference.AttachToTaskbarShowSidebar')).not.toBeInTheDocument();
expect(screen.queryByText('Preference.TidgiMiniWindowShowSidebar')).not.toBeInTheDocument();
expect(screen.queryByText('Preference.TidgiMiniWindowAlwaysOnTop')).not.toBeInTheDocument();
expect(screen.queryByText('Preference.TidgiMiniWindowSyncWorkspaceWithMainWindow')).not.toBeInTheDocument();
});
it('should show additional settings when tidgiMiniWindow is true', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowShowSidebar: false,
tidgiMiniWindowAlwaysOnTop: false,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Changed to false to show sidebar option
}),
);
await renderComponent();
expect(screen.getByText('Preference.AttachToTaskbarShowSidebar')).toBeInTheDocument();
expect(screen.getByText('Preference.TidgiMiniWindowAlwaysOnTop')).toBeInTheDocument();
expect(screen.getByText('Preference.TidgiMiniWindowSyncWorkspaceWithMainWindow')).toBeInTheDocument();
});
});
describe('Sidebar on tidgi mini window toggle', () => {
it('should display sidebar toggle with correct initial state', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowShowSidebar: false,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Must be false to show sidebar option
}),
);
await renderComponent();
const sidebarSwitchContainer = screen.getByTestId('sidebar-on-tidgi-mini-window-switch');
const sidebarSwitch = sidebarSwitchContainer.querySelector('input[type="checkbox"]');
expect(sidebarSwitch).not.toBeChecked();
});
it('should call backend API when sidebar toggle is changed', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowShowSidebar: false,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Must be false to show sidebar option
}),
);
await renderComponent();
const sidebarSwitchContainer = screen.getByTestId('sidebar-on-tidgi-mini-window-switch');
const sidebarSwitch = sidebarSwitchContainer.querySelector('input[type="checkbox"]');
if (!sidebarSwitch) throw new Error('Switch input not found');
await user.click(sidebarSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowShowSidebar', true);
});
});
});
describe('Always on top toggle', () => {
it('should display always on top toggle with correct initial state', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowAlwaysOnTop: false,
}),
);
await renderComponent();
const alwaysOnTopSwitchContainer = screen.getByTestId('tidgi-mini-window-always-on-top-switch');
const alwaysOnTopSwitch = alwaysOnTopSwitchContainer.querySelector('input[type="checkbox"]');
expect(alwaysOnTopSwitch).not.toBeChecked();
});
it('should call backend API when always on top is toggled', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowAlwaysOnTop: false,
}),
);
await renderComponent();
const alwaysOnTopSwitchContainer = screen.getByTestId('tidgi-mini-window-always-on-top-switch');
const alwaysOnTopSwitch = alwaysOnTopSwitchContainer.querySelector('input[type="checkbox"]');
if (!alwaysOnTopSwitch) throw new Error('Switch input not found');
await user.click(alwaysOnTopSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowAlwaysOnTop', true);
});
});
});
describe('Workspace sync toggle', () => {
it('should display workspace sync toggle with correct initial state', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
}),
);
await renderComponent();
const syncSwitchContainer = screen.getByTestId('tidgi-mini-window-sync-workspace-switch');
const syncSwitch = syncSwitchContainer.querySelector('input[type="checkbox"]');
expect(syncSwitch).toBeChecked();
});
it('should call backend API when workspace sync is toggled', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
}),
);
await renderComponent();
const syncSwitchContainer = screen.getByTestId('tidgi-mini-window-sync-workspace-switch');
const syncSwitch = syncSwitchContainer.querySelector('input[type="checkbox"]');
if (!syncSwitch) throw new Error('Switch input not found');
await user.click(syncSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowSyncWorkspaceWithMainWindow', false);
});
});
it('should hide fixed workspace selector when sync is enabled', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
}),
);
await renderComponent();
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
});
it('should show fixed workspace selector when sync is disabled', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
}),
);
await renderComponent();
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
});
describe('Fixed workspace selector', () => {
it('should display workspace list in selector', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
}),
);
await renderComponent();
const select = screen.getByRole('combobox');
await user.click(select);
await waitFor(() => {
expect(screen.getByText('Test Workspace 1')).toBeInTheDocument();
expect(screen.getByText('Test Workspace 2')).toBeInTheDocument();
});
});
it('should have workspace selector that can trigger API calls', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
tidgiMiniWindowFixedWorkspaceId: '',
}),
);
await renderComponent();
// Verify workspace selector is present with proper configuration
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
// Verify the selector displays the placeholder/empty state
const container = select.closest('.MuiFormControl-root');
expect(container).toBeInTheDocument();
});
it('should display currently selected workspace', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
tidgiMiniWindowFixedWorkspaceId: 'workspace-2',
}),
);
const { container } = await renderComponent();
// MUI Select stores value in a hidden input with name attribute or as data attribute
const selectDiv = container.querySelector('.MuiSelect-select') as HTMLDivElement;
expect(selectDiv).toBeTruthy();
// Check if the selected workspace name is displayed
expect(selectDiv.textContent).toBe('Test Workspace 2');
});
});
describe('Keyboard shortcut registration', () => {
it('should display keyboard shortcut button when tidgi mini window is attached', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
keyboardShortcuts: {},
}),
);
await renderComponent();
const shortcutButton = screen.getByRole('button', { name: /Preference\.TidgiMiniWindowShortcutKey/ });
expect(shortcutButton).toBeInTheDocument();
});
it('should display current keyboard shortcut value', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
keyboardShortcuts: {
'Window.toggleTidgiMiniWindow': 'Ctrl+Shift+M',
},
}),
);
await renderComponent();
expect(screen.getByText(/Ctrl\+Shift\+M/)).toBeInTheDocument();
});
it('should call registerKeyboardShortcut API when new shortcut is set', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
keyboardShortcuts: {},
}),
);
await renderComponent();
const shortcutButton = screen.getByRole('button', { name: /Preference\.TidgiMiniWindowShortcutKey/ });
const mockOnChange = vi.fn(async (value: string) => {
if (value && value.trim() !== '') {
await window.service.native.registerKeyboardShortcut('Window', 'toggleTidgiMiniWindow', value);
} else {
await window.service.native.unregisterKeyboardShortcut('Window', 'toggleTidgiMiniWindow');
}
});
shortcutButton.onclick = () => {
void mockOnChange('Ctrl+Shift+T');
};
await user.click(shortcutButton);
await waitFor(() => {
expect(window.service.native.registerKeyboardShortcut).toHaveBeenCalledWith('Window', 'toggleTidgiMiniWindow', 'Ctrl+Shift+T');
});
});
it('should call unregisterKeyboardShortcut API when shortcut is cleared', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
keyboardShortcuts: {
'Window.toggleTidgiMiniWindow': 'Ctrl+Shift+M',
},
}),
);
await renderComponent();
const shortcutButton = screen.getByRole('button', { name: /Preference\.TidgiMiniWindowShortcutKey/ });
const mockOnChange = vi.fn(async (value: string) => {
if (value && value.trim() !== '') {
await window.service.native.registerKeyboardShortcut('Window', 'toggleTidgiMiniWindow', value);
} else {
await window.service.native.unregisterKeyboardShortcut('Window', 'toggleTidgiMiniWindow');
}
});
shortcutButton.onclick = () => {
void mockOnChange('');
};
await user.click(shortcutButton);
await waitFor(() => {
expect(window.service.native.unregisterKeyboardShortcut).toHaveBeenCalledWith('Window', 'toggleTidgiMiniWindow');
});
});
it('should display helper text for keyboard shortcut', async () => {
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
keyboardShortcuts: {},
}),
);
await renderComponent();
expect(screen.getByText('Preference.TidgiMiniWindowShortcutKeyHelperText')).toBeInTheDocument();
});
});
describe('Integration: Toggle sequence', () => {
it('should show all settings when tidgiMiniWindow is toggled on', async () => {
const user = userEvent.setup();
// Create a fresh subject for this test to avoid interference
const toggleTestSubject = new BehaviorSubject(
createMockPreference({ tidgiMiniWindow: false }),
);
Object.defineProperty(window.observables.preference, 'preference$', {
value: toggleTestSubject.asObservable(),
writable: true,
configurable: true,
});
// Mock preference.set to update our test subject
Object.defineProperty(window.service.preference, 'set', {
value: vi.fn(async (key: string, value: unknown) => {
const currentPreference = toggleTestSubject.value;
if (currentPreference) {
toggleTestSubject.next({ ...currentPreference, [key]: value });
}
}),
writable: true,
});
render(
,
);
// Wait for component to fully load
await waitFor(() => {
expect(screen.getByText('Menu.TidGiMiniWindow')).toBeInTheDocument();
});
// Verify additional settings are hidden initially
expect(screen.queryByText('Preference.TidgiMiniWindowAlwaysOnTop')).not.toBeInTheDocument();
// Wait for and click the attach to tidgi mini window toggle
await waitFor(() => {
expect(screen.getAllByRole('switch')).toHaveLength(1);
});
const switches = screen.getAllByRole('switch');
const attachSwitch = switches[0];
await user.click(attachSwitch);
// Wait for API call
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindow', true);
});
// Now verify new elements appear (they should appear automatically after the state update)
await waitFor(() => {
expect(screen.getByText('Preference.TidgiMiniWindowAlwaysOnTop')).toBeInTheDocument();
expect(screen.getByText('Preference.TidgiMiniWindowSyncWorkspaceWithMainWindow')).toBeInTheDocument();
});
});
it('should handle multiple switch toggles correctly', async () => {
const user = userEvent.setup();
preferenceSubject.next(
createMockPreference({
tidgiMiniWindow: true,
tidgiMiniWindowShowSidebar: false,
tidgiMiniWindowShowTitleBar: true,
tidgiMiniWindowAlwaysOnTop: false,
tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Changed to false so sidebar option is visible
}),
);
await renderComponent();
// Use test IDs and find actual input elements
const sidebarSwitchContainer = screen.getByTestId('sidebar-on-tidgi-mini-window-switch');
const sidebarSwitch = sidebarSwitchContainer.querySelector('input[type="checkbox"]');
if (!sidebarSwitch) throw new Error('Sidebar switch not found');
await user.click(sidebarSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowShowSidebar', true);
});
const alwaysOnTopSwitchContainer = screen.getByTestId('tidgi-mini-window-always-on-top-switch');
const alwaysOnTopSwitch = alwaysOnTopSwitchContainer.querySelector('input[type="checkbox"]');
if (!alwaysOnTopSwitch) throw new Error('Always on top switch not found');
await user.click(alwaysOnTopSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowAlwaysOnTop', true);
});
const syncSwitchContainer = screen.getByTestId('tidgi-mini-window-sync-workspace-switch');
const syncSwitch = syncSwitchContainer.querySelector('input[type="checkbox"]');
if (!syncSwitch) throw new Error('Sync switch not found');
await user.click(syncSwitch);
await waitFor(() => {
expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowSyncWorkspaceWithMainWindow', true);
});
expect(window.service.preference.set).toHaveBeenCalledTimes(3);
});
});
});