feat(core): initialize project skeleton with Electron + React + TypeScript

Set up the complete project foundation for ClawX, a graphical AI assistant:

- Electron main process with IPC handlers, menu, tray, and gateway management
- React renderer with routing, layout components, and page scaffolding
- Zustand state management for gateway, settings, channels, skills, chat, and cron
- shadcn/ui components with Tailwind CSS and CSS variable theming
- Build tooling with Vite, electron-builder, and TypeScript configuration
- Testing setup with Vitest and Playwright
- Development configurations (ESLint, Prettier, gitignore, env example)
This commit is contained in:
Haze
2026-02-05 23:09:17 +08:00
Unverified
parent 9442e5f77a
commit b8ab0208d0
71 changed files with 14086 additions and 3 deletions

44
tests/setup.ts Normal file
View File

@@ -0,0 +1,44 @@
/**
* Vitest Test Setup
* Global test configuration and mocks
*/
import { vi } from 'vitest';
import '@testing-library/jest-dom';
// Mock window.electron API
const mockElectron = {
ipcRenderer: {
invoke: vi.fn(),
on: vi.fn(),
once: vi.fn(),
off: vi.fn(),
},
openExternal: vi.fn(),
platform: 'darwin',
isDev: true,
};
Object.defineProperty(window, 'electron', {
value: mockElectron,
writable: true,
});
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Reset mocks after each test
afterEach(() => {
vi.clearAllMocks();
});

75
tests/unit/stores.test.ts Normal file
View File

@@ -0,0 +1,75 @@
/**
* Zustand Stores Tests
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useSettingsStore } from '@/stores/settings';
import { useGatewayStore } from '@/stores/gateway';
describe('Settings Store', () => {
beforeEach(() => {
// Reset store to default state
useSettingsStore.setState({
theme: 'system',
language: 'en',
sidebarCollapsed: false,
devModeUnlocked: false,
gatewayAutoStart: true,
gatewayPort: 18789,
autoCheckUpdate: true,
autoDownloadUpdate: false,
startMinimized: false,
launchAtStartup: false,
updateChannel: 'stable',
});
});
it('should have default values', () => {
const state = useSettingsStore.getState();
expect(state.theme).toBe('system');
expect(state.sidebarCollapsed).toBe(false);
expect(state.gatewayAutoStart).toBe(true);
});
it('should update theme', () => {
const { setTheme } = useSettingsStore.getState();
setTheme('dark');
expect(useSettingsStore.getState().theme).toBe('dark');
});
it('should toggle sidebar collapsed state', () => {
const { setSidebarCollapsed } = useSettingsStore.getState();
setSidebarCollapsed(true);
expect(useSettingsStore.getState().sidebarCollapsed).toBe(true);
});
it('should unlock dev mode', () => {
const { setDevModeUnlocked } = useSettingsStore.getState();
setDevModeUnlocked(true);
expect(useSettingsStore.getState().devModeUnlocked).toBe(true);
});
});
describe('Gateway Store', () => {
beforeEach(() => {
// Reset store
useGatewayStore.setState({
status: { state: 'stopped', port: 18789 },
isInitialized: false,
});
});
it('should have default status', () => {
const state = useGatewayStore.getState();
expect(state.status.state).toBe('stopped');
expect(state.status.port).toBe(18789);
});
it('should update status', () => {
const { setStatus } = useGatewayStore.getState();
setStatus({ state: 'running', port: 18789, pid: 12345 });
const state = useGatewayStore.getState();
expect(state.status.state).toBe('running');
expect(state.status.pid).toBe(12345);
});
});

45
tests/unit/utils.test.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* Utility Functions Tests
*/
import { describe, it, expect } from 'vitest';
import { cn, formatRelativeTime, formatDuration, truncate } from '@/lib/utils';
describe('cn (class name merge)', () => {
it('should merge class names', () => {
expect(cn('foo', 'bar')).toBe('foo bar');
});
it('should handle conditional classes', () => {
expect(cn('base', true && 'active')).toBe('base active');
expect(cn('base', false && 'active')).toBe('base');
});
it('should merge tailwind classes correctly', () => {
expect(cn('px-2', 'px-4')).toBe('px-4');
expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500');
});
});
describe('formatDuration', () => {
it('should format seconds only', () => {
expect(formatDuration(45)).toBe('45s');
});
it('should format minutes and seconds', () => {
expect(formatDuration(125)).toBe('2m 5s');
});
it('should format hours and minutes', () => {
expect(formatDuration(3725)).toBe('1h 2m');
});
});
describe('truncate', () => {
it('should not truncate short text', () => {
expect(truncate('Hello', 10)).toBe('Hello');
});
it('should truncate long text with ellipsis', () => {
expect(truncate('Hello World!', 8)).toBe('Hello...');
});
});