open-design/apps/web/tests/components/ProjectView.projectInstructions.test.tsx
Eli-tangerine 8193981511
Keep PR 2400 changes without folder pickers (#2462)
* feat(daemon): add project working directory management and editor hand-off functionality

- Introduced new flags for project commands to manage working directories, including `--working-dir` and `--dir`.
- Implemented API routes for listing available editors and opening projects in selected editors.
- Added a hand-off button in the ChatPane header to facilitate opening project folders in local applications.
- Enhanced the HomeHero component to include working directory and design system settings, improving user experience in project creation.
- Created HomeHeroSettingsChips component for inline management of working directory and design system selection.

* feat(chat): implement voice transcription proxy and enhance UI components

- Added a new API route for voice transcription using OpenAI's `/audio/transcriptions` endpoint, allowing users to send audio blobs directly for transcription.
- Integrated multer for handling audio file uploads in memory, ensuring efficient processing without disk storage.
- Updated the HomeHero component to include example prompt suggestions for plugins, enhancing user interaction.
- Introduced the EditorIcon component to visually represent different editors in the hand-off menu, improving the user experience.
- Refined the HandoffButton component to utilize the new EditorIcon, providing a more cohesive interface for selecting editors.
- Enhanced CSS styles for various components to improve layout and responsiveness, including adjustments to tab and button sizes for better usability.

* style(workspace-shell): enhance layout and overflow handling

- Updated CSS for .workspace-shell to ensure full viewport width and height, with proper overflow management.
- Adjusted grid layout to prevent content overflow and maintain responsiveness.
- Modified styles for .workspace-tabs-chrome to improve width handling and prevent overflow issues.

* refactor(chat): remove voice transcription proxy and related components

- Deleted the voice transcription proxy implementation, including the associated API route and multer configuration.
- Removed the MicButton component from the ChatComposer and HomeHero components to streamline the UI.
- Updated HomeHero to include example suggestions without the voice input functionality.
- Adjusted CSS styles for various components to maintain layout consistency after the removal of the MicButton.

* feat(daemon): implement minting of HMAC tokens for working directory management

- Added a new function `mintImportTokenFromCurrentSecret` to generate HMAC tokens bound to a specified base directory, enhancing security for working directory operations.
- Updated the `desktop-auth.ts` file to include the new token minting functionality, which returns structured errors when the desktop auth secret is cleared.
- Introduced new IPC message types for minting import tokens in the sidecar protocol, allowing seamless integration with the daemon's working directory management.
- Enhanced the `WorkingDirPill` component to utilize the new token minting flow for secure directory selection in desktop builds.
- Updated CSS styles for the HomeHero component to accommodate new example suggestion features and maintain layout consistency.

* fix(HomeView): import HOME_HERO_CHIPS constant for improved chip management

- Updated the HomeView component to import the HOME_HERO_CHIPS constant from the chips module, enhancing the management of hero chips within the component.

* feat(daemon): implement mintImportTokenViaSidecar for secure working directory management

- Introduced the `mintImportTokenViaSidecar` function to facilitate the minting of HMAC tokens for desktop-import operations via the daemon's sidecar IPC. This allows CLI commands to bypass authentication when the desktop-auth gate is active.
- Updated the CLI to utilize the new token minting function when setting the working directory, ensuring secure access to trust-gated API endpoints.
- Enhanced the sidecar server to handle minting requests and return structured error messages for improved user feedback.
- Added tests to validate the new token minting functionality and its integration with the working directory management process.
- Refactored related components to support the new token flow, improving overall security and user experience.

* feat(HomeHero): enhance UI components and styles for improved user experience

- Updated HomeHero component to replace active dot indicators with Plug icons for better visual representation of active plugins.
- Adjusted CSS styles for various elements, including padding and dimensions, to enhance layout consistency and responsiveness.
- Introduced new styles for active type icons and improved hover effects for buttons.
- Updated HomeHeroSettingsChips to change button titles and icons for clarity.
- Added tests to ensure proper rendering and functionality of updated components.

* feat(ProjectDesignSystemPicker): enhance design system selection with preview functionality

- Updated the ProjectDesignSystemPicker component to include a preview feature for design systems, allowing users to see a preview of the selected design system.
- Implemented hover functionality to update the preview based on the hovered design system.
- Added fullscreen preview capability for a more immersive experience.
- Enhanced CSS styles for the design system picker to improve layout and responsiveness.
- Introduced tests to validate the new preview functionality and ensure proper interaction within the component.

* feat: refactor project metadata handling and enhance design system picker

- Updated the default scenario plugin ID retrieval to use project metadata, improving the logic for determining the appropriate plugin based on project intent.
- Enhanced the ProjectDesignSystemPicker and related components to support localized design system summaries and categories, improving user experience.
- Introduced new translations for working directory and design system picker components, ensuring better accessibility and usability across different locales.
- Added a new 'live-artifact' project type to the HomeHero chips, expanding the functionality for users creating refreshable artifacts.
- Updated tests to validate the new project metadata handling and design system picker functionalities.

* feat: enhance localization and styling for design system components

- Added French translations for working directory and design system picker components, improving accessibility for French-speaking users.
- Updated CSS styles for the pet task item to ensure consistent padding and layout.
- Introduced a new test suite for HomeHeroSettingsChips to validate localization and design system selection functionality.
- Enhanced ProjectDesignSystemPicker tests to ensure proper localization and interaction with design system categories.

* fix: update .gitignore to include all claude-sessions directories and remove specific session files

- Modified .gitignore to ensure all claude-sessions directories are ignored by using a wildcard pattern.
- Deleted two specific claude-sessions markdown files to clean up unnecessary session data.

* fix: repair home automation ci regressions

* fix: stabilize artifact consistency e2e

* Remove folder picker changes from PR 2400

---------

Co-authored-by: pftom <1043269994@qq.com>
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-20 22:07:30 +08:00

251 lines
8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useState } from 'react';
import type { ReactNode } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ProjectView } from '../../src/components/ProjectView';
import type {
AgentInfo,
AppConfig,
Conversation,
DesignSystemSummary,
Project,
SkillSummary,
} from '../../src/types';
import {
createConversation,
listConversations,
listMessages,
loadTabs,
patchProject,
} from '../../src/state/projects';
import { fetchPreviewComments } from '../../src/providers/registry';
vi.mock('../../src/i18n', () => ({
useI18n: () => ({
locale: 'en',
setLocale: () => undefined,
t: (key: string) => key,
}),
useT: () => (key: string) => key,
}));
vi.mock('../../src/router', () => ({
navigate: vi.fn(),
}));
vi.mock('../../src/providers/anthropic', () => ({
streamMessage: vi.fn(),
}));
vi.mock('../../src/providers/daemon', () => ({
fetchChatRunStatus: vi.fn(),
listActiveChatRuns: vi.fn().mockResolvedValue([]),
listProjectRuns: vi.fn().mockResolvedValue([]),
reattachDaemonRun: vi.fn(),
streamViaDaemon: vi.fn(),
}));
vi.mock('../../src/providers/project-events', () => ({
useProjectFileEvents: vi.fn(),
}));
vi.mock('../../src/providers/registry', async () => {
const actual = await vi.importActual<typeof import('../../src/providers/registry')>(
'../../src/providers/registry',
);
return {
...actual,
deletePreviewComment: vi.fn(),
fetchDesignSystem: vi.fn(),
fetchLiveArtifacts: vi.fn().mockResolvedValue([]),
fetchPreviewComments: vi.fn(),
fetchProjectFiles: vi.fn().mockResolvedValue([]),
fetchSkill: vi.fn(),
getTemplate: vi.fn(),
patchPreviewCommentStatus: vi.fn(),
upsertPreviewComment: vi.fn(),
writeProjectTextFile: vi.fn(),
};
});
vi.mock('../../src/state/projects', async () => {
const actual = await vi.importActual<typeof import('../../src/state/projects')>(
'../../src/state/projects',
);
return {
...actual,
createConversation: vi.fn(),
listConversations: vi.fn(),
listMessages: vi.fn(),
loadTabs: vi.fn(),
patchConversation: vi.fn(),
patchProject: vi.fn(),
saveMessage: vi.fn(),
saveTabs: vi.fn(),
};
});
vi.mock('../../src/components/AppChromeHeader', () => ({
AppChromeHeader: ({ children }: { children: ReactNode }) => <header>{children}</header>,
}));
vi.mock('../../src/components/AvatarMenu', () => ({
AvatarMenu: () => null,
}));
vi.mock('../../src/components/FileWorkspace', () => ({
FileWorkspace: () => <div data-testid="file-workspace" />,
}));
vi.mock('../../src/components/Loading', () => ({
CenteredLoader: () => <div data-testid="loader" />,
}));
vi.mock('../../src/components/ChatPane', () => ({
ChatPane: () => <div data-testid="chat-pane" />,
}));
const mockedListConversations = vi.mocked(listConversations);
const mockedCreateConversation = vi.mocked(createConversation);
const mockedListMessages = vi.mocked(listMessages);
const mockedLoadTabs = vi.mocked(loadTabs);
const mockedFetchPreviewComments = vi.mocked(fetchPreviewComments);
const mockedPatchProject = vi.mocked(patchProject);
const config: AppConfig = {
mode: 'api',
apiKey: '',
baseUrl: '',
model: '',
agentId: null,
skillId: null,
designSystemId: null,
};
const baseProject: Project = {
id: 'project-1',
name: 'Project 1',
skillId: null,
designSystemId: null,
createdAt: 1,
updatedAt: 1,
};
const conversation: Conversation = {
id: 'conv-1',
projectId: baseProject.id,
title: null,
createdAt: 1,
updatedAt: 1,
};
function ProjectViewHarness({ initialProject }: { initialProject: Project }) {
const [project, setProject] = useState(initialProject);
return (
<ProjectView
project={project}
routeFileName={null}
config={config}
agents={[] as AgentInfo[]}
skills={[] as SkillSummary[]}
designTemplates={[] as SkillSummary[]}
designSystems={[] as DesignSystemSummary[]}
daemonLive
onModeChange={vi.fn()}
onAgentChange={vi.fn()}
onAgentModelChange={vi.fn()}
onRefreshAgents={vi.fn()}
onOpenSettings={vi.fn()}
onBack={vi.fn()}
onClearPendingPrompt={vi.fn()}
onTouchProject={vi.fn()}
onProjectChange={setProject}
onProjectsRefresh={vi.fn()}
/>
);
}
const SAVED = 'Always use tabs, never spaces.';
describe('ProjectView saved Project instructions surface (#1822)', () => {
beforeEach(() => {
mockedListConversations.mockResolvedValue([conversation]);
mockedCreateConversation.mockResolvedValue(conversation);
mockedListMessages.mockResolvedValue([]);
mockedLoadTabs.mockResolvedValue({ tabs: ['index.html'], active: 'index.html' });
mockedFetchPreviewComments.mockResolvedValue([]);
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
it('shows a persistent saved-state chip (not just a bare pencil) when instructions exist', async () => {
render(<ProjectViewHarness initialProject={{ ...baseProject, customInstructions: SAVED }} />);
const chip = await screen.findByTestId('project-instructions-chip');
expect(chip).toBeTruthy();
// The empty-state add affordance must not be the surface once a value exists.
expect(screen.queryByTestId('project-instructions-add')).toBeNull();
// Nothing is expanded until the user opts in.
expect(screen.queryByTestId('project-instructions-preview')).toBeNull();
expect(screen.queryByTestId('project-instructions-textarea')).toBeNull();
});
it('opens a read-only review panel that previews the saved instructions', async () => {
render(<ProjectViewHarness initialProject={{ ...baseProject, customInstructions: SAVED }} />);
fireEvent.click(await screen.findByTestId('project-instructions-chip'));
const preview = screen.getByTestId('project-instructions-preview');
expect(preview.textContent).toBe(SAVED);
// The panel makes the active/injected state explicit.
expect(screen.getByText('project.instructionsActive')).toBeTruthy();
// Review is read-only — no editor until the user asks to edit.
expect(screen.queryByTestId('project-instructions-textarea')).toBeNull();
});
it('reopens the editor from the review panel with the saved value prefilled', async () => {
render(<ProjectViewHarness initialProject={{ ...baseProject, customInstructions: SAVED }} />);
fireEvent.click(await screen.findByTestId('project-instructions-chip'));
fireEvent.click(screen.getByTestId('project-instructions-edit'));
const textarea = screen.getByTestId('project-instructions-textarea') as HTMLTextAreaElement;
expect(textarea.value).toBe(SAVED);
});
it('offers an add affordance and opens an empty editor when no instructions are saved', async () => {
render(<ProjectViewHarness initialProject={baseProject} />);
const add = await screen.findByTestId('project-instructions-add');
expect(screen.queryByTestId('project-instructions-chip')).toBeNull();
fireEvent.click(add);
const textarea = screen.getByTestId('project-instructions-textarea') as HTMLTextAreaElement;
expect(textarea.value).toBe('');
});
it('reads the saved value back in the review panel right after a save', async () => {
mockedPatchProject.mockResolvedValue({ ...baseProject, customInstructions: SAVED });
render(<ProjectViewHarness initialProject={baseProject} />);
fireEvent.click(await screen.findByTestId('project-instructions-add'));
fireEvent.change(screen.getByTestId('project-instructions-textarea'), {
target: { value: SAVED },
});
fireEvent.click(screen.getByTestId('project-instructions-save'));
expect(mockedPatchProject).toHaveBeenCalledWith('project-1', { customInstructions: SAVED });
// Save lands on the review panel so the stored value is confirmed back.
await waitFor(() => {
expect(screen.getByTestId('project-instructions-preview').textContent).toBe(SAVED);
});
expect(screen.getByTestId('project-instructions-chip')).toBeTruthy();
});
});