// @vitest-environment jsdom import { cleanup, fireEvent, render, screen, waitFor, } from '@testing-library/react'; import { useState } from 'react'; import { afterEach, describe, expect, test, vi } from 'vitest'; import { ResourcePromptPolishSlot } from '../src/features/resource-canvas/ResourcePromptPolishSlot'; type TauriInvoke = ( command: string, args?: Record, ) => Promise; /** * 平台侧润色的桩:`ResourcePromptPolishSlot` 走 `usePromptPolish` 的默认实现 * (`polish_local_project_prompt`),所以这里和生成素材面板的既有用例一样, * 用 `window.__TAURI__` 打桩,不碰真实 Provider。 */ function installPolishInvoke(result: string) { const invoke = vi.fn(async (command: string) => { if (command !== 'polish_local_project_prompt') { throw new Error(`unexpected invoke ${command}`); } return result; }); ( window as unknown as { __TAURI__?: { core?: { invoke: TauriInvoke } } } ).__TAURI__ = { core: { invoke } }; return invoke; } afterEach(() => { cleanup(); delete (window as unknown as { __TAURI__?: unknown }).__TAURI__; }); /** * 宿主侧的最小接线:slot 本身是受控的,提示词文本存在宿主里 * (与 AGC 快速编辑面板 / 生成素材弹窗同一形状),`output` 只是把宿主状态显示出来。 */ function PolishSlotHarness({ prompt }: { prompt: string }) { const [text, setText] = useState(prompt); return ( <> {/* 刻意用 span:`` 自带 status 角色,会和 slot 的状态行撞名。 */} {text} {/* 宿主输入框里的手改:与 AGC 面板里改提示词同一条路径(改的是宿主状态)。 */} ); } describe('ResourcePromptPolishSlot', () => { test('回包与原文相同:状态行说清「未做修改」,也不给「恢复原文」假入口', async () => { const invoke = installPolishInvoke('把角色头发改成红色'); render(); fireEvent.click(screen.getByRole('button', { name: 'AI 润色' })); // 验收项 AGC-004 的用户可见面:必须有一条说得清的反馈,而不是静默什么都不发生。 await waitFor(() => expect(screen.getByRole('status').textContent).toBe( 'AI 润色结果与原文相同,未做修改', ), ); expect(invoke).toHaveBeenCalledWith('polish_local_project_prompt', { prompt: '把角色头发改成红色', context: expect.stringContaining('图片素材的快速编辑提示词'), }); // 文本没变,也就没有可回退的变化:不渲染「恢复原文」。 expect(screen.getByLabelText('宿主提示词').textContent).toBe( '把角色头发改成红色', ); expect(screen.queryByRole('button', { name: '恢复原文' })).toBeNull(); }); test('用户接着手改提示词:上一轮的提示收起,宿主文本就是用户写的那份', async () => { installPolishInvoke('把角色头发改成红色'); render(); fireEvent.click(screen.getByRole('button', { name: 'AI 润色' })); await waitFor(() => expect(screen.getByRole('status').textContent).toBe( 'AI 润色结果与原文相同,未做修改', ), ); // 宿主文本被用户改写:上一轮「与原文相同」的结论不再描述当前这段话,提示收起, // 免得用户把上一轮的结论当成对现在这段文本的判断。 fireEvent.click(screen.getByRole('button', { name: '模拟用户手改' })); await waitFor(() => expect(screen.queryByRole('status')).toBeNull()); expect(screen.getByLabelText('宿主提示词').textContent).toBe( '把角色头发改成蓝色', ); }); test('回包不同:「恢复原文」把原输入回填给宿主,并收起自己与提示', async () => { installPolishInvoke('把角色头发设定改为亮红色'); render(); fireEvent.click(screen.getByRole('button', { name: 'AI 润色' })); const restore = await screen.findByRole('button', { name: '恢复原文' }); expect(screen.getByLabelText('宿主提示词').textContent).toBe( '把角色头发设定改为亮红色', ); fireEvent.click(restore); // 验收项 AGC-005 的动作面:恢复原文 = 回填最初原文,快照被消费掉,按钮与提示一起收干净。 expect(screen.getByLabelText('宿主提示词').textContent).toBe( '把角色头发改成红色', ); expect(screen.queryByRole('button', { name: '恢复原文' })).toBeNull(); expect(screen.queryByRole('status')).toBeNull(); }); });