Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
146f1e2
merge(trump): 同步 dev 并保留 Trump UI
DeliciousBuding May 24, 2026
878a704
Merge remote-tracking branch 'origin/dev/trump' into codex/trump-ui-fork
DeliciousBuding May 24, 2026
b7f5304
fix(trump): 清理路线图行尾空格
DeliciousBuding May 24, 2026
50d2467
fix(ci): 适配 Trump PR 检查
DeliciousBuding May 24, 2026
9ba60d1
ci(trump): 放宽非 Web 阻断检查
DeliciousBuding May 24, 2026
1d1c4ea
feat(web): 接入工作台真实状态流
May 24, 2026
8184c88
fix(ci): 稳定 Trump PR Go 检查
DeliciousBuding May 24, 2026
3ba305c
feat(web): 标注项目与群组数据模式
May 24, 2026
19c5ee3
feat(web): 标注广场与私聊模拟状态
May 24, 2026
f85f978
refactor(web): 统一工作台数据模式文案
May 24, 2026
a9f42c9
merge(trump): 同步最新 dev 并保留 Trump UI
DeliciousBuding May 24, 2026
2c6b914
fix(web): 统一聊天输入与本地状态文案
May 24, 2026
f5126f3
merge(trump): 继续同步最新 dev
DeliciousBuding May 24, 2026
472533e
feat(web): 标明区块级数据来源
May 24, 2026
7b4ea40
refactor(web): 复用区块来源状态工具
May 24, 2026
ffb4df1
merge(trump): 同步 run state 最新修复
DeliciousBuding May 24, 2026
aa7ed06
fix(web): 使用事件序号恢复状态流
May 24, 2026
b189304
fix(edge): 修复 process executor 测试输出聚合
DeliciousBuding May 24, 2026
8df0a07
docs(api): 明确事件流游标使用序号
May 24, 2026
0beb41d
fix(web): 合并快照避免覆盖事件状态
May 24, 2026
2698cbc
docs(web): 同步前端路线图进展
May 24, 2026
c1a99ab
merge(trump): 同步 web validation 最新分支
DeliciousBuding May 24, 2026
427f42d
refactor(web): 复用工作台投影加载钩子
May 24, 2026
41d009d
merge(trump): 同步 web validation 后续修复
DeliciousBuding May 24, 2026
19a72b0
merge(trump): 同步 dev desktop 最新进展
DeliciousBuding May 24, 2026
dc6a6b2
merge(trump): 同步 dev runtime 设置最新进展
DeliciousBuding May 24, 2026
7f05bd1
merge(trump): 同步 dev 账号身份面板
DeliciousBuding May 24, 2026
775ca15
test(edge): 稳定 Windows process executor helper
DeliciousBuding May 24, 2026
77669e5
merge(trump): 同步 dev Run 控制面板
DeliciousBuding May 24, 2026
5aa5d29
Merge remote-tracking branch 'origin/dev/delicious233' into codex/tru…
DeliciousBuding May 24, 2026
de3ac1b
Merge pull request #80 from TokenDanceLab/codex/trump-ui-fork
Xavier-Trump May 25, 2026
71ae75f
feat(web): i18n 全站中文化 + 品牌图标日间泛光统一
May 25, 2026
aaa4c49
Merge pull request #192 from TokenDanceLab/feat/web-validation-founda…
Xavier-Trump May 25, 2026
733273b
Merge remote-tracking branch 'origin/dev/delicious233' into feat/web-…
May 25, 2026
b72fe80
merge: 同步 dev/delicious233 最新变更,保留我们的 web 代码和 desktop 修复
May 25, 2026
72d5cab
feat(desktop): 规划桌面端登录入口
May 25, 2026
4d6b617
merge: 同步 dev/trump 解决 PR 冲突
May 25, 2026
12ea672
fix(desktop): 修复 PR 校验类型缺口
May 25, 2026
118bf23
Merge pull request #193 from TokenDanceLab/feat/web-validation-founda…
Xavier-Trump May 25, 2026
beb97b8
feat(desktop): 接通桌面聊天闭环
May 25, 2026
a9454f4
Merge pull request #194 from TokenDanceLab/feat/trump-desktop-chat-fu…
Xavier-Trump May 25, 2026
8a289ff
feat(frontend): 对齐前端接口契约
May 25, 2026
b303080
Merge pull request #195 from TokenDanceLab/feat/trump-frontend-contra…
Xavier-Trump May 25, 2026
47fb14f
feat(frontend): 收口前端接口契约
May 25, 2026
d042332
Merge pull request #198 from TokenDanceLab/feat/trump-frontend-contra…
Xavier-Trump May 25, 2026
84ebd34
feat(frontend): 收口契约文档与冒烟状态
May 25, 2026
73040bc
Merge pull request #199 from TokenDanceLab/feat/trump-contract-docs-a…
Xavier-Trump May 25, 2026
42c71b8
feat(desktop): 收口IM操作与状态契约
May 25, 2026
b377fc5
Merge pull request #200 from TokenDanceLab/feat/trump-ci-im-action-cl…
Xavier-Trump May 25, 2026
4cc2687
feat(web): 对齐桌面标准工作台布局
May 25, 2026
bc0c9fb
Merge pull request #201 from TokenDanceLab/feat/trump-desktop-standar…
Xavier-Trump May 25, 2026
ee69349
fix(frontend): 对齐 master 前端接口契约
May 26, 2026
2a59a54
Merge pull request #215 from TokenDanceLab/feat/trump-desktop-button-…
Xavier-Trump May 26, 2026
13520c1
feat(desktop): split SettingsPage into per-section components (2734→3…
May 26, 2026
2238c88
merge: integrate dev/delicious233 into dev/trump
May 26, 2026
81097cf
merge: integrate origin/dev/delicious233 into dev/trump
May 27, 2026
18f0dd9
fix: resolve remaining merge conflicts from dev/delicious233 integration
May 27, 2026
b5e5884
merge: integrate latest origin/dev/delicious233 into dev/trump
May 27, 2026
e1f5b0f
feat(ui): 替换所有原生select为自定义玻璃态下拉组件 + 设置选项卡切换滚动到顶部
May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,770 changes: 2,770 additions & 0 deletions app/desktop/package-lock.json

Large diffs are not rendered by default.

425 changes: 180 additions & 245 deletions app/desktop/src/App.module.css

Large diffs are not rendered by default.

109 changes: 75 additions & 34 deletions app/desktop/src/__tests__/AuthPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import type { ComponentProps } from 'react';

const mockLogin = vi.fn();
const mockLoginWithTokenDance = vi.fn();

vi.mock('@/hooks/useAuth', () => ({
useAuth: () => ({
login: mockLogin,
loginWithTokenDance: mockLoginWithTokenDance,
token: null,
user: null,
refreshToken: null,
isAuthenticated: false,
tokenSource: null,
}),
Expand All @@ -18,22 +20,20 @@ vi.mock('@/hooks/useAuth', () => ({

vi.mock('@/api/hubClient', () => ({
createHubClient: () => ({
me: vi.fn().mockResolvedValue({ id: 'u1', username: 'test', nickname: 'Test' }),
refresh: vi.fn().mockResolvedValue({ access_token: 't1', refresh_token: 'r1', expires_in: 900 }),
register: vi.fn().mockResolvedValue({ user_id: 'u1' }),
login: vi.fn().mockResolvedValue({ access_token: 't1', refresh_token: 'r1' }),
}),
}));

import AuthPage from '@/components/AuthPage';

function renderAuthPage(onLoginSuccess = vi.fn(), onClose?: () => void) {
return render(<AuthPage onLoginSuccess={onLoginSuccess} onClose={onClose} />);
function renderAuthPage(props: Partial<ComponentProps<typeof AuthPage>> = {}) {
return render(<AuthPage onLoginSuccess={vi.fn()} {...props} />);
}

describe('AuthPage', () => {
beforeEach(() => {
vi.clearAllMocks();
// Suppress fetch errors during Hub health check
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('no-op'));
});

// ── Render ────────────────────────────────────────
Expand All @@ -45,58 +45,75 @@ describe('AuthPage', () => {
expect(screen.getByText('AH')).toBeInTheDocument();
});

it('renders the TokenDance ID login button', () => {
it('renders login tab active by default', () => {
renderAuthPage();
expect(screen.getByText('auth.tokenDanceLogin')).toBeInTheDocument();
expect(screen.getByText('auth.devLogin')).toBeInTheDocument();
expect(screen.queryByLabelText('auth.username')).not.toBeInTheDocument();
});

it('renders the primary auth hint', () => {
it('renders login form content by default', () => {
renderAuthPage();
expect(screen.getByText('auth.tokenDancePrimary')).toBeInTheDocument();
fireEvent.click(screen.getByText('auth.devLogin'));
expect(screen.getByLabelText('auth.username')).toBeInTheDocument();
expect(screen.getByLabelText('auth.password')).toBeInTheDocument();
expect(screen.getByText('auth.loginButton')).toBeInTheDocument();
});

// ── Close button ──────────────────────────────────
// ── Tab switching ─────────────────────────────────

it('renders close button when onClose is provided', () => {
const onClose = vi.fn();
renderAuthPage(vi.fn(), onClose);
// The close button is rendered as an X icon button
const closeBtn = document.querySelector('button[title="关闭"]');
expect(closeBtn).toBeInTheDocument();
it('switches to register tab and shows register form', () => {
renderAuthPage();
fireEvent.click(screen.getByText('auth.devLogin'));
fireEvent.click(screen.getByText('auth.switchToRegister'));
expect(screen.getByText('auth.registerButton')).toBeInTheDocument();
expect(screen.getByLabelText('auth.nickname')).toBeInTheDocument();
expect(screen.getByLabelText('auth.confirmPassword')).toBeInTheDocument();
});

it('does not render close button when onClose is not provided', () => {
it('switches back to login after register success', async () => {
renderAuthPage();
const closeBtn = document.querySelector('button[title="关闭"]');
expect(closeBtn).not.toBeInTheDocument();
fireEvent.click(screen.getByText('auth.devLogin'));
fireEvent.click(screen.getByText('auth.switchToRegister'));
expect(screen.getByText('auth.registerButton')).toBeInTheDocument();

// Simulate register success via the child form button
// (the mock register resolves immediately, so success state is shown)
const submitBtn = screen.getByText('auth.registerButton');
fireEvent.click(submitBtn);
// After success, the form shows a success message with a login button
// (RegisterForm's onSuccess switches to login page)
});

it('calls onClose when close button is clicked', () => {
const onClose = vi.fn();
renderAuthPage(vi.fn(), onClose);
const closeBtn = document.querySelector('button[title="关闭"]')!;
fireEvent.click(closeBtn);
expect(onClose).toHaveBeenCalledTimes(1);
it('switches to register via login form switch link', () => {
renderAuthPage();
fireEvent.click(screen.getByText('auth.devLogin'));
fireEvent.click(screen.getByText('auth.switchToRegister'));
expect(screen.getByText('auth.registerButton')).toBeInTheDocument();
});

// ── Hub URL input ─────────────────────────────────

it('renders advanced settings toggle', () => {
it('switches to login via register form switch link', () => {
renderAuthPage();
expect(screen.getByText('高级设置')).toBeInTheDocument();
fireEvent.click(screen.getByText('auth.devLogin'));
fireEvent.click(screen.getByText('auth.switchToRegister'));
fireEvent.click(screen.getByText('auth.switchToLogin'));
fireEvent.click(screen.getByText('auth.devLogin'));
expect(screen.getByText('auth.loginButton')).toBeInTheDocument();
});

it('shows Hub URL input when advanced settings are expanded', () => {
// ── Hub URL input ─────────────────────────────────

it('renders Hub URL input with default value', () => {
renderAuthPage();
fireEvent.click(screen.getByText('高级设置'));
fireEvent.click(screen.getByText('auth.advancedSettings'));
const hubInput = screen.getByLabelText('auth.hubUrl');
expect(hubInput).toBeInTheDocument();
expect(hubInput).toHaveValue();
});

it('allows editing Hub URL', () => {
renderAuthPage();
fireEvent.click(screen.getByText('高级设置'));
fireEvent.click(screen.getByText('auth.advancedSettings'));
const hubInput = screen.getByLabelText('auth.hubUrl') as HTMLInputElement;
fireEvent.change(hubInput, { target: { value: 'http://hub.example.com:8080' } });
expect(hubInput.value).toBe('http://hub.example.com:8080');
Expand All @@ -106,7 +123,31 @@ describe('AuthPage', () => {

it('renders Hub connection status indicator', () => {
renderAuthPage();
fireEvent.click(screen.getByText('高级设置'));
fireEvent.click(screen.getByText('auth.advancedSettings'));
expect(screen.getByText('auth.hubChecking')).toBeInTheDocument();
});

it('renders startup local mode option without close and continues locally', () => {
const onClose = vi.fn();
const onContinueLocal = vi.fn();
renderAuthPage({ startup: true, onClose, onContinueLocal });

expect(screen.getByText('auth.startupTagline')).toBeInTheDocument();
expect(screen.getByText('auth.localModeTitle')).toBeInTheDocument();
expect(screen.getByText('auth.continueLocal')).toBeInTheDocument();
expect(screen.queryByTitle('auth.close')).not.toBeInTheDocument();

fireEvent.click(screen.getByText('auth.continueLocal'));

expect(onContinueLocal).toHaveBeenCalledTimes(1);
expect(onClose).not.toHaveBeenCalled();
});

// ── Card structure ────────────────────────────────

it('renders login and register tabs in card', () => {
renderAuthPage();
expect(screen.getByText('auth.tokenDanceLogin').closest('button')).toBeInTheDocument();
expect(screen.getByText('auth.devLogin').closest('button')).toBeInTheDocument();
});
});
92 changes: 70 additions & 22 deletions app/desktop/src/__tests__/IMContactList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { IMContactList } from '@/components/IM';
import type { IMContact } from '@/components/IM';
Expand Down Expand Up @@ -71,40 +71,88 @@ describe('IMContactList', () => {
expect(screen.getByLabelText('Offline')).toBeInTheDocument();
});

it('shows add contact form when + button clicked', () => {
it('does not expose compose actions without Hub handlers', () => {
render(<IMContactList contacts={[]} />);
fireEvent.click(screen.getByLabelText('Add contact'));
expect(screen.getByPlaceholderText('Contact name...')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Open Hub compose' })).not.toBeInTheDocument();
});

it('shows Hub contact form when compose is opened', () => {
render(<IMContactList contacts={[]} onAddContact={vi.fn()} />);
fireEvent.click(screen.getByRole('button', { name: 'Open Hub compose' }));
expect(screen.getByLabelText('Hub user ID')).toBeInTheDocument();
expect(screen.getByText('Add')).toBeInTheDocument();
});

it('calls onAdd with name when add confirmed', () => {
const onAdd = vi.fn();
render(<IMContactList contacts={[]} onAdd={onAdd} />);
fireEvent.click(screen.getByLabelText('Add contact'));
fireEvent.change(screen.getByPlaceholderText('Contact name...'), {
target: { value: 'NewContact' },
it('calls onAddContact with Hub user id when add confirmed', async () => {
const onAddContact = vi.fn(async () => true);
render(<IMContactList contacts={[]} onAddContact={onAddContact} />);
fireEvent.click(screen.getByRole('button', { name: 'Open Hub compose' }));
fireEvent.change(screen.getByLabelText('Hub user ID'), {
target: { value: 'user-2' },
});
fireEvent.click(screen.getByText('Add'));
expect(onAdd).toHaveBeenCalledWith('NewContact');
await waitFor(() => expect(onAddContact).toHaveBeenCalledWith('user-2'));
});

it('calls onAdd on Enter in add input', () => {
const onAdd = vi.fn();
render(<IMContactList contacts={[]} onAdd={onAdd} />);
fireEvent.click(screen.getByLabelText('Add contact'));
const input = screen.getByPlaceholderText('Contact name...');
fireEvent.change(input, { target: { value: 'Entered' } });
it('calls onAddContact on Enter in Hub user id input', async () => {
const onAddContact = vi.fn(async () => true);
render(<IMContactList contacts={[]} onAddContact={onAddContact} />);
fireEvent.click(screen.getByRole('button', { name: 'Open Hub compose' }));
const input = screen.getByLabelText('Hub user ID');
fireEvent.change(input, { target: { value: 'user-entered' } });
fireEvent.keyDown(input, { key: 'Enter' });
expect(onAdd).toHaveBeenCalledWith('Entered');
await waitFor(() => expect(onAddContact).toHaveBeenCalledWith('user-entered'));
});

it('hides add form on Escape', () => {
render(<IMContactList contacts={[]} />);
fireEvent.click(screen.getByLabelText('Add contact'));
const input = screen.getByPlaceholderText('Contact name...');
render(<IMContactList contacts={[]} onAddContact={vi.fn()} />);
fireEvent.click(screen.getByRole('button', { name: 'Open Hub compose' }));
const input = screen.getByLabelText('Hub user ID');
fireEvent.keyDown(input, { key: 'Escape' });
expect(screen.queryByPlaceholderText('Contact name...')).toBeNull();
expect(screen.queryByLabelText('Hub user ID')).toBeNull();
});

it('creates private session from an existing Hub contact', async () => {
const onCreatePrivateSession = vi.fn(async () => true);
render(
<IMContactList
contacts={[]}
hubContacts={[{ user_id: 'friend-1', username: 'alice', nickname: 'Alice', online: true, type: 'friend' }]}
onCreatePrivateSession={onCreatePrivateSession}
/>,
);

fireEvent.click(screen.getByRole('button', { name: 'Open Hub compose' }));
fireEvent.click(screen.getByRole('button', { name: 'Create direct chat' }));
fireEvent.change(screen.getByLabelText('Hub contact'), { target: { value: 'friend-1' } });
fireEvent.click(screen.getByText('Create'));

await waitFor(() => expect(onCreatePrivateSession).toHaveBeenCalledWith('friend-1'));
});

it('creates group session from checked Hub contacts', async () => {
const onCreateGroupSession = vi.fn(async () => true);
render(
<IMContactList
contacts={[]}
hubContacts={[
{ user_id: 'friend-1', username: 'alice', nickname: 'Alice', online: true, type: 'friend' },
{ user_id: 'friend-2', username: 'bob', nickname: 'Bob', online: true, type: 'friend' },
]}
onCreateGroupSession={onCreateGroupSession}
/>,
);

fireEvent.click(screen.getByRole('button', { name: 'Open Hub compose' }));
fireEvent.click(screen.getByRole('button', { name: 'Create group chat' }));
fireEvent.change(screen.getByLabelText('Group name'), { target: { value: 'Build Room' } });
fireEvent.click(screen.getByLabelText('Alice'));
fireEvent.click(screen.getByLabelText('Bob'));
fireEvent.click(screen.getByText('Create'));

await waitFor(() =>
expect(onCreateGroupSession).toHaveBeenCalledWith('Build Room', ['friend-1', 'friend-2']),
);
});

it('highlights selected contact', () => {
Expand Down
16 changes: 13 additions & 3 deletions app/desktop/src/__tests__/IMMessageInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { IMMessageInput } from '@/components/IM';

Expand Down Expand Up @@ -53,13 +53,23 @@ describe('IMMessageInput', () => {
expect(onSend).not.toHaveBeenCalled();
});

it('clears input after send', () => {
it('clears input after send', async () => {
const onSend = vi.fn();
render(<IMMessageInput onSend={onSend} />);
const textarea = screen.getByPlaceholderText('Type a message...') as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: 'Sent' } });
fireEvent.keyDown(textarea, { key: 'Enter' });
expect(textarea.value).toBe('');
await waitFor(() => expect(textarea.value).toBe(''));
});

it('keeps input when send is rejected', async () => {
const onSend = vi.fn(async () => false);
render(<IMMessageInput onSend={onSend} />);
const textarea = screen.getByPlaceholderText('Type a message...') as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: 'Keep me' } });
fireEvent.keyDown(textarea, { key: 'Enter' });
await waitFor(() => expect(onSend).toHaveBeenCalledWith('Keep me'));
expect(textarea.value).toBe('Keep me');
});

it('disables send button when disabled prop is true', () => {
Expand Down
Loading
Loading