feat: integrate pixel teacher presence and resilient classroom preview
This commit is contained in:
@@ -96,6 +96,9 @@ async function installCodingFirstChatHost(
|
||||
releaseSnapshot: (() => void) | null;
|
||||
snapshotPending: boolean;
|
||||
snapshotSettled: boolean;
|
||||
workReady?: boolean;
|
||||
workFailed?: boolean;
|
||||
workGeneration?: number;
|
||||
};
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__makelorePiFirstChatE2E?: MainState;
|
||||
@@ -521,6 +524,11 @@ async function installCodingFirstChatHost(
|
||||
};
|
||||
}
|
||||
|
||||
if (path.endsWith('/teacher-check-in') && method === 'POST') {
|
||||
const current = consultationTopics.teacher ?? {schemaVersion:1,revision:0,id:'teacher-topic',accountId:'e2e',projectId:project.id,role:'teacher',sourceConversationId:'project',definition:teacherDefinition,version:1,createdAt:now,updatedAt:now,requests:[]};
|
||||
consultationTopics.teacher = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,intent:'check-in',text:'',references:[],sourceConversationId:body!.sourceConversationId,createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,status:'completed',response:'你刚才想加排行榜,我们一起想想比什么更有意思?'}]};
|
||||
return respond({topic:consultationTopics.teacher});
|
||||
}
|
||||
if (path === '/api/coding/teacher/config') return respond({enabled:true,revision:1,published_version:1,definition:teacherDefinition});
|
||||
if (path === '/api/coding/friend/config') return respond({enabled:true,revision:1,published_version:1,definition:friendDefinition});
|
||||
const consultationRole = path.match(/\/(teacher|friend)-topics(?:\/|$)/)?.[1];
|
||||
@@ -584,7 +592,13 @@ async function installCodingFirstChatHost(
|
||||
if (path.startsWith('/api/agent-browser/state?')) {
|
||||
return respond({ success: true, browser: browserSnapshot() });
|
||||
}
|
||||
if (path === '/api/agent-browser/open' && method === 'POST') {
|
||||
if (path === '/api/agent-browser/ensure-work' && method === 'POST') {
|
||||
if (state.workFailed) return respond({ success: true, status: 'failed', message: '作品还没有打开,我可以检查启动情况,再帮你打开。' });
|
||||
return respond(state.workReady === false
|
||||
? { success: true, status: 'starting', conversation }
|
||||
: { success: true, status: 'ready', browser: browserSnapshot({ browserId: 'browser-e2e', state: 'attached', generation: state.workGeneration ?? 1, url: 'http://127.0.0.1:4173/' }) });
|
||||
}
|
||||
if ((path === '/api/agent-browser/open' || path === '/api/agent-browser/navigate') && method === 'POST') {
|
||||
return respond({
|
||||
success: true,
|
||||
browser: browserSnapshot({
|
||||
@@ -603,7 +617,7 @@ async function installCodingFirstChatHost(
|
||||
browser: browserSnapshot({
|
||||
browserId: 'browser-e2e',
|
||||
state: 'attached',
|
||||
generation: 1,
|
||||
generation: state.workGeneration ?? 1,
|
||||
url: 'http://127.0.0.1:4173/',
|
||||
visible: body?.visible === true,
|
||||
bounds: body?.bounds ?? null,
|
||||
@@ -1054,7 +1068,7 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
|
||||
page.getByTestId('coding-conversation-timeline').getByText('Build the first PI scene'),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('img', { name: '对话图片附件' })).toBeVisible();
|
||||
await expect(page.getByText('1 条消息已被本地 Agent 接收。')).toBeVisible();
|
||||
await expect(page.getByText(/条消息已被本地 Agent 接收/)).toHaveCount(0);
|
||||
const state = await readState(electronApp);
|
||||
const uploads = state.captured.filter((request) => (
|
||||
request.path === '/api/coding/attachments' && request.method === 'POST'
|
||||
@@ -1432,7 +1446,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
}
|
||||
await browserToggle.click();
|
||||
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
|
||||
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveAttribute('data-state', 'closed');
|
||||
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveCount(0);
|
||||
await expect.poll(async () => {
|
||||
const state = await readState(electronApp);
|
||||
return state.captured.some((request) => request.path.startsWith('/api/agent-browser/state?'));
|
||||
@@ -1726,10 +1740,34 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await page.reload();page=await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();await page.evaluate(()=>{window.location.hash='/chat';});
|
||||
await expect(page.getByTestId('project-conversations')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar')).toHaveCSS('width', '171px');
|
||||
const rail = page.getByTestId('sidebar');
|
||||
const railResizer = page.getByRole('separator', { name: '调整左侧功能栏宽度' });
|
||||
const handle = (await railResizer.boundingBox())!;
|
||||
await page.mouse.move(handle.x + handle.width / 2, handle.y + 80);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(handle.x + 180, handle.y + 80, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
await expect(rail).toHaveCSS('width', '256px');
|
||||
await expect(page.getByTestId('titlebar-sidebar-surface')).toHaveCSS('width', '256px');
|
||||
await expect(page.getByTestId('coding-conversation-header-titlebar')).toHaveCSS('left', '256px');
|
||||
await railResizer.focus();
|
||||
await railResizer.press('Home');
|
||||
await expect(rail).toHaveCSS('width', '128px');
|
||||
await railResizer.press('End');
|
||||
await expect(rail).toHaveCSS('width', '256px');
|
||||
await page.getByTestId('titlebar-sidebar-toggle').click();
|
||||
await expect(railResizer).toHaveCount(0);
|
||||
await page.getByTestId('titlebar-sidebar-toggle').click();
|
||||
await expect(rail).toHaveCSS('width', '256px');
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await expect(rail).toHaveCSS('width', '256px');
|
||||
await railResizer.dblclick({ position: { x: 3, y: 80 } });
|
||||
await expect(rail).toHaveCSS('width', '171px');
|
||||
await expect(page.getByTestId('coding-conversation-sidebar')).toHaveCount(0);
|
||||
await expect(page.getByRole('tab',{name:'操作对话',exact:true})).toHaveAttribute('aria-selected','true');
|
||||
await expect(page.getByRole('button',{name:/项目共识|记一下|记入共识/})).toHaveCount(0);
|
||||
const composer=page.getByTestId('coding-message-composer').getByRole('textbox');
|
||||
const composer=page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
|
||||
await composer.fill('保留我的草稿');
|
||||
await page.getByRole('tab',{name:'作品',exact:true}).click();
|
||||
await expect(page.getByRole('tabpanel',{name:'作品',exact:true})).toBeVisible();
|
||||
@@ -1738,12 +1776,19 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await expect(composer).toHaveValue('保留我的草稿');
|
||||
await page.getByRole('button',{name:'老师',exact:true}).click();
|
||||
const teacher=page.getByTestId('teacher-chat-panel');
|
||||
await expect(page.getByRole('tab',{name:'作品',exact:true})).toHaveAttribute('aria-selected','true');
|
||||
await expect(page.getByRole('tabpanel',{name:'作品',exact:true})).toBeVisible();
|
||||
await expect(composer).toBeHidden();
|
||||
await expect(composer).toHaveValue('保留我的草稿');
|
||||
await expect(teacher.getByText('有问题,随时来找我')).toBeVisible();
|
||||
await expect(page.locator('#coding-consultation-dock')).toHaveCSS('width', '508px');
|
||||
await expect(teacher.getByTestId('consultation-composer-actions').getByRole('button', { name: '老师帮我看看', exact: true })).toBeVisible();
|
||||
await expect(teacher.getByText('把你的困惑说出来就好')).toHaveCount(0);
|
||||
await expect(teacher.getByText('当前项目',{exact:true})).toHaveCount(0);
|
||||
await expect(teacher.getByRole('button',{name:'解释当前代码',exact:true})).toHaveCount(0);
|
||||
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
|
||||
await teacher.getByRole('textbox',{name:'向老师提问'}).fill('还没说完的困惑');
|
||||
await teacher.getByRole('button',{name:'老师,帮我看看',exact:true}).click();
|
||||
await teacher.getByRole('button',{name:'老师帮我看看',exact:true}).click();
|
||||
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
|
||||
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
|
||||
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
|
||||
@@ -1751,12 +1796,16 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
|
||||
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
|
||||
await expect(teacher.getByRole('button',{name:'带回主会话草稿'})).toHaveCount(0);
|
||||
await teacher.getByRole('button',{name:'我去试一试'}).click();
|
||||
await expect(teacher.getByRole('button',{name:'我去试一试'})).toHaveCount(0);
|
||||
await expect(teacher.getByRole('button',{name:'复制',exact:true})).toHaveCount(0);
|
||||
await expect(composer).toHaveValue('保留我的草稿');
|
||||
await teacher.getByRole('button',{name:'我也说不清,你带我看看',exact:true}).click();
|
||||
await expect(teacher.getByText('你最近做的哪一步,让你停下来想了一会儿?')).toBeVisible();
|
||||
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
|
||||
await page.getByRole('tab',{name:'操作对话',exact:true}).click();
|
||||
await expect(composer).toBeVisible();
|
||||
await page.getByRole('button',{name:'朋友',exact:true}).click();
|
||||
await expect(page.getByRole('tab',{name:'操作对话',exact:true})).toHaveAttribute('aria-selected','true');
|
||||
await expect(teacher).toHaveAttribute('data-role','friend');
|
||||
await expect(teacher.getByText('一起看看你的作品')).toBeVisible();
|
||||
await expect(teacher.getByRole('textbox',{name:'向朋友提问'})).toHaveValue('');
|
||||
@@ -1768,12 +1817,13 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await expect(teacher.getByRole('textbox',{name:'向朋友提问'})).toHaveValue('你觉得哪里有意思?');
|
||||
await page.getByRole('button',{name:'老师',exact:true}).click();
|
||||
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
|
||||
await expect(page.getByRole('tab',{name:'作品',exact:true})).toHaveAttribute('aria-selected','true');
|
||||
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
|
||||
const requests=(await readState(electronApp)).captured;
|
||||
expect(requests.filter(item=>item.path.endsWith('/prompt')&&item.method==='POST')).toHaveLength(0);
|
||||
const consultationRequests=requests.filter(item=>item.path.endsWith('/messages')&&item.method==='POST');
|
||||
expect(consultationRequests).toHaveLength(3);
|
||||
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();
|
||||
expect(consultationRequests[2].body).toMatchObject({intent:'guided-help',text:'我也说不清,你带我看看'});
|
||||
@@ -1784,3 +1834,242 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
|
||||
} finally {await releaseSnapshot(electronApp);}
|
||||
});
|
||||
|
||||
|
||||
test('settled prompt releases the composer without a client receipt and keeps the next draft', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const connection = await page.evaluate(async () => ({ token: await window.electron.ipcRenderer.invoke('hostapi:token') as string, baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string }));
|
||||
await installCodingFirstChatHost(electronApp, connection, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await disableCodingEventSource(page);
|
||||
try {
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const composer = page.getByTestId('coding-message-composer');
|
||||
const input = composer.getByRole('textbox');
|
||||
const send = composer.getByRole('button', { name: '发送', exact: true });
|
||||
await input.fill('hello');
|
||||
await send.click();
|
||||
await expect.poll(async () => (await readState(electronApp)).captured.filter((item) => item.path.endsWith('/prompt') && item.method === 'POST').length).toBe(1);
|
||||
await expect(page.getByTestId('coding-file-attachment-input')).toBeEnabled();
|
||||
await input.fill('我的游戏在哪里哦');
|
||||
await expect(send).toBeDisabled();
|
||||
await expect(page.getByText(/条消息已被本地 Agent 接收/)).toHaveCount(0);
|
||||
const snapshot = {
|
||||
schemaVersion: 1,
|
||||
conversation: { id: 'conversation-pi-first-chat', projectId: 'project-pi-first-chat', agentId: 'builder', title: '新对话', model: { model: { accountId: 'account-e2e', modelId: 'model-a', thinkingLevel: 'off' }, modelResolution: 'resolved' } },
|
||||
nodes: [{ kind: 'message', id: 'durable-hello', sourceEntryId: 'durable-hello', role: 'user', status: 'complete', blocks: [{ kind: 'text', id: 'durable-hello:text', text: 'hello', status: 'complete' }] }],
|
||||
run: { status: 'idle', runId: 'run-e2e-feature', mode: 'prompt', terminalReason: 'completed', settledAt: 2000 },
|
||||
worker: { status: 'ready', generation: 1 },
|
||||
cursor: { workerGeneration: 1, seq: 2 },
|
||||
queue: { items: [] }, context: { usedTokens: 0, contextWindow: 4096, compaction: 'idle' }, pendingInteractions: [],
|
||||
};
|
||||
await emitCodingEvent(page, 'snapshot', { type: 'snapshot', conversationId: snapshot.conversation.id, workerGeneration: 1, seq: 2, snapshot });
|
||||
// An older run's completion is not permission to send the accepted prompt twice.
|
||||
await expect(send).toBeDisabled();
|
||||
await emitCodingEvent(page, 'snapshot', { type: 'snapshot', conversationId: snapshot.conversation.id, workerGeneration: 1, seq: 3, snapshot: { ...snapshot, run: { ...snapshot.run, runId: 'run-e2e-1' }, cursor: { workerGeneration: 1, seq: 3 } } });
|
||||
await expect(send).toBeEnabled();
|
||||
await expect(input).toHaveValue('我的游戏在哪里哦');
|
||||
await expect(page.getByText('正在提交…', { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByTestId('coding-conversation-timeline').getByText('hello', { exact: true })).toHaveCount(1);
|
||||
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/prompt') && item.method === 'POST')).toHaveLength(1);
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
|
||||
test('teacher stays in the top-right header and proactively checks in without taking over the student draft', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const connection = await page.evaluate(async () => ({ token: await window.electron.ipcRenderer.invoke('hostapi:token') as string, baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string }));
|
||||
await installCodingFirstChatHost(electronApp, connection, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await disableCodingEventSource(page);
|
||||
await page.clock.install();
|
||||
try {
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const header = page.getByTestId('coding-conversation-header');
|
||||
const companion = header.getByTestId('teacher-companion');
|
||||
const teacherButton = companion.getByRole('button', { name: '老师', exact: true });
|
||||
await expect(teacherButton).toHaveText('代码老师');
|
||||
await expect(teacherButton).toBeVisible();
|
||||
await expect(teacherButton.locator('img')).toHaveAttribute('src', /teacher-pixel/);
|
||||
await expect(page.getByRole('checkbox', { name: '老师偶尔来看看' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('teacher-companion')).toHaveCount(1);
|
||||
const invitation = companion.getByTestId('teacher-invitation');
|
||||
await expect(invitation).toHaveAttribute('data-bubble-kind', 'welcome');
|
||||
await expect(invitation).toContainText('一起理解代码');
|
||||
await expect(invitation).toBeInViewport({ ratio: 1 });
|
||||
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(0);
|
||||
await page.screenshot({ path: test.info().outputPath('teacher-entry-greeting.png') });
|
||||
await expect(page.getByRole('button', { name: /举手/ })).toHaveCount(0);
|
||||
const headerBounds = await header.boundingBox();
|
||||
const teacherBounds = await teacherButton.boundingBox();
|
||||
expect(headerBounds).not.toBeNull();
|
||||
expect(teacherBounds).not.toBeNull();
|
||||
expect(teacherBounds!.x).toBeGreaterThan(headerBounds!.x + headerBounds!.width / 2);
|
||||
expect(teacherBounds!.y).toBeGreaterThanOrEqual(headerBounds!.y);
|
||||
expect(teacherBounds!.y + teacherBounds!.height).toBeLessThanOrEqual(headerBounds!.y + headerBounds!.height + 1);
|
||||
const composer = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
|
||||
await composer.fill('我还在写自己的想法');
|
||||
await page.getByRole('tab', { name: '作品', exact: true }).click();
|
||||
await expect(teacherButton).toBeVisible();
|
||||
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
|
||||
await composer.focus();
|
||||
await page.clock.fastForward(300_000);
|
||||
await expect(invitation).toContainText('排行榜');
|
||||
await expect(invitation).toHaveAttribute('data-bubble-kind', 'check-in');
|
||||
await expect(invitation).toBeInViewport({ ratio: 1 });
|
||||
const presence = companion.getByTestId('teacher-presence');
|
||||
await expect(presence.getByRole('button', { name: '打开这条老师消息' })).toBeVisible();
|
||||
await expect(presence.getByRole('button', { name: '打开这条老师消息' }).locator('img')).toHaveAttribute('src', await teacherButton.locator('img').getAttribute('src') as string);
|
||||
const [invitationBounds, companionBounds, avatarBounds, presenceBounds, speakerBounds] = await Promise.all([
|
||||
invitation.boundingBox(), companion.boundingBox(), teacherButton.locator('img').boundingBox(), presence.boundingBox(), presence.getByRole('button', { name: '打开这条老师消息' }).boundingBox(),
|
||||
]);
|
||||
expect(invitationBounds).not.toBeNull();
|
||||
expect(companionBounds).not.toBeNull();
|
||||
expect(avatarBounds).not.toBeNull();
|
||||
expect(invitationBounds!.y).toBeGreaterThanOrEqual(avatarBounds!.y + avatarBounds!.height);
|
||||
expect(Math.abs(presenceBounds!.x + presenceBounds!.width - companionBounds!.x - companionBounds!.width)).toBeLessThanOrEqual(2);
|
||||
expect(speakerBounds!.x - invitationBounds!.x - invitationBounds!.width).toBeLessThanOrEqual(10);
|
||||
await expect(composer).toBeFocused();
|
||||
await expect(composer).toHaveValue('我还在写自己的想法');
|
||||
await expect(page.getByRole('tab', { name: '操作对话', exact: true })).toHaveAttribute('aria-selected', 'true');
|
||||
await page.screenshot({ path: test.info().outputPath('teacher-proactive-invitation.png') });
|
||||
await page.getByRole('tab', { name: '作品', exact: true }).click();
|
||||
await expect(invitation).toBeInViewport({ ratio: 1 });
|
||||
const [workInvitationBounds, browserViewportBounds] = await Promise.all([
|
||||
presence.boundingBox(), page.getByTestId('agent-browser-viewport').boundingBox(),
|
||||
]);
|
||||
expect(workInvitationBounds).not.toBeNull();
|
||||
expect(browserViewportBounds).not.toBeNull();
|
||||
// The native view uses this rectangle and cannot be covered by a DOM z-index.
|
||||
expect(browserViewportBounds!.y).toBeGreaterThanOrEqual(workInvitationBounds!.y + workInvitationBounds!.height);
|
||||
const workPane = page.getByRole('tabpanel', { name: '作品', exact: true });
|
||||
const workPanel = page.getByTestId('agent-browser-panel');
|
||||
// Account for existing DOM toolbars instead of reserving a full bubble row.
|
||||
expect((await workPanel.boundingBox())!.y - (await workPane.boundingBox())!.y).toBeLessThan(65);
|
||||
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
|
||||
await expect(composer).toHaveValue('我还在写自己的想法');
|
||||
await page.clock.fastForward(300_000);
|
||||
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(1);
|
||||
await invitation.getByRole('button', { name: '和老师聊聊' }).click();
|
||||
await expect(invitation).toHaveCount(0);
|
||||
await expect(presence).toHaveCount(0);
|
||||
await expect(teacherButton).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '作品', exact: true })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.getByRole('tabpanel', { name: '作品', exact: true })).toBeVisible();
|
||||
const teacher = page.getByTestId('teacher-chat-panel');
|
||||
await expect(teacher.getByText('你刚才想加排行榜,我们一起想想比什么更有意思?')).toBeVisible();
|
||||
await expect(teacher.locator('.consultation-user')).toHaveCount(0);
|
||||
const teacherInput = teacher.getByRole('textbox', { name: '向老师提问' });
|
||||
await expect(teacherInput).toBeFocused();
|
||||
await teacherInput.fill('我想比谁更了解自己的宠物');
|
||||
await teacher.getByRole('button', { name: '关闭老师' }).click();
|
||||
await expect(page.getByRole('button', { name: '老师', exact: true })).toBeFocused();
|
||||
await page.getByRole('button', { name: '老师', exact: true }).click();
|
||||
await expect(teacherInput).toHaveValue('我想比谁更了解自己的宠物');
|
||||
await teacherInput.fill('');
|
||||
// Let the cleared draft propagate through the parent before advancing the check-in timer.
|
||||
await page.clock.runFor(32);
|
||||
await page.clock.fastForward(300_000);
|
||||
await page.clock.runFor(15_000);
|
||||
await expect(invitation).toBeVisible();
|
||||
await expect(teacher).toBeVisible();
|
||||
await expect(invitation.getByRole('button', { name: '看看老师说的' })).toBeVisible();
|
||||
// The message sits over the separate teacher pane and must not push Work down.
|
||||
await expect.poll(async () => (await workPanel.boundingBox())!.y - (await workPane.boundingBox())!.y).toBe(20);
|
||||
await page.screenshot({ path: test.info().outputPath('teacher-open-proactive-invitation.png') });
|
||||
await expect(invitation).toBeInViewport({ ratio: 1 });
|
||||
await invitation.getByRole('button', { name: '等会儿聊' }).click();
|
||||
await expect(invitation).toHaveCount(0);
|
||||
await expect(composer).toHaveValue('我还在写自己的想法');
|
||||
expect((await readState(electronApp)).captured.filter((item) => /\/(messages|prompt)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
|
||||
} finally { await releaseSnapshot(electronApp); }
|
||||
});
|
||||
|
||||
test('work tab prepares the page automatically and keeps the student draft through repeated switches', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const connection = await page.evaluate(async () => ({ token: await window.electron.ipcRenderer.invoke('hostapi:token') as string, baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string }));
|
||||
await installCodingFirstChatHost(electronApp, connection, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workReady: boolean } }).__makelorePiFirstChatE2E.workReady = false; });
|
||||
await disableCodingEventSource(page);
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const composer = page.getByTestId('coding-message-composer').getByRole('textbox');
|
||||
await composer.fill('还没有发出的想法');
|
||||
await page.getByRole('tab', { name: '作品', exact: true }).click();
|
||||
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '展开调试面板' })).toHaveCount(0);
|
||||
await expect(page.getByRole('status').filter({ hasText: '正在打开你的作品' })).toBeVisible();
|
||||
await page.getByRole('button', { name: '回到操作对话', exact: true }).click();
|
||||
await expect(composer).toHaveValue('还没有发出的想法');
|
||||
await page.getByRole('tab', { name: '作品', exact: true }).click();
|
||||
await expect(page.getByText('正在打开你的作品…', { exact: true })).toBeVisible();
|
||||
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workReady: boolean } }).__makelorePiFirstChatE2E.workReady = true; });
|
||||
await expect(page.getByRole('textbox', { name: '网页地址' })).toHaveValue('http://127.0.0.1:4173/');
|
||||
await expect(page.getByText('正在打开你的作品…', { exact: true })).toHaveCount(0);
|
||||
// A manual address open must not permanently disable recovery checks.
|
||||
await page.getByRole('textbox', { name: '网页地址' }).fill('http://127.0.0.1:4173/');
|
||||
await page.getByRole('textbox', { name: '网页地址' }).press('Enter');
|
||||
await expect(page.getByRole('button', { name: '刷新网页', exact: true })).toBeEnabled();
|
||||
// Background sleep disposes the native view. Returning to the already-open
|
||||
// Work tab must recover without another click or another prompt identity.
|
||||
const opensBeforeSleep = (await readState(electronApp)).captured.filter((item) => item.path === '/api/agent-browser/ensure-work').length;
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
(globalThis as unknown as { __makelorePiFirstChatE2E: { workGeneration: number } }).__makelorePiFirstChatE2E.workGeneration = 2;
|
||||
BrowserWindow.getAllWindows()[0].webContents.send('agent-browser:state', {
|
||||
browserId: null, projectId: 'project-pi-first-chat', projectPath: null, state: 'closed',
|
||||
generation: 1, url: '', title: '', visible: false, bounds: null,
|
||||
canGoBack: false, canGoForward: false, eventCursor: 0,
|
||||
});
|
||||
});
|
||||
// Recover from the closed state itself, even if a focus event already happened.
|
||||
await expect.poll(async () => (await readState(electronApp)).captured.filter((item) => item.path === '/api/agent-browser/ensure-work').length).toBeGreaterThan(opensBeforeSleep);
|
||||
await expect(page.getByRole('button', { name: '刷新网页', exact: true })).toBeEnabled();
|
||||
await expect(page.getByText('正在打开你的作品…', { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText('开发浏览器已关闭。', { exact: true })).toHaveCount(0);
|
||||
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
|
||||
await expect(composer).toHaveValue('还没有发出的想法');
|
||||
const requests = (await readState(electronApp)).captured;
|
||||
const opens = requests.filter((request) => request.path === '/api/agent-browser/ensure-work');
|
||||
expect(opens.length).toBeGreaterThanOrEqual(2);
|
||||
expect(new Set(opens.map((request) => request.body?.request_id)).size).toBe(1);
|
||||
expect(requests.filter((request) => request.path.endsWith('/prompt'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('work failure offers a useful recovery and retains the student draft after opening', async ({ launchElectronApp }) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const connection = await page.evaluate(async () => ({ token: await window.electron.ipcRenderer.invoke('hostapi:token') as string, baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string }));
|
||||
await installCodingFirstChatHost(electronApp, connection, true);
|
||||
await settleSnapshot(electronApp);
|
||||
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workFailed: boolean } }).__makelorePiFirstChatE2E.workFailed = true; });
|
||||
await disableCodingEventSource(page);
|
||||
await page.reload(); page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
const composer = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
|
||||
await composer.fill('我还没有写完的想法');
|
||||
await page.getByRole('tab', { name: '作品', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '帮我检查并打开' })).toBeVisible();
|
||||
await expect(page.getByText('本次处理失败。', { exact: true })).toHaveCount(0);
|
||||
await page.screenshot({ path: test.info().outputPath('work-actionable-recovery.png') });
|
||||
const first = (await readState(electronApp)).captured.filter((item) => item.path === '/api/agent-browser/ensure-work');
|
||||
expect(first).toHaveLength(1);
|
||||
await electronApp.evaluate(() => { (globalThis as unknown as { __makelorePiFirstChatE2E: { workFailed: boolean } }).__makelorePiFirstChatE2E.workFailed = false; });
|
||||
await page.getByRole('button', { name: '帮我检查并打开' }).click();
|
||||
await expect(page.getByRole('button', { name: '帮我检查并打开' })).toHaveCount(0);
|
||||
await expect(page.getByRole('textbox', { name: '网页地址' })).toHaveValue('http://127.0.0.1:4173/');
|
||||
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
|
||||
await expect(composer).toHaveValue('我还没有写完的想法');
|
||||
const requests = (await readState(electronApp)).captured;
|
||||
const opens = requests.filter((item) => item.path === '/api/agent-browser/ensure-work');
|
||||
expect(new Set(opens.map((item) => item.body?.request_id)).size).toBe(2);
|
||||
expect(requests.filter((item) => item.path.endsWith('/prompt'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user