maestro/ui/src/components/chat/ChatPane.drafts.test.tsx
oss-sync ed3dc5529a
All checks were successful
CI / build-and-test (push) Successful in 9m8s
CI / build-and-test (pull_request) Successful in 9m42s
sync: update from private repo (9a86f49b)
2026-07-12 23:51:48 +00:00

127 lines
4.9 KiB
TypeScript

// @vitest-environment jsdom
import '../../test/dom-setup';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../test/render-helpers';
import { ChatPane } from './ChatPane';
import { __resetDraftPruneForTest } from '../../hooks/useDraft';
import type { LocalTask, LocalTaskComment } from '../../api';
const DRAFT_KEY = 'maestro:draft:v1:chat:42';
// useJobStream が EventSource を張るため jsdom にスタブを入れる
class FakeEventSource {
onmessage: ((e: MessageEvent) => void) | null = null;
onerror: (() => void) | null = null;
addEventListener() {}
removeEventListener() {}
close() {}
}
const task = {
id: 42,
title: 'テストタスク',
pieceName: 'chat',
latestJob: undefined,
} as unknown as LocalTask;
beforeEach(() => {
localStorage.clear();
__resetDraftPruneForTest();
vi.stubGlobal('EventSource', FakeEventSource);
vi.stubGlobal('fetch', vi.fn(async () => new Response('{}', { status: 404 })));
// ChatPetOverlay (rendered unconditionally by ChatPane) reads matchMedia for
// prefers-reduced-motion; jsdom doesn't implement it. Minimal stub only —
// not part of the brief, needed to get past mount.
vi.stubGlobal('matchMedia', (query: string) => ({
matches: false,
media: query,
addEventListener: () => {},
removeEventListener: () => {},
}));
});
describe('ChatPane drafts', () => {
it('入力途中の内容がアンマウント後も復元される', async () => {
const user = userEvent.setup();
const first = renderWithProviders(
<ChatPane task={task} comments={[]} onSubmit={vi.fn(async () => {})} />,
);
await user.type(screen.getByRole('textbox'), '書きかけの返信');
first.unmount();
expect(JSON.parse(localStorage.getItem(DRAFT_KEY)!).text).toBe('書きかけの返信');
renderWithProviders(
<ChatPane task={task} comments={[]} onSubmit={vi.fn(async () => {})} />,
);
expect(screen.getByRole('textbox')).toHaveValue('書きかけの返信');
});
it('送信成功で下書きが消える', async () => {
localStorage.setItem(DRAFT_KEY, JSON.stringify({ text: '送る内容', updatedAt: Date.now() }));
const user = userEvent.setup();
const onSubmit = vi.fn(async () => {});
renderWithProviders(<ChatPane task={task} comments={[]} onSubmit={onSubmit} />);
const textbox = screen.getByRole('textbox');
expect(textbox).toHaveValue('送る内容');
// Ctrl+Enter は textarea にフォーカスがないと発火しないため、まずフォーカスする。
await user.click(textbox);
await user.keyboard('{Control>}{Enter}{/Control}');
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
expect(localStorage.getItem(DRAFT_KEY)).toBeNull();
});
it('queued のままでもコメントが反映されたら送信ロックを外す', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn(async () => {});
const queuedTask = {
...task,
latestJob: { status: 'queued' },
} as LocalTask;
const rendered = renderWithProviders(<ChatPane task={queuedTask} comments={[]} onSubmit={onSubmit} />);
const textbox = screen.getByRole('textbox');
await user.click(textbox);
await user.type(textbox, '追記');
const queuedButton = screen.getByRole('button', { name: /Add to task|追加/ });
await user.click(queuedButton);
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith('追記', undefined));
expect(queuedButton).toBeDisabled();
rendered.rerender(
<ChatPane
task={queuedTask}
comments={[
{
id: 1,
taskId: 42,
author: 'agent',
kind: 'comment',
body: '追記',
createdAt: new Date().toISOString(),
injectedAt: null,
} as LocalTaskComment,
]}
onSubmit={onSubmit}
/>,
);
const textboxAfter = screen.getByRole('textbox');
await user.click(textboxAfter);
await user.type(textboxAfter, '続き');
await waitFor(() => expect(screen.getByRole('button', { name: /Add to task|追加/ })).toBeEnabled());
});
it('入力中のプロンプトコーチを常設ボタンから開閉できる', async () => {
const user = userEvent.setup();
renderWithProviders(<ChatPane task={task} comments={[]} onSubmit={vi.fn(async () => {})} />);
await user.type(screen.getByRole('textbox'), '評価してほしい依頼');
const coachButton = screen.getByRole('button', { name: /Evaluate prompt|プロンプトを評価/i });
await user.click(coachButton);
expect(screen.getByTestId('chat-prompt-coach')).toBeInTheDocument();
await user.click(coachButton);
expect(screen.queryByTestId('chat-prompt-coach')).not.toBeInTheDocument();
});
});