{new URLSearchParams(location.search).has('sidebar') &&
}
diff --git a/tests/e2e/fixtures/teacher-preview-layout.tsx b/tests/e2e/fixtures/teacher-preview-layout.tsx
index 36edd026..e453b43b 100644
--- a/tests/e2e/fixtures/teacher-preview-layout.tsx
+++ b/tests/e2e/fixtures/teacher-preview-layout.tsx
@@ -19,7 +19,7 @@ export function Layout() {
const [refreshStatus, setRefreshStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [selected, setSelected] = useState(0);
const welcomeMode = new URLSearchParams(location.search).has('welcomes');
- const [dismissed, setDismissed] = useState
([]);
+ const [dismissed, setDismissed] = useState(false);
const agents = Array.from({ length: count }, (_, index) => ({
teacher_id: `agent-${index}`, version: index + 1, is_default: index === 0,
definition: { config_id: `agent-${index}`, name: `智能体${index + 1}`, avatar_id: 'avatar-06',
@@ -31,8 +31,8 @@ export function Layout() {
definition: agents[selected]?.definition ?? { name: '编程老师', avatar_id: 'avatar-06' },
invitation: !welcomeMode && visible ? { response: long ? '你刚才说想做一个宠物游戏。我们一起想想,什么样的小动作,会让你愿意一直陪它玩下去?可以先挑一个最想试的想法。' : '有什么编程问题想一起讨论?' } : null,
welcome: null, enabled: true, loading: false, checking: false, error: null,
- welcomes: welcomeMode && !open ? agents.flatMap((agent, index) => dismissed.includes(agent.teacher_id) ? [] : [{ agentId: agent.teacher_id, definition: agent.definition, text: agent.definition.description, tone: index % 3 }]) : [],
- dismissWelcome: (id: string) => setDismissed(ids => [...new Set([...ids, id])]),
+ welcomes: welcomeMode && !open && !dismissed ? agents.map((agent, index) => ({ agentId: agent.teacher_id, definition: agent.definition, text: agent.definition.description, tone: index % 3 })) : [],
+ dismissWelcomes: () => setDismissed(true),
refreshing, refreshStatus,
refresh: async () => {
setRefreshing(true);
diff --git a/tests/e2e/pi-coding-first-chat.spec.ts b/tests/e2e/pi-coding-first-chat.spec.ts
index ae65e89e..0a7bcb75 100644
--- a/tests/e2e/pi-coding-first-chat.spec.ts
+++ b/tests/e2e/pi-coding-first-chat.spec.ts
@@ -2324,6 +2324,42 @@ test('teacher introductions collapse after three operation replies and remain av
} finally { await releaseSnapshot(electronApp); }
});
+test('teacher introduction group closes all three and stays closed for a new teacher after refresh and reload', 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);
+ const addTeacher = async (id: string) => electronApp.evaluate((_electron, id) => {
+ const state = (globalThis as typeof globalThis & { __makelorePiFirstChatE2E: TeacherCatalogFixture }).__makelorePiFirstChatE2E;
+ const original = state.teacherCatalog.items[0];
+ state.teacherCatalog.items.push({ ...original, teacher_id: id, is_default: false,
+ definition: { ...original.definition, config_id: id, name: id } });
+ }, id);
+ try {
+ await addTeacher('第三位老师');
+ await page.reload(); page = await getStableWindow(electronApp);
+ await page.getByTestId('ai-module-option-programming').click();
+ await page.evaluate(() => { window.location.hash = '/chat'; });
+ const group = page.getByTestId('teacher-welcomes');
+ await expect(group.locator('.teacher-welcome-card')).toHaveCount(3);
+ await group.getByRole('button', { name: '收起全部老师介绍(朋友)' }).click();
+ await expect(group).toHaveCount(0);
+ await expect(page.getByTestId('teacher-chat-panel')).toHaveCount(0);
+ await addTeacher('第四位老师');
+ await page.getByRole('button', { name: '刷新智能体', exact: true }).click();
+ await expect(page.getByRole('button', { name: '与第四位老师聊天', exact: true })).toBeVisible();
+ await expect(group).toHaveCount(0);
+ await page.reload();
+ await expect(page.getByRole('button', { name: '与第四位老师聊天', exact: true })).toBeVisible();
+ await expect(group).toHaveCount(0);
+ await page.getByRole('button', { name: '与代码智能体聊天', exact: true }).hover();
+ await expect(page.getByTestId('teacher-introduction')).toContainText('一起发现代码里的小奥秘');
+ expect((await readState(electronApp)).captured.filter(item => /\/(agent-topics|messages|teacher-check-in|prompt)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
+ } finally { await releaseSnapshot(electronApp); }
+});
+
test('teacher avatar consumes the welcome bubble through close, reopen and reload without a model request', async ({ launchElectronApp }) => {
const electronApp = await launchElectronApp({ skipSetup: true });
let page = await getStableWindow(electronApp);
@@ -2355,7 +2391,7 @@ test('teacher avatar consumes the welcome bubble through close, reopen and reloa
await page.reload();
await expect(teacherButton).toBeVisible();
await expect(invitation).toHaveCount(0);
- await expect(page.getByRole('button', { name: '收起朋友的介绍' })).toBeVisible();
+ await expect(page.getByTestId('teacher-welcomes')).toHaveCount(0);
expect((await readState(electronApp)).captured.filter(item => /\/(agent-topics|messages|teacher-check-in|prompt)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
} finally { await releaseSnapshot(electronApp); }
});
diff --git a/tests/e2e/teacher-preview-layout.spec.ts b/tests/e2e/teacher-preview-layout.spec.ts
index ee016f61..2e33e951 100644
--- a/tests/e2e/teacher-preview-layout.spec.ts
+++ b/tests/e2e/teacher-preview-layout.spec.ts
@@ -79,15 +79,15 @@ for (const width of [375, 768, 1280]) {
return !!bubble && !!viewport && viewport.y >= bubble.y + bubble.height;
}).toBe(true);
await page.screenshot({ path: test.info().outputPath(`teacher-introductions-${width}.png`) });
- await group.getByRole('button', { name: '收起智能体2的介绍' }).click();
- await expect(group.locator('.teacher-welcome-card')).toHaveCount(2);
+ await group.getByRole('button', { name: '收起全部老师介绍(智能体2)' }).click();
+ await expect(group).toHaveCount(0);
await expect(page.locator('#coding-consultation-dock')).toHaveCount(0);
+ await page.reload();
+ await expect(group.locator('.teacher-welcome-card')).toHaveCount(3);
await group.getByRole('button', { name: '和智能体3聊聊' }).click({ position: { x: 8, y: 8 } });
await expect(page.getByRole('button', { name: '与智能体3聊天' })).toHaveAttribute('aria-pressed', 'true');
await expect(group).toHaveCount(0);
await page.getByRole('button', { name: '关闭智能体栏' }).click();
- await expect(group.locator('.teacher-welcome-card')).toHaveCount(1);
- while (await group.count()) await group.getByRole('button', { name: /^收起.*的介绍$/ }).first().click();
await expect(group).toHaveCount(0);
});
}
@@ -108,8 +108,6 @@ test('many teacher introductions remain scrollable and honor reduced motion', as
await page.keyboard.press('Enter');
await expect(page.getByRole('button', { name: '与智能体8聊天' })).toHaveAttribute('aria-pressed', 'true');
await page.getByRole('button', { name: '关闭智能体栏' }).click();
- await expect(group.locator('.teacher-welcome-card')).toHaveCount(7);
- while (await group.count()) await group.getByRole('button', { name: /^收起.*的介绍$/ }).first().click();
await expect(group).toHaveCount(0);
});
diff --git a/tests/unit/teacher-companion.test.tsx b/tests/unit/teacher-companion.test.tsx
index 672adec8..26e4dfd8 100644
--- a/tests/unit/teacher-companion.test.tsx
+++ b/tests/unit/teacher-companion.test.tsx
@@ -59,7 +59,7 @@ beforeEach(() => {
});
afterEach(() => { cleanup(); vi.useRealTimers(); vi.unstubAllGlobals(); });
-describe('individual teacher introductions', () => {
+describe('grouped teacher introductions', () => {
const agents = ['方法老师', '体验老师', '故事老师'].map((name, index) => ({
teacher_id: `intro-${index}`, version: 1, is_default: index === 0,
definition: { ...definition, config_id: `intro-${index}`, name, description: `${name}的自我介绍`, welcome_message: `${name}的聊天欢迎语` },
@@ -133,7 +133,7 @@ describe('individual teacher introductions', () => {
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
});
- it('keeps round windows independent for projects/accounts and newly delivered teachers', async () => {
+ it('keeps project/account windows independent without reopening for newly delivered teachers', async () => {
api.catalog.mockResolvedValue({ items: agents });
const view = await mount();
await operation('one');
@@ -161,15 +161,10 @@ describe('individual teacher introductions', () => {
const added = { ...agents[0], teacher_id: 'new', definition: { ...agents[0].definition, config_id: 'new', name: '新老师' } };
api.catalog.mockResolvedValue({ items: [...agents, added] });
await act(async () => fireEvent.click(screen.getByRole('button', { name: '刷新智能体' })));
- expect(screen.getByTestId('teacher-welcomes').querySelectorAll('.teacher-welcome-card')).toHaveLength(1);
- await operation('new-one');
- await operation('new-two');
- expect(screen.getByRole('button', { name: '收起新老师的介绍' })).toBeVisible();
- await operation('new-three');
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
});
- it('does not count teacher turns and still collapses the other teachers while consultation is open', async () => {
+ it('keeps the entire group dismissed after teacher turns and closing consultation', async () => {
api.catalog.mockResolvedValue({ items: agents });
const topic = { ...checkInTopic(), definition: agents[0].definition, requests: [],
conversation: { agentId: agents[0].teacher_id, segmentTurns: 0, discussions: {} } };
@@ -181,7 +176,7 @@ describe('individual teacher introductions', () => {
await act(async () => fireEvent.click(screen.getByRole('button', { name: '提问', exact: true })));
}
await act(async () => fireEvent.click(screen.getByRole('button', { name: '与方法老师聊天' })));
- expect(screen.getByTestId('teacher-welcomes').querySelectorAll('.teacher-welcome-card')).toHaveLength(2);
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
await act(async () => fireEvent.click(screen.getByRole('button', { name: '与方法老师聊天' })));
await operation('one'); await operation('two'); await operation('three');
await act(async () => fireEvent.click(screen.getByRole('button', { name: '与方法老师聊天' })));
@@ -198,11 +193,9 @@ describe('individual teacher introductions', () => {
expect(within(group).queryByRole('button', { name: '先自己试试' })).not.toBeInTheDocument();
for (const agent of agents) expect(within(group).getByText(agent.definition.description)).toBeVisible();
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
- await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起体验老师的介绍' })));
- expect(within(group).queryByText('体验老师的自我介绍')).not.toBeInTheDocument();
+ await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起全部老师介绍(体验老师)' })));
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
expect(screen.queryByTestId('teacher-chat-panel')).not.toBeInTheDocument();
- expect(within(group).getByText('方法老师的自我介绍')).toBeVisible();
- expect(within(group).getByText('故事老师的自我介绍')).toBeVisible();
await advance();
expect(api.create).not.toHaveBeenCalled();
expect(api.send).not.toHaveBeenCalled();
@@ -214,24 +207,24 @@ describe('individual teacher introductions', () => {
it('keeps dismissal across reloads for this account/project while leaving other projects and accounts independent', async () => {
api.catalog.mockResolvedValue({ items: agents });
const first = await mount();
- await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起方法老师的介绍' })));
+ await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起全部老师介绍(方法老师)' })));
first.unmount();
const next = await mount();
- expect(screen.queryByRole('button', { name: '收起方法老师的介绍' })).not.toBeInTheDocument();
- expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible();
+ expect(screen.queryByRole('button', { name: '收起全部老师介绍(方法老师)' })).not.toBeInTheDocument();
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
await act(async () => next.rerender());
- expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible();
- for (const agent of agents) await act(async () => fireEvent.click(screen.getByRole('button', { name: `收起${agent.definition.name}的介绍` })));
+ expect(screen.getByRole('button', { name: '收起全部老师介绍(方法老师)' })).toBeVisible();
+ await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起全部老师介绍(方法老师)' })));
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
await act(async () => next.rerender());
- expect(screen.queryByRole('button', { name: '收起方法老师的介绍' })).not.toBeInTheDocument();
- expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible();
+ expect(screen.queryByRole('button', { name: '收起全部老师介绍(方法老师)' })).not.toBeInTheDocument();
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
identity.id = 'another-kid';
await act(async () => next.rerender());
- expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible();
+ expect(screen.getByRole('button', { name: '收起全部老师介绍(方法老师)' })).toBeVisible();
});
- it('opens the clicked teacher and consumes only their introduction, preserving the others for later', async () => {
+ it('opens the clicked teacher and consumes the whole group', async () => {
api.catalog.mockResolvedValue({ items: agents });
await mount();
await act(async () => fireEvent.click(within(screen.getByTestId('teacher-welcomes')).getByText('故事老师的自我介绍')));
@@ -240,27 +233,74 @@ describe('individual teacher introductions', () => {
expect(screen.getByTestId('teacher-chat-panel')).not.toHaveTextContent('故事老师的自我介绍');
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
await act(async () => fireEvent.click(screen.getByRole('button', { name: '与故事老师聊天' })));
- expect(screen.queryByRole('button', { name: '收起故事老师的介绍' })).not.toBeInTheDocument();
- expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible();
- expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible();
+ expect(screen.queryByRole('button', { name: '收起全部老师介绍(故事老师)' })).not.toBeInTheDocument();
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
expect(api.create).not.toHaveBeenCalled();
expect(api.sendConversation).not.toHaveBeenCalled();
});
- it('introduces only newly delivered teachers after individually dismissing every introduction, without resetting seen replies or existing receipts on refresh', async () => {
+ it('keeps refreshed and newly delivered teachers dismissed without resetting seen replies', async () => {
localStorage.setItem('makelore-teacher-presence:kid:seen', JSON.stringify(['keep-seen']));
api.catalog.mockResolvedValue({ items: agents });
await mount();
- for (const agent of agents) await act(async () => fireEvent.click(screen.getByRole('button', { name: `收起${agent.definition.name}的介绍` })));
+ await act(async () => fireEvent.click(screen.getByRole('button', { name: '收起全部老师介绍(方法老师)' })));
const added = { ...agents[0], teacher_id: 'new', definition: { ...agents[0].definition, config_id: 'new', name: '新老师', description: '', welcome_message: '新老师的云端欢迎语' } };
api.catalog.mockResolvedValue({ items: [...agents.map(agent => ({ ...agent, version: 2 })), added] });
await act(async () => fireEvent.click(screen.getByRole('button', { name: '刷新智能体' })));
- expect(screen.getByTestId('teacher-welcomes')).toHaveTextContent('新老师的云端欢迎语');
- for (const agent of agents) expect(screen.queryByRole('button', { name: `收起${agent.definition.name}的介绍` })).not.toBeInTheDocument();
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
+ for (const agent of agents) expect(screen.queryByRole('button', { name: `收起全部老师介绍(${agent.definition.name})` })).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem('makelore-teacher-presence:kid:seen')!)).toEqual(['keep-seen']);
expect(api.sendConversation).not.toHaveBeenCalled();
expect(api.checkIn).not.toHaveBeenCalled();
});
+ it('adds every new teacher to the visible group without resetting the shared count', async () => {
+ api.catalog.mockResolvedValue({ items: agents });
+ await mount();
+ await operation('one'); await operation('two');
+ const added = { ...agents[0], teacher_id: 'new', definition: { ...agents[0].definition, config_id: 'new', name: '新老师' } };
+ api.catalog.mockResolvedValue({ items: [...agents, added] });
+ await act(async () => fireEvent.click(screen.getByRole('button', { name: '刷新智能体' })));
+ expect(screen.getByTestId('teacher-welcomes').querySelectorAll('.teacher-welcome-card')).toHaveLength(4);
+ await operation('three');
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
+ });
+
+ const legacyKeys = [
+ 'makelore-teacher-presence:kid:project:project:introduced-agents',
+ 'makelore-teacher-presence:kid:project:project:project:introduced-agents',
+ ];
+ it.each(legacyKeys)('migrates partial dismissal from %s to the entire group without changing old data', async (key) => {
+ const old = JSON.stringify(['intro-1']);
+ localStorage.setItem(key, old);
+ api.catalog.mockResolvedValue({ items: agents });
+ const first = await mount();
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
+ fireEvent.pointerEnter(screen.getByRole('button', { name: '与方法老师聊天' }), { pointerType: 'mouse' });
+ expect(screen.getByRole('tooltip')).toHaveTextContent('方法老师的自我介绍');
+ first.unmount();
+ const next = await mount();
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
+ expect(localStorage.getItem(key)).toBe(old);
+ await act(async () => next.rerender());
+ expect(screen.getByTestId('teacher-welcomes').querySelectorAll('.teacher-welcome-card')).toHaveLength(3);
+ });
+
+ it.each(legacyKeys)('continues legacy operation counts from %s without discarding history', async (key) => {
+ const roundsKey = key + ':rounds';
+ const rounds = JSON.stringify(Object.fromEntries(agents.map(agent => [agent.teacher_id, {
+ since: Date.now() - 1000, runs: [JSON.stringify(['source', 'one']), JSON.stringify(['source', 'two'])],
+ }])));
+ localStorage.setItem(roundsKey, rounds);
+ api.catalog.mockResolvedValue({ items: agents });
+ await mount();
+ expect(screen.getByTestId('teacher-welcomes').querySelectorAll('.teacher-welcome-card')).toHaveLength(3);
+ await operation('two');
+ expect(screen.getByTestId('teacher-welcomes')).toBeVisible();
+ await operation('three');
+ expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
+ expect(localStorage.getItem(roundsKey)).toBe(rounds);
+ });
+
});
describe('manual agent refresh', () => {