feat: integrate pixel teacher presence and resilient classroom preview

This commit is contained in:
鲨鱼辣椒 committed 2026-09-23 18:23:53 +08:00
1 parent 12d800ec51
commit 7951cca700
60 files changed
+3313 -180

No files matched your search

+94
View File
@@ -82,6 +82,7 @@ describe('AgentBrowserPanel', () => {
width: 400, height: 600, toJSON: () => ({}),
} as DOMRect);
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === '/api/agent-browser/ensure-work') return { success: true, status: 'ready', browser: snapshot() };
if (path.startsWith('/api/agent-browser/state?')) {
return { success: true, browser: snapshot() };
}
@@ -114,6 +115,95 @@ describe('AgentBrowserPanel', () => {
vi.restoreAllMocks();
});
it('offers login recovery instead of repeatedly trying to start the page', async () => {
const original = hostApiFetchMock.getMockImplementation()!;
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => path === '/api/agent-browser/ensure-work'
? { status: 'failed', errorCode: 'CODING_PROVIDER_AUTH_REQUIRED', message: '登录已失效' } : original(path, init));
const onRecovery = vi.fn();
render(<AgentBrowserPanel projectId="project-a" embedded open onOpenChange={vi.fn()} onRecovery={onRecovery} />);
fireEvent.click(await screen.findByRole('button', { name: '登录并继续' }));
expect(onRecovery).toHaveBeenCalledWith('login', true);
expect(hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/agent-browser/ensure-work')).toHaveLength(1);
});
it('shows preparation and a retryable failure without covering it with the native page', async () => {
const original = hostApiFetchMock.getMockImplementation()!;
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === '/api/agent-browser/ensure-work') return { status: 'failed', message: '当前项目还没有可打开的页面。' };
return original(path, init);
});
const onOpenChange = vi.fn();
render(<AgentBrowserPanel projectId="project-a" conversationId="c1" embedded open onOpenChange={onOpenChange} />);
await screen.findByText('当前项目还没有可打开的页面。');
expect(latestPresentation()).toMatchObject({ visible: false });
const first = hostApiFetchMock.mock.calls.find(([path]) => path === '/api/agent-browser/ensure-work');
fireEvent.click(screen.getByRole('button', { name: '帮我检查并打开' }));
await waitFor(() => expect(hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/agent-browser/ensure-work')).toHaveLength(2));
const last = hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/agent-browser/ensure-work').at(-1);
expect(body(first![1]).request_id).not.toBe(body(last![1]).request_id);
fireEvent.click(screen.getByRole('button', { name: '回到操作对话' }));
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it('ignores a stale closed snapshot arriving after foreground restoration', async () => {
let resolveState!: (value: unknown) => void;
const original = hostApiFetchMock.getMockImplementation()!;
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path.startsWith('/api/agent-browser/state?')) return new Promise((resolve) => { resolveState = resolve; });
if (path === '/api/agent-browser/ensure-work') return { status: 'ready', browser: snapshot({ generation: 2 }) };
if (path === '/api/agent-browser/present') return { success: true, browser: snapshot({ generation: 2, visible: body(init).visible === true }) };
return original(path, init);
});
render(<AgentBrowserPanel projectId="project-a" embedded open onOpenChange={vi.fn()} />);
await waitFor(() => expect(latestPresentation()).toMatchObject({ visible: true }));
await act(async () => resolveState({ success: true, browser: snapshot({ generation: 1, browserId: null, state: 'closed', url: '' }) }));
expect(screen.queryByText('正在打开你的作品…')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '刷新网页' })).toBeEnabled();
expect(latestPresentation()).toMatchObject({ visible: true });
});
it('recovers a closed view without waiting for another focus event', async () => {
render(<AgentBrowserPanel projectId="project-a" embedded open onOpenChange={vi.fn()} />);
await waitFor(() => expect(latestPresentation()).toMatchObject({ visible: true }));
act(() => hostEventListeners.get('agent-browser:state')?.(snapshot({ browserId: null, state: 'closed', url: '' })));
await waitFor(() => expect(hostApiFetchMock.mock.calls.filter(([path]) => path === '/api/agent-browser/ensure-work')).toHaveLength(2));
await waitFor(() => expect(screen.getByRole('button', { name: '刷新网页' })).toBeEnabled());
});
it('recovers a presentation-close race and removes the internal error', async () => {
const original = hostApiFetchMock.getMockImplementation()!;
let failures = 0;
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === '/api/agent-browser/present' && body(init).visible && failures++ === 0) throw new Error('开发浏览器已关闭。');
return original(path, init);
});
render(<AgentBrowserPanel projectId="project-a" embedded open onOpenChange={vi.fn()} />);
await waitFor(() => expect(failures).toBe(2));
expect(screen.queryByText('开发浏览器已关闭。')).not.toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('bounds repeated presentation failures and offers a working retry', async () => {
const original = hostApiFetchMock.getMockImplementation()!;
let unavailable = true;
let failures = 0;
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === '/api/agent-browser/present' && body(init).visible && unavailable) {
failures++;
throw new Error('开发浏览器已关闭。');
}
return original(path, init);
});
render(<AgentBrowserPanel projectId="project-a" embedded open onOpenChange={vi.fn()} />);
const retry = await screen.findByRole('button', { name: '重新打开作品' });
expect(failures).toBe(3);
expect(screen.queryByText('开发浏览器已关闭。')).not.toBeInTheDocument();
unavailable = false;
fireEvent.click(retry);
await waitFor(() => expect(latestPresentation()).toMatchObject({ visible: true }));
await waitFor(() => expect(screen.queryByText('作品画面暂时没能显示')).not.toBeInTheDocument());
});
it('does no browser API work while closed and presents an agent-opened snapshot', async () => {
render(<Harness />);
@@ -185,6 +275,10 @@ describe('AgentBrowserPanel', () => {
await waitFor(() => expect(latestPresentation()).toMatchObject({ visible: true }));
expect(screen.getByRole('textbox', { name: '网页地址' })).toHaveValue('http://127.0.0.1:4173/');
expect(screen.getByRole('button', { name: '刷新网页' })).toBeEnabled();
expect(screen.queryByTestId('agent-browser-diagnostics')).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /Console|Network/ })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '展开调试面板' })).not.toBeInTheDocument();
expect(hostApiFetchMock.mock.calls.some(([path]) => ['/api/agent-browser/diagnostics', '/api/agent-browser/cdp/events'].includes(String(path)))).toBe(false);
expect(hostApiFetchMock.mock.calls.some(([path]) => ['/api/agent-browser/close', '/api/agent-browser/open', '/api/agent-browser/navigate'].includes(String(path)))).toBe(false);
view.unmount();