mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* 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>
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
import { forwardRef, useImperativeHandle } from 'react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { ChatPane } from '../../src/components/ChatPane';
|
|
import type { Conversation, ProjectMetadata } from '../../src/types';
|
|
|
|
const composerMocks = vi.hoisted(() => ({
|
|
focus: vi.fn(),
|
|
restoreDraft: vi.fn(),
|
|
setDraft: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../src/i18n', () => ({
|
|
useI18n: () => ({ locale: 'en', setLocale: () => undefined, t: (key: string) => key }),
|
|
useT: () => (key: string) => key,
|
|
}));
|
|
|
|
vi.mock('../../src/components/ChatComposer', () => ({
|
|
ChatComposer: forwardRef((_props, ref) => {
|
|
useImperativeHandle(ref, () => ({
|
|
focus: composerMocks.focus,
|
|
restoreDraft: composerMocks.restoreDraft,
|
|
setDraft: composerMocks.setDraft,
|
|
}));
|
|
return <output data-testid="composer" />;
|
|
}),
|
|
}));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
const conversations: Conversation[] = [
|
|
{ id: 'conv-1', projectId: 'project-1', title: 'Conversation 1', createdAt: 1, updatedAt: 1 },
|
|
];
|
|
|
|
const projectMetadata: ProjectMetadata = { kind: 'prototype' };
|
|
|
|
function renderPane(extra: Partial<React.ComponentProps<typeof ChatPane>>) {
|
|
return render(
|
|
<ChatPane
|
|
projectKindForTracking="prototype"
|
|
messages={[]}
|
|
streaming={false}
|
|
error={null}
|
|
projectId="project-1"
|
|
projectFiles={[]}
|
|
onEnsureProject={async () => 'project-1'}
|
|
onSend={vi.fn()}
|
|
onStop={vi.fn()}
|
|
conversations={conversations}
|
|
activeConversationId="conv-1"
|
|
onSelectConversation={vi.fn()}
|
|
onDeleteConversation={vi.fn()}
|
|
projectMetadata={projectMetadata}
|
|
{...extra}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe('ChatPane connect-repo CTA', () => {
|
|
it('fires onConnectRepo with the Connect GitHub label when the repo evidence is incomplete', () => {
|
|
const onConnectRepo = vi.fn();
|
|
const { container } = renderPane({ connectRepoNeeded: true, githubConnected: false, onConnectRepo });
|
|
|
|
expect(container.querySelector('.chat-connect-repo')).not.toBeNull();
|
|
const connectButton = screen.getByRole('button', { name: /Connect GitHub/ });
|
|
fireEvent.click(connectButton);
|
|
|
|
expect(onConnectRepo).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('shows a disabled pending button until the connector status resolves', () => {
|
|
const onConnectRepo = vi.fn();
|
|
// githubConnected omitted -> undefined -> status still loading.
|
|
renderPane({ connectRepoNeeded: true, onConnectRepo });
|
|
|
|
const pendingButton = screen.getByRole('button', { name: /Checking GitHub/ });
|
|
expect((pendingButton as HTMLButtonElement).disabled).toBe(true);
|
|
fireEvent.click(pendingButton);
|
|
expect(onConnectRepo).not.toHaveBeenCalled();
|
|
expect(screen.queryByRole('button', { name: /Connect GitHub/ })).toBeNull();
|
|
expect(screen.queryByRole('button', { name: /Import repo/ })).toBeNull();
|
|
});
|
|
|
|
it('switches to an Import repo action when GitHub is already connected', () => {
|
|
const onConnectRepo = vi.fn();
|
|
const { container } = renderPane({ connectRepoNeeded: true, githubConnected: true, onConnectRepo });
|
|
|
|
expect(container.querySelector('.chat-connect-repo')).not.toBeNull();
|
|
expect(screen.getByText('GitHub is connected')).toBeTruthy();
|
|
const importButton = screen.getByRole('button', { name: /Import repo/ });
|
|
fireEvent.click(importButton);
|
|
|
|
expect(onConnectRepo).toHaveBeenCalledTimes(1);
|
|
expect(screen.queryByRole('button', { name: /Connect GitHub/ })).toBeNull();
|
|
});
|
|
|
|
it('prefills the composer when the parent pushes a draft signal', () => {
|
|
renderPane({
|
|
connectRepoNeeded: true,
|
|
githubConnected: true,
|
|
onConnectRepo: vi.fn(),
|
|
composerDraftSignal: { text: 'Pull the linked repo', nonce: 1 },
|
|
});
|
|
|
|
expect(composerMocks.setDraft).toHaveBeenCalledWith('Pull the linked repo');
|
|
});
|
|
|
|
it('hides the CTA when the project does not need a repo connection', () => {
|
|
const { container } = renderPane({ connectRepoNeeded: false, onOpenSettings: vi.fn() });
|
|
expect(container.querySelector('.chat-connect-repo')).toBeNull();
|
|
});
|
|
|
|
it('hides the CTA once the conversation has messages', () => {
|
|
const { container } = renderPane({
|
|
connectRepoNeeded: true,
|
|
onOpenSettings: vi.fn(),
|
|
messages: [{ id: 'user-1', role: 'user', content: 'hi', createdAt: 1 }],
|
|
});
|
|
expect(container.querySelector('.chat-connect-repo')).toBeNull();
|
|
});
|
|
});
|