mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
- Updated HandoffButton to support framework-specific CLI prompts and improved local project path handling. - Enhanced DesignBrowserPanel to manage browser history with favicon support and improved address display. - Introduced new utility functions for formatting addresses and extracting hostnames. - Refactored CSS styles for better layout and responsiveness across components. - Added tests for new functionalities in HandoffButton and DesignBrowserPanel, ensuring robust behavior. These changes improve user experience by streamlining the handoff process and enhancing the design browsing capabilities within the application.
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
// Regression for the zero-editors fallback: when no editor is detected, the
|
|
// fallback button must perform a real reveal (open the project folder via the
|
|
// daemon's open-in catalogue: finder / explorer / file-manager) rather than a
|
|
// no-op that advertises an action it never runs.
|
|
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { HandoffButton } from '../../src/components/HandoffButton';
|
|
import { I18nProvider } from '../../src/i18n';
|
|
import type { AgentInfo, HostEditorsResponse } from '@open-design/contracts';
|
|
|
|
const fetchHostEditors = vi.fn<() => Promise<HostEditorsResponse>>();
|
|
const openProjectInEditor = vi.fn();
|
|
const copyToClipboard = vi.fn();
|
|
|
|
vi.mock('../../src/providers/registry', () => ({
|
|
fetchHostEditors: () => fetchHostEditors(),
|
|
openProjectInEditor: (...args: unknown[]) => openProjectInEditor(...args),
|
|
}));
|
|
|
|
vi.mock('../../src/lib/copy-to-clipboard', () => ({
|
|
copyToClipboard: (...args: unknown[]) => copyToClipboard(...args),
|
|
}));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
fetchHostEditors.mockReset();
|
|
openProjectInEditor.mockReset();
|
|
copyToClipboard.mockReset();
|
|
});
|
|
|
|
describe('HandoffButton zero-editors fallback', () => {
|
|
it('opens the project folder in the OS file manager via the daemon', async () => {
|
|
fetchHostEditors.mockResolvedValue({
|
|
platform: 'darwin',
|
|
editors: [],
|
|
});
|
|
openProjectInEditor.mockResolvedValue(undefined);
|
|
|
|
render(
|
|
<I18nProvider initial="en">
|
|
<HandoffButton projectId="p1" />
|
|
</I18nProvider>,
|
|
);
|
|
|
|
const fallback = (await screen.findByText('Finder')).closest('button') as HTMLButtonElement;
|
|
fireEvent.click(fallback);
|
|
|
|
await waitFor(() => expect(openProjectInEditor).toHaveBeenCalledWith('p1', 'finder'));
|
|
});
|
|
|
|
it('surfaces a daemon spawn failure inline so the fallback is not a silent no-op', async () => {
|
|
// The production caller (`ProjectView`) mounts `<HandoffButton projectId={…} />`
|
|
// with no `onRequestRevealInFinder` callback, so a rejected
|
|
// `openProjectInEditor` would otherwise leave users with a CTA that
|
|
// advertises Finder/Explorer/File Manager but does nothing visible.
|
|
fetchHostEditors.mockResolvedValue({
|
|
platform: 'darwin',
|
|
editors: [],
|
|
});
|
|
openProjectInEditor.mockRejectedValue(new Error('daemon refused: ENOENT'));
|
|
|
|
render(
|
|
<I18nProvider initial="en">
|
|
<HandoffButton projectId="p1" />
|
|
</I18nProvider>,
|
|
);
|
|
|
|
const fallback = (await screen.findByText('Finder')).closest('button') as HTMLButtonElement;
|
|
fireEvent.click(fallback);
|
|
|
|
const errorEl = await screen.findByTestId('handoff-fallback-error');
|
|
expect(errorEl.textContent).toContain('daemon refused: ENOENT');
|
|
});
|
|
|
|
it('copies a framework-specific CLI handoff prompt with the local project path', async () => {
|
|
fetchHostEditors.mockResolvedValue({
|
|
platform: 'darwin',
|
|
editors: [
|
|
{
|
|
id: 'cursor',
|
|
label: 'Cursor',
|
|
available: true,
|
|
},
|
|
],
|
|
});
|
|
copyToClipboard.mockResolvedValue(true);
|
|
const agents: AgentInfo[] = [
|
|
{
|
|
id: 'claude',
|
|
name: 'Claude Code',
|
|
bin: 'claude',
|
|
available: true,
|
|
},
|
|
{
|
|
id: 'codex',
|
|
name: 'Codex CLI',
|
|
bin: 'codex',
|
|
available: false,
|
|
},
|
|
];
|
|
|
|
render(
|
|
<I18nProvider initial="zh-CN">
|
|
<HandoffButton
|
|
projectId="p1"
|
|
projectName="Landing"
|
|
projectDir="/tmp/open-design/Landing"
|
|
agents={agents}
|
|
/>
|
|
</I18nProvider>,
|
|
);
|
|
|
|
fireEvent.click(await screen.findByTestId('handoff-caret'));
|
|
fireEvent.click(await screen.findByRole('button', { name: 'Vue.js' }));
|
|
fireEvent.click(await screen.findByTestId('handoff-cli-item-claude'));
|
|
|
|
await waitFor(() => expect(copyToClipboard).toHaveBeenCalledTimes(1));
|
|
const prompt = copyToClipboard.mock.calls[0]?.[0] as string;
|
|
expect(prompt).toContain('/tmp/open-design/Landing');
|
|
expect(prompt).toContain('Vue.js');
|
|
expect(prompt).toContain('Claude Code');
|
|
expect(prompt).toContain('真实可运行');
|
|
});
|
|
});
|