open-design/apps/web/tests/components/ProjectView.projectInstructions.test.tsx
chaoxiaoche fce444bcab
Consolidate chat comments preview on main (#2906)
* feat(web): queue chat sends

* feat(web): render code comment directives

* feat(web): add preview comments and manual edits

* fix(web): polish shared chrome controls

* fix(web): align queued send loading state

* feat(web): open primary project artifacts

* fix(web): keep queued sends and tests aligned

* fix(web): restore docked comment tools layout

* fix(web): align preview comment toolbar

* fix(web): place local cli beside handoff

* fix(web): move agent menu beside handoff

* fix(web): make project instructions a direct header action

* fix(web): compact handoff and toolbar labels

* fix(web): clarify handoff menu and annotation label

* fix(web): restore compact cursor handoff trigger

* fix(web): align agent menu trigger with handoff

* fix(web): add draw toolbar close action

* fix(web): move inspect editing into edit mode

* fix(web): avoid reserving comment sidebar in annotation mode

* fix(web): float preview comments panel

* fix(web): keep edit canvas full width

* fix(web): polish preview annotation tools

* fix(web): highlight active preview comments

* fix(web): open comments panel after annotation save

* fix(web): polish comment handoff controls

* fix(web): remove palette preview tool

* fix(web): simplify draw annotation toolbar

* fix(web): restore queued tasks into composer

* fix(web): restore queued send strip styling

* fix(web): hide internal comment target ids

* fix(web): align manual edit panel header

* test(web): cover visual interaction contracts

* fix(web): address PR feedback regressions

* fix(web): preserve artifact chrome state

* fix(daemon): restore project raw file routes

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-26 10:31:19 +00:00

269 lines
8.3 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,
fileActionsBefore,
actions,
}: {
children: ReactNode;
fileActionsBefore?: ReactNode;
actions?: ReactNode;
}) => (
<header>
{children}
{fileActionsBefore}
{actions}
</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.';
async function openProjectInstructionsFromSettings() {
fireEvent.click(await screen.findByTestId('project-settings-trigger'));
}
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} />);
expect(await screen.findByTestId('project-settings-trigger')).toBeTruthy();
expect(screen.queryByTestId('project-instructions-chip')).toBeNull();
await openProjectInstructionsFromSettings();
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} />);
await openProjectInstructionsFromSettings();
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();
});
});