open-design/apps/web/tests/components/ChatPane.connect-repo.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

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();
});
});