127 lines
4.9 KiB
TypeScript
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();
|
|
});
|
|
});
|