feat: add creative welcome and external works square links
This commit is contained in:
90
tests/e2e/community-welcome.spec.ts
Normal file
90
tests/e2e/community-welcome.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
let server: ViteDevServer;
|
||||
let baseUrl: string;
|
||||
test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
||||
test.beforeAll(async () => {
|
||||
server = await createServer({
|
||||
configFile: false, cacheDir: 'node_modules/.vite-community-layout',
|
||||
resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' },
|
||||
server: { host: '127.0.0.1', port: 0 },
|
||||
plugins: [{
|
||||
name: 'isolated-community', enforce: 'pre',
|
||||
load(id) {
|
||||
if (id.endsWith('/src/lib/host-events.ts')) return 'export function subscribeHostEvent() { return () => {}; }';
|
||||
if (id.endsWith('/src/lib/host-api.ts')) return `
|
||||
export async function createHostEventSource() { return { close() {}, addEventListener() {} }; }
|
||||
export async function ensureHostApiToken() { return 'fixture'; }
|
||||
export function getHostApiBase() { return ''; }
|
||||
export function reportDesktopActivity() {}
|
||||
export function setDesktopBackgroundLease() {}
|
||||
export async function hostApiFetch() { return {}; }
|
||||
`;
|
||||
},
|
||||
configureServer(vite) {
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
if (req.url !== '/' && !req.url?.startsWith('/?')) return next();
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<html><head><meta charset="utf-8"></head><body><div id="root"></div><script type="module" src="/tests/e2e/fixtures/community-welcome.tsx"></script></body></html>');
|
||||
});
|
||||
},
|
||||
}],
|
||||
});
|
||||
await server.listen();
|
||||
baseUrl = server.resolvedUrls!.local[0];
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('pageerror', error => { throw error; });
|
||||
await page.addInitScript(() => {
|
||||
const scope = window as typeof window & { __communityCalls: unknown[][] };
|
||||
scope.__communityCalls = [];
|
||||
Object.defineProperty(window, 'electron', { value: { ipcRenderer: {
|
||||
invoke: async (...args: unknown[]) => { scope.__communityCalls.push(args); return undefined; },
|
||||
} } });
|
||||
});
|
||||
});
|
||||
|
||||
for (const width of [375, 768, 1280, 1470]) {
|
||||
test(`welcome, composer and community link clear teacher introductions at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 850 });
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(baseUrl + '?teachers');
|
||||
const heading = page.getByTestId('coding-start-heading');
|
||||
const teachers = page.getByTestId('teacher-welcomes');
|
||||
await expect(teachers.locator('.teacher-welcome-card')).toHaveCount(2);
|
||||
await expect.poll(async () => {
|
||||
const a = await heading.boundingBox(), b = await teachers.boundingBox();
|
||||
return !!a && !!b && (a.x + a.width <= b.x || a.y >= b.y + b.height);
|
||||
}).toBe(true);
|
||||
await expect(page.locator('.coding-start-mark')).toHaveCSS('animation-name', 'none');
|
||||
await page.getByTestId('community-invitation').scrollIntoViewIfNeeded();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
await expect(page.getByRole('button', { name: '去作品广场逛逛' })).toBeVisible();
|
||||
await teachers.getByRole('button', { name: '先自己试试' }).click();
|
||||
await heading.scrollIntoViewIfNeeded();
|
||||
await page.screenshot({ path: test.info().outputPath(`community-welcome-${width}.png`) });
|
||||
});
|
||||
}
|
||||
|
||||
test('the community link opens only on click and leaves the draft mounted', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 850 });
|
||||
const communityRequests: string[] = [];
|
||||
page.on('request', request => { if (request.url().includes('square.nianxx.cn')) communityRequests.push(request.url()); });
|
||||
await page.goto(baseUrl);
|
||||
const composer = page.getByRole('textbox');
|
||||
await composer.fill('我想做一个自己的小镇');
|
||||
await composer.evaluate(node => node.setAttribute('data-mounted', 'same'));
|
||||
await page.getByRole('button', { name: '去作品广场逛逛' }).click();
|
||||
expect(await page.evaluate(() => (window as typeof window & { __communityCalls: unknown[][] }).__communityCalls))
|
||||
.toContainEqual(['shell:openExternal', 'https://square.nianxx.cn/#explore']);
|
||||
expect(communityRequests).toEqual([]);
|
||||
await expect(page.locator('iframe')).toHaveCount(0);
|
||||
await expect(composer).toHaveValue('我想做一个自己的小镇');
|
||||
await expect(composer).toHaveAttribute('data-mounted', 'same');
|
||||
await page.getByRole('button', { name: '发送', exact: true }).click();
|
||||
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
|
||||
await expect(composer).toHaveAttribute('data-mounted', 'same');
|
||||
});
|
||||
51
tests/e2e/fixtures/community-welcome.tsx
Normal file
51
tests/e2e/fixtures/community-welcome.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState, type ComponentProps } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { CodingChatBody } from '../../../src/pages/Chat/CodingChatBody';
|
||||
import { CodingStartHeading } from '../../../src/pages/Chat/CodingStartHeading';
|
||||
import { CodingComposer } from '../../../src/pages/Chat/CodingComposer';
|
||||
import { TeacherCompanion } from '../../../src/pages/Chat/TeacherCompanion';
|
||||
import { CommunityInvitation } from '../../../src/components/community/CommunityInvitation';
|
||||
import '../../../src/styles/globals.css';
|
||||
import '../../../src/pages/Chat/classroom-workspace.css';
|
||||
|
||||
export function Layout() {
|
||||
const [bubble, setBubble] = useState<HTMLDivElement | null>(null);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [sent, setSent] = useState(false);
|
||||
const [dismissed, setDismissed] = useState<string[]>([]);
|
||||
const agents = ['架构工程师-陈宗琦', '产品工程师-Alice'].map((name, index) => ({
|
||||
teacher_id: String(index), version: 1, is_default: index === 0,
|
||||
definition: { config_id: String(index), name, avatar_id: 'avatar-06',
|
||||
description: index === 0 ? '有什么编程问题想一起讨论?' : '陪你把天马行空的点子,变成能玩、能用、眼睛一亮的小作品。' },
|
||||
}));
|
||||
const companion = {
|
||||
agents, selectedAgent: agents[0], definition: agents[0].definition, enabled: true,
|
||||
unreadAgentIds: [], invitation: null, welcome: null, loading: false, error: null, checking: false,
|
||||
refreshing: false, refreshStatus: 'idle', refresh: async () => {}, reloadCatalog: async () => {}, dismiss: () => {},
|
||||
welcomes: new URLSearchParams(location.search).has('teachers') ? agents.flatMap((agent, index) => dismissed.includes(agent.teacher_id)
|
||||
? [] : [{ agentId: agent.teacher_id, definition: agent.definition, text: agent.definition.description, tone: index }]) : [],
|
||||
dismissWelcome: (id: string) => setDismissed(values => [...values, id]),
|
||||
dismissWelcomes: () => setDismissed(['0', '1']),
|
||||
} as ComponentProps<typeof TeacherCompanion>['companion'];
|
||||
return <div className="classroom-workspace flex h-screen flex-col bg-white">
|
||||
<header className="relative z-50 flex h-12 shrink-0 items-center justify-end border-b px-3" data-testid="coding-conversation-header">
|
||||
<span className="mr-auto min-w-0 truncate text-xs">测试项目 › 新对话</span>
|
||||
<TeacherCompanion companion={companion} open={false} onOpen={() => {}} onToggle={() => {}} bubbleRef={setBubble} />
|
||||
</header>
|
||||
<main className="classroom-main flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div className="classroom-tabs" role="tablist"><button role="tab" aria-selected="true">操作对话</button><button role="tab">作品</button></div>
|
||||
<CodingChatBody welcome={!sent} hidden={false} bubble={bubble}>
|
||||
<div className="classroom-chat-view min-h-0 flex-1">{!sent ? <CodingStartHeading /> : <p>第一条消息已发送(测试)</p>}</div>
|
||||
<div className="classroom-composer-view shrink-0">
|
||||
<CodingComposer value={draft} editable canSend={Boolean(draft)} preparing={false} recovering={false}
|
||||
runStatus="idle" mode="prompt" queue={{ items: [] }} error={null} recoverableError={false}
|
||||
attachments={[]} submitting={false} placeholder="说说你的想法,接下来想做什么?" onChange={setDraft}
|
||||
onModeChange={() => {}} onSubmit={() => { setSent(true); setDraft(''); }} onRecover={() => {}}
|
||||
onAddFiles={() => {}} onRemoveAttachment={() => {}} />
|
||||
</div>
|
||||
{!sent && <CommunityInvitation />}
|
||||
</CodingChatBody>
|
||||
</main>
|
||||
</div>;
|
||||
}
|
||||
createRoot(document.getElementById('root')!).render(<Layout />);
|
||||
@@ -1121,10 +1121,25 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
|
||||
buffer: largePng,
|
||||
});
|
||||
await composer.fill('Build the first PI scene');
|
||||
await electronApp.evaluate(({ shell }) => {
|
||||
const scope = globalThis as typeof globalThis & { __communityOpened?: string[] };
|
||||
scope.__communityOpened = [];
|
||||
shell.openExternal = async url => { scope.__communityOpened!.push(url); };
|
||||
});
|
||||
await page.getByRole('button', { name: '作品广场', exact: true }).click();
|
||||
await page.getByRole('button', { name: '去作品广场逛逛' }).click();
|
||||
await expect.poll(() => electronApp.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __communityOpened?: string[] }).__communityOpened
|
||||
))).toEqual(['https://square.nianxx.cn/#explore', 'https://square.nianxx.cn/#explore']);
|
||||
await expect(composer).toHaveValue('Build the first PI scene');
|
||||
await expect(page.getByText('large.png', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
expect((await readState(electronApp)).captured.filter(item => item.path.endsWith('/prompt') && item.method === 'POST')).toHaveLength(0);
|
||||
await expect(page.getByRole('button', { name: '发送' })).toBeEnabled();
|
||||
await page.getByTestId('coding-message-composer').evaluate(
|
||||
(form: HTMLFormElement) => form.requestSubmit(),
|
||||
);
|
||||
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
|
||||
|
||||
await expect.poll(async () => {
|
||||
const state = await readState(electronApp);
|
||||
@@ -1869,7 +1884,7 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await expect(page.getByRole('button', { name: '与朋友聊天' })).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(page.locator('#coding-consultation-dock')).toHaveCSS('width', '508px');
|
||||
await expect(teacher.locator('form .consultation-hint')).toBeVisible();
|
||||
await expect(teacher.locator('form .consultation-hint')).toHaveText('不知道问什么,你先帮我看看👀');
|
||||
await expect(teacher.locator('form .consultation-hint')).toHaveText('帮我整体看🧠');
|
||||
await expect(teacher.getByText('把你的困惑说出来就好')).toHaveCount(0);
|
||||
await expect(teacher.getByText('当前项目',{exact:true})).toHaveCount(0);
|
||||
await expect(teacher.getByRole('button',{name:'解释当前代码',exact:true})).toBeVisible();
|
||||
@@ -1924,7 +1939,7 @@ test('project consultations preserve student drafts and switch between work and
|
||||
expect(request.body).not.toHaveProperty('discussion');
|
||||
}
|
||||
await expect(teacher).toHaveAccessibleName('朋友咨询');
|
||||
expect(consultationRequests[0].body).toMatchObject({intent:'suggestions',text:'不知道问什么,你先帮我看看👀'});
|
||||
expect(consultationRequests[0].body).toMatchObject({intent:'suggestions',text:'帮我整体看🧠'});
|
||||
expect(consultationRequests[1].body).toMatchObject({text:'怎样观察别人玩游戏?'});
|
||||
expect(consultationRequests[1].body?.intent).toBeUndefined();
|
||||
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
|
||||
|
||||
@@ -29,7 +29,7 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
|
||||
const panel = page.getByTestId('teacher-chat-panel');
|
||||
const retry = panel.locator('.consultation-retry');
|
||||
await expect(retry).toBeEnabled();
|
||||
await expect(retry).toHaveText('不知道问什么,你先帮我看看👀');
|
||||
await expect(retry).toHaveText('帮我整体看🧠');
|
||||
expect(await retry.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
|
||||
const [panelBounds, retryBounds] = await Promise.all([panel.boundingBox(), retry.boundingBox()]);
|
||||
expect(retryBounds!.x).toBeGreaterThanOrEqual(panelBounds!.x);
|
||||
@@ -41,7 +41,7 @@ test('retry stickers stay readable and preserve drafts in a 319px consultation',
|
||||
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
|
||||
await expect(input).toHaveValue('我还有一点自己的想法');
|
||||
expect(await page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs)).toEqual([
|
||||
{ requestId: expect.any(String), text: '不知道问什么,你先帮我看看👀', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' },
|
||||
{ requestId: expect.any(String), text: '帮我整体看🧠', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' },
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -89,7 +89,7 @@ for (const width of [319, 508]) {
|
||||
const send = page.getByRole('button', { name: '提问', exact: true });
|
||||
const sent = () => page.evaluate(() => (window as unknown as { __teacherReplyInputs: unknown[] }).__teacherReplyInputs);
|
||||
await expect(hint).toBeEnabled();
|
||||
await expect(hint).toHaveText('不知道问什么,你先帮我看看👀');
|
||||
await expect(hint).toHaveText('帮我整体看🧠');
|
||||
expect(await hint.evaluate(el => el.scrollWidth <= el.clientWidth && el.scrollHeight <= el.clientHeight)).toBe(true);
|
||||
await expect(hint).toHaveCSS('background-color', 'rgb(255, 242, 201)');
|
||||
const initialSend = (await send.boundingBox())!;
|
||||
@@ -110,9 +110,9 @@ for (const width of [319, 508]) {
|
||||
expect(await sent()).toEqual([]);
|
||||
await hint.click();
|
||||
await expect(page.getByText('我们可以先从一个小地方开始。')).toBeVisible();
|
||||
expect(await sent()).toEqual([{ requestId: expect.any(String), text: '不知道问什么,你先帮我看看👀', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' }]);
|
||||
expect(await sent()).toEqual([{ requestId: expect.any(String), text: '帮我整体看🧠', intent: 'suggestions', references: [], projectId: 'fixture', sourceConversationId: 'source' }]);
|
||||
await expect(page.getByRole('button', { name: '我也说不清,你带我看看', exact: true })).toHaveCount(0);
|
||||
await expect(page.locator('.consultation-messages').getByRole('button', { name: '不知道问什么,你先帮我看看👀', exact: true })).toHaveCount(0);
|
||||
await expect(page.locator('.consultation-messages').getByRole('button', { name: '帮我整体看🧠', exact: true })).toHaveCount(0);
|
||||
const card = page.getByRole('button', { name: '可以从哪个小地方开始?' });
|
||||
await expect(card).toBeVisible();
|
||||
await input.fill('留着我自己的草稿');
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
await waitFor(() => expect(codingConversationStore.getState().selectedConversationId).toBe(conversation.id));
|
||||
fireEvent.keyDown(menu, { key: 'Enter' });
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: '归档(任务继续运行)' }));
|
||||
await screen.findByTestId('coding-conversation-pending');
|
||||
await screen.findByRole('heading', { name: '今天,你想创造什么?' });
|
||||
expect(projectApi.create).not.toHaveBeenCalled();
|
||||
expect(conversationApi.abort).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看归档对话' }));
|
||||
@@ -434,13 +434,15 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
expect(onOpenProjectSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps an empty project editable without creating a conversation until first send', async () => {
|
||||
it('keeps an empty project and its draft mounted across community visits until first send', async () => {
|
||||
projectApi.list.mockResolvedValue({ projects: [project], activeProjectId: project.id });
|
||||
projectApi.config.mockResolvedValue({ project, config });
|
||||
projectApi.conversations.mockResolvedValue([]);
|
||||
projectApi.create.mockResolvedValue(conversation);
|
||||
conversationApi.events.mockResolvedValue(new FakeEventSource() as unknown as EventSource);
|
||||
const { CodingChatPanel } = await import('@/pages/Chat/CodingChatPanel');
|
||||
const api = await import('@/lib/api-client');
|
||||
const external = vi.spyOn(api, 'invokeIpc').mockResolvedValue(undefined);
|
||||
const { createLocalConversationSnapshot } = await import('@/pages/Chat/coding-chat-snapshot');
|
||||
conversationApi.snapshot.mockResolvedValue(createLocalConversationSnapshot(project.id, conversation));
|
||||
render(<CodingChatPanel />);
|
||||
@@ -448,8 +450,17 @@ describe('CodingChatPanel first Conversation', () => {
|
||||
expect(projectApi.create).not.toHaveBeenCalled();
|
||||
expect(conversationApi.snapshot).not.toHaveBeenCalled();
|
||||
fireEvent.change(textbox, {target:{value:'First prompt'}});
|
||||
fireEvent.click(screen.getByRole('button', { name: '去作品广场逛逛' }));
|
||||
await waitFor(() => expect(external).toHaveBeenCalledWith('shell:openExternal', 'https://square.nianxx.cn/#explore'));
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toBe(textbox);
|
||||
expect(textbox).toHaveValue('First prompt');
|
||||
expect(projectApi.create).not.toHaveBeenCalled();
|
||||
expect(conversationApi.submit).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button',{name:'发送'}));
|
||||
await waitFor(()=>expect(projectApi.create).toHaveBeenCalledOnce());
|
||||
await waitFor(() => expect(screen.queryByTestId('community-invitation')).not.toBeInTheDocument());
|
||||
expect(screen.getByRole('textbox')).toBe(textbox);
|
||||
});
|
||||
|
||||
it('rehydrates a preselected stale Conversation when the programming view mounts', async () => {
|
||||
|
||||
@@ -496,13 +496,13 @@ describe('teacher side chat', () => {
|
||||
}));
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
const help = () => screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' });
|
||||
const help = () => screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' });
|
||||
await waitFor(() => expect(help()).toBeEnabled());
|
||||
expect(screen.getByTestId('consultation-composer-actions')).toContainElement(help());
|
||||
expect(help()).toHaveAccessibleName('不知道问什么,你先帮我看看👀');
|
||||
expect(help()).toHaveAccessibleName('帮我整体看🧠');
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: '我先自己想想' } });
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.change(input, { target: { value: ' ' } });
|
||||
expect(help()).toBeEnabled();
|
||||
@@ -517,11 +517,11 @@ describe('teacher side chat', () => {
|
||||
expect(screen.queryByRole('button', { name: '尚未确认的问题' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(api.send).toHaveBeenCalledWith('p/agent-topics', 'first', expect.objectContaining({
|
||||
intent: 'suggestions', text: '不知道问什么,你先帮我看看👀', sourceConversationId: 'c',
|
||||
intent: 'suggestions', text: '帮我整体看🧠', sourceConversationId: 'c',
|
||||
}));
|
||||
expect(help()).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '我自己还想问的另一件事' } });
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify({
|
||||
@@ -532,7 +532,7 @@ describe('teacher side chat', () => {
|
||||
expect(screen.queryByText('我看看你最近做到了哪里…')).not.toBeInTheDocument();
|
||||
for (const question of suggestedQuestions) expect(screen.getByRole('button', { name: question })).toBeEnabled();
|
||||
expect(screen.queryByRole('button', { name: '我也说不清,你带我看看', exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
expect(input).toHaveValue('我自己还想问的另一件事');
|
||||
});
|
||||
@@ -567,8 +567,8 @@ describe('teacher side chat', () => {
|
||||
it('keeps an uncertain suggestions request identity through reopening and draft edits', async () => {
|
||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我还没写完的问题' } });
|
||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||
@@ -578,10 +578,10 @@ describe('teacher side chat', () => {
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByLabelText('向智能体提问')).toBeEnabled());
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我还没写完的问题');
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '' } });
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(api.send.mock.calls[1][2]).toEqual(originalRequest);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
@@ -592,16 +592,16 @@ describe('teacher side chat', () => {
|
||||
localStorage.setItem('makelore-consultation-draft:signed-out:p:teacher', JSON.stringify({ text: '', references: [], pending: original }));
|
||||
api.send.mockRejectedValue(new Error('网络暂不可用'));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[0][2]))).toEqual(original);
|
||||
view.unmount();
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('网络暂不可用');
|
||||
expect(JSON.parse(JSON.stringify(api.send.mock.calls[1][2]))).toEqual(original);
|
||||
});
|
||||
@@ -626,13 +626,13 @@ describe('teacher side chat', () => {
|
||||
api.read.mockResolvedValue({ ...first, requests: [suggestionsRequest({ id: original.requestId, text: original.text, status })] });
|
||||
api.send.mockResolvedValue({ ...first, revision: 2 });
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' }));
|
||||
await waitFor(() => expect(api.send).toHaveBeenCalledTimes(1));
|
||||
const sent = api.send.mock.calls[0][2] as TeacherSend;
|
||||
expect(sent.requestId).not.toBe(original.requestId);
|
||||
expect(sent).toMatchObject({ text: '不知道问什么,你先帮我看看👀', intent: 'suggestions', references: [], sourceConversationId: 'c' });
|
||||
expect(sent).toMatchObject({ text: '帮我整体看🧠', intent: 'suggestions', references: [], sourceConversationId: 'c' });
|
||||
});
|
||||
|
||||
it.each(['failed', 'cancelled', 'interrupted'] as const)('retries a %s suggestion request without leaking raw JSON or reusing its accepted request id', async status => {
|
||||
@@ -645,7 +645,7 @@ describe('teacher side chat', () => {
|
||||
await screen.findByText('这次没有看清楚,请再试一次。');
|
||||
expect(screen.queryByText('{"intro":"未完成')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: suggestedQuestions[0] })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: '.consultation-retry' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: '.consultation-retry' }));
|
||||
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
||||
expect(api.send.mock.calls[0][2]).toMatchObject({ intent: 'suggestions' });
|
||||
expect(api.send.mock.calls[0][2].requestId).not.toBe(failed.id);
|
||||
@@ -657,9 +657,9 @@ describe('teacher side chat', () => {
|
||||
...first, revision: 3, requests: [accepted, suggestionsRequest({ id: input.requestId })],
|
||||
}));
|
||||
const view = render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeEnabled());
|
||||
await waitFor(() => expect(streams.has('first')).toBe(true));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' }));
|
||||
await screen.findByText('响应中途断开');
|
||||
fireEvent.change(screen.getByLabelText('向智能体提问'), { target: { value: '我的自由提问继续留着' } });
|
||||
const originalRequest = api.send.mock.calls[0][2] as TeacherSend;
|
||||
@@ -673,10 +673,10 @@ describe('teacher side chat', () => {
|
||||
act(() => streams.get('first')!.dispatchEvent(new MessageEvent('snapshot', { data: JSON.stringify(recovered) })));
|
||||
}
|
||||
await screen.findByText('智能体已接到,但这次回复失败了');
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: '.consultation-retry' })).toBeEnabled());
|
||||
await waitFor(() => expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: '.consultation-retry' })).toBeEnabled());
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('我的自由提问继续留着');
|
||||
expect(api.send).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: '.consultation-retry' }));
|
||||
fireEvent.click(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: '.consultation-retry' }));
|
||||
await screen.findByText('我们可以从最近遇到的这两个地方聊起。');
|
||||
expect(api.send).toHaveBeenCalledTimes(2);
|
||||
expect(api.send.mock.calls[1][2].requestId).not.toBe(originalRequest.requestId);
|
||||
@@ -699,7 +699,7 @@ describe('teacher side chat', () => {
|
||||
expect(api.send.mock.calls[0][2].intent).toBeUndefined();
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
expect(screen.queryByRole('button', { name: '移除引用' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeEnabled();
|
||||
expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeEnabled();
|
||||
view.unmount();
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('');
|
||||
@@ -787,7 +787,7 @@ describe('teacher side chat', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '运营配置的预览问题' }));
|
||||
expect(screen.getByLabelText('向智能体提问')).toHaveValue('运营配置的预览问题');
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '智能体新话题' }));
|
||||
await waitFor(() => expect(api.create).toHaveBeenCalledWith('preview/sample', 7, '学生的练习代码', undefined));
|
||||
});
|
||||
@@ -911,7 +911,7 @@ describe('merged cloud teacher classroom', () => {
|
||||
api.sendConversation.mockRejectedValue(new Error('结果暂未确认'));
|
||||
const props = { projectId: 'p', sourceId: 'original-source', selectedAgent: agent };
|
||||
const view = render(<TeacherChatPanel {...props} />);
|
||||
const hint = () => screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' });
|
||||
const hint = () => screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' });
|
||||
await waitFor(() => expect(hint()).toBeEnabled());
|
||||
expect(api.sendConversation).not.toHaveBeenCalled();
|
||||
fireEvent.click(hint());
|
||||
@@ -984,10 +984,10 @@ describe('merged cloud teacher classroom', () => {
|
||||
render(<TeacherChatPanel projectId="p" sourceId="c" />);
|
||||
await waitFor(() => expect(screen.getByText('智能体暂未开放,历史仍可查看。')).toBeVisible());
|
||||
const input = screen.getByLabelText('向智能体提问');
|
||||
expect(screen.getByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).toBeDisabled();
|
||||
expect(screen.getByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).toBeDisabled();
|
||||
fireEvent.change(input, { target: { value: '继续讨论' } });
|
||||
expect(screen.getByRole('button', { name: '提问', exact: true })).toBeDisabled();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '不知道问什么,你先帮我看看👀', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText((_text, element) => element?.textContent === '帮我整体看🧠', { selector: 'form .consultation-hint' })).not.toBeInTheDocument();
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(api.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
33
tests/unit/community.test.tsx
Normal file
33
tests/unit/community.test.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { COMMUNITY_URL } from '@/lib/community';
|
||||
import { CommunityLink } from '@/components/community/CommunityLink';
|
||||
import { CommunityInvitation } from '@/components/community/CommunityInvitation';
|
||||
|
||||
vi.mock('@/lib/api-client', () => ({ invokeIpc: vi.fn() }));
|
||||
beforeEach(() => { vi.mocked(invokeIpc).mockReset().mockResolvedValue(undefined); });
|
||||
afterEach(cleanup);
|
||||
|
||||
it('only opens the public explore URL after a click, without fetching or embedding works', async () => {
|
||||
const fetch = vi.spyOn(globalThis, 'fetch');
|
||||
render(<CommunityInvitation />);
|
||||
expect(invokeIpc).not.toHaveBeenCalled();
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
expect(document.querySelector('iframe')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '去作品广场逛逛' }));
|
||||
await waitFor(() => expect(invokeIpc).toHaveBeenCalledExactlyOnceWith('shell:openExternal', COMMUNITY_URL));
|
||||
fetch.mockRestore();
|
||||
});
|
||||
|
||||
it('provides an accessible collapsed sidebar entry and a retry after opening fails', async () => {
|
||||
vi.mocked(invokeIpc).mockRejectedValueOnce(new Error('unavailable'));
|
||||
render(<CommunityLink variant="sidebar" collapsed />);
|
||||
const button = screen.getByRole('button', { name: '作品广场' });
|
||||
fireEvent.click(button);
|
||||
await screen.findByRole('alert');
|
||||
await waitFor(() => expect(button).toBeEnabled());
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => expect(invokeIpc).toHaveBeenCalledTimes(2));
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
Reference in New Issue
Block a user