Merge branch 'main' of https://git.nianxx.cn/wangxuming/makelore
Electron E2E / Electron E2E (macos-latest) (push) Canceled after 0s
Electron E2E / Electron E2E (ubuntu-latest) (push) Canceled after 0s
Electron E2E / Electron E2E (windows-latest) (push) Canceled after 0s

This commit is contained in:
brother7 committed 2026-10-09 16:11:55 +08:00
commit 87d3f03d75
9 files changed
+259 -34

No files matched your search

@@ -12,7 +12,7 @@ export function Layout() {
const [bubble, setBubble] = useState<HTMLDivElement | null>(null);
const [introduction, setIntroduction] = useState<HTMLDivElement | null>(null);
const [toolbarContainer, setToolbarContainer] = useState<HTMLDivElement | null>(null);
const [workOpen, setWorkOpen] = useState(true);
const [workOpen, setWorkOpen] = useState(!new URLSearchParams(location.search).has('chat'));
const [visible, setVisible] = useState(!new URLSearchParams(location.search).has('quiet'));
const [open, setOpen] = useState(false);
const [width, setWidth] = useState(508);
@@ -51,7 +51,7 @@ export function Layout() {
return <div className="classroom-workspace flex h-screen flex-col bg-white">
<header className="relative z-50 flex h-10 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">布局测试项目 › hello</span>
<TeacherCompanion companion={companion} open={open}
<TeacherCompanion companion={companion} open={open} showWelcomes={!workOpen}
onOpen={(id) => { if (id) setSelected(agents.findIndex((item) => item.teacher_id === id)); setOpen(true); }}
onToggle={(id) => { if (!id || id === agents[selected]?.teacher_id) setOpen(!open); else { setSelected(agents.findIndex((item) => item.teacher_id === id)); setOpen(true); } }}
bubbleRef={setBubble} introductionRef={setIntroduction} />
+15 -15
View File
@@ -2490,6 +2490,16 @@ test('teacher stays in the top-right header without legacy check-ins or taking o
const composer = page.getByTestId('coding-message-composer').getByRole('textbox', { includeHidden: true });
await composer.fill('我还在写自己的想法');
await page.getByRole('tab', { name: '作品', exact: true }).click();
await expect(companion.getByTestId('teacher-welcomes')).toHaveCount(0);
const workPane = page.getByRole('tabpanel', { name: '作品', exact: true });
const workPanel = page.getByTestId('agent-browser-panel');
const expectFullPreview = async () => {
await expect.poll(async () => (await workPanel.boundingBox())!.y - (await workPane.boundingBox())!.y).toBe(20);
const [pane, preview] = await Promise.all([workPane.boundingBox(), workPanel.boundingBox()]);
expect(preview!.height).toBeGreaterThanOrEqual(pane!.height - 41);
};
await expectFullPreview();
await page.screenshot({ path: test.info().outputPath('first-work-full-preview.png') });
await expect(teacherButton).toBeVisible();
await teacherButton.hover();
await page.clock.runFor(250);
@@ -2505,7 +2515,8 @@ test('teacher stays in the top-right header without legacy check-ins or taking o
expect(introductionViewportBounds!.y).toBeGreaterThanOrEqual(introductionBounds!.y + introductionBounds!.height);
await page.keyboard.press('Escape');
await expect(introduction).toHaveCount(0);
await expect(invitation).toBeVisible();
await expect(invitation).toHaveCount(0);
await expectFullPreview();
expect((await readState(electronApp)).captured.filter(item => /\/(messages|teacher-check-in)$/.test(item.path) && item.method === 'POST')).toHaveLength(0);
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await composer.focus();
@@ -2532,21 +2543,10 @@ test('teacher stays in the top-right header without legacy check-ins or taking o
await expect(page.getByRole('tab', { name: '操作对话', exact: true })).toHaveAttribute('aria-selected', 'true');
await page.screenshot({ path: test.info().outputPath('teacher-published-welcome.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');
// Multiple introductions need their actual height, with only the native-view gap added.
const nativeGap = browserViewportBounds!.y - workInvitationBounds!.y - workInvitationBounds!.height;
expect(nativeGap).toBeGreaterThanOrEqual(8);
expect(nativeGap).toBeLessThanOrEqual(9);
await expect(presence).toHaveCount(0);
await expectFullPreview();
await page.getByRole('tab', { name: '操作对话', exact: true }).click();
await expect(presence.locator('.teacher-welcome-card')).toHaveCount(2);
await expect(composer).toHaveValue('我还在写自己的想法');
await page.clock.fastForward(300_000);
expect((await readState(electronApp)).captured.filter((item) => item.path.endsWith('/teacher-check-in'))).toHaveLength(0);
+41 -10
View File
@@ -117,11 +117,15 @@ test('empty work shows a local hint without opening the teacher or polling', asy
await page.screenshot({ path: test.info().outputPath('empty-work-hint.png') });
});
for (const width of [375, 768, 1280]) {
test(`all teacher introductions fit at ${width}px and keep the native preview clear`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 });
await page.goto(baseUrl + '?agents=3&welcomes');
for (const size of [{ width: 375, height: 800 }, { width: 768, height: 800 }, { width: 1280, height: 800 }, { width: 1440, height: 900 }]) {
test(`teacher introductions pause on Work and restore on chat at ${size.width}px`, async ({ page }) => {
await page.setViewportSize(size);
await page.goto(baseUrl + '?agents=3&welcomes&chat=1');
const tabs = page.getByRole('tablist', { name: '当前工作' });
const chatTab = tabs.getByRole('tab', { name: '操作对话' });
const workTab = tabs.getByRole('tab', { name: '作品', exact: true });
const group = page.getByTestId('teacher-welcomes');
await expect(chatTab).toHaveAttribute('aria-selected', 'true');
await expect(group.locator('.teacher-welcome-card')).toHaveCount(3);
await expect(group).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(group).toHaveCSS('box-shadow', 'none');
@@ -132,14 +136,41 @@ for (const width of [375, 768, 1280]) {
await expect(card).toBeInViewport({ ratio: 1 });
expect(await card.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
}
await expect.poll(async () => {
const [bubble, viewport] = await Promise.all([group.boundingBox(), page.getByTestId('agent-browser-viewport').boundingBox()]);
return !!bubble && !!viewport && viewport.y >= bubble.y + bubble.height;
}).toBe(true);
await page.screenshot({ path: test.info().outputPath(`teacher-introductions-${width}.png`) });
await page.screenshot({ path: test.info().outputPath(`teacher-introductions-${size.width}.png`) });
const expectFullPreview = async () => {
await expect(workTab).toHaveAttribute('aria-selected', 'true');
await expect(group).toHaveCount(0);
await expect(page.locator('#coding-consultation-dock')).toHaveCount(0);
await expect(page.getByTestId('agent-browser-panel')).toHaveCSS('margin-top', '0px');
await expect.poll(async () => {
const [work, viewport] = await Promise.all([
page.getByTestId('work-view').boundingBox(), page.getByTestId('agent-browser-viewport').boundingBox(),
]);
// The full native viewport keeps only the work view's 20px padding
// and the panel's 1px border at both ends, with no welcome clearance.
return !!work && !!viewport && Math.abs(viewport.y - work.y - 21) <= 1
&& Math.abs(work.y + work.height - viewport.y - viewport.height - 21) <= 1;
}).toBe(true);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
};
for (let visit = 0; visit < 2; visit += 1) {
await workTab.click();
await expectFullPreview();
if (visit === 0) await page.screenshot({ path: test.info().outputPath(`first-work-with-welcomes-paused-${size.width}.png`) });
await chatTab.click();
await expect(group.locator('.teacher-welcome-card')).toHaveCount(3);
await expect(group).toBeInViewport({ ratio: 1 });
}
await group.getByRole('button', { name: '收起全部老师介绍(智能体2)' }).click();
await expect(group).toHaveCount(0);
await expect(page.locator('#coding-consultation-dock')).toHaveCount(0);
await workTab.click();
await expectFullPreview();
await chatTab.click();
await expect(group).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 } });
@@ -153,7 +184,7 @@ for (const width of [375, 768, 1280]) {
test('many teacher introductions remain scrollable and honor reduced motion', async ({ page }) => {
await page.setViewportSize({ width: 520, height: 600 });
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(baseUrl + '?agents=8&welcomes');
await page.goto(baseUrl + '?agents=8&welcomes&chat=1');
const group = page.getByTestId('teacher-welcomes');
await expect(group).toBeInViewport({ ratio: 1 });
await expect(group.locator('.teacher-welcome-card')).toHaveCount(8);
+48 -2
View File
@@ -21,11 +21,11 @@ const definition: TeacherDefinition = {
function checkInTopic(projectId = 'project', id = 'check-1'): TeacherTopic {
return { schemaVersion: 1, revision: 2, id: 'topic-' + projectId, role: 'teacher', accountId: 'kid', projectId, sourceConversationId: 'project', version: 1, definition, createdAt: 'now', updatedAt: 'now', requests: [{ id, intent: 'check-in', text: '', references: [], createdAt: 'now', sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: 'now', includedSourceMessageIds: [], omittedMessages: 0, status: 'completed', response: '你刚才想加排行榜,我们一起想想比什么更有意思?' }] };
}
function Harness({ projectId = 'project', sourceId = 'source', busy = false, archived = false }: { projectId?: string; sourceId?: string; busy?: boolean; archived?: boolean }) {
function Harness({ projectId = 'project', sourceId = 'source', busy = false, archived = false, showWelcomes = true }: { projectId?: string; sourceId?: string; busy?: boolean; archived?: boolean; showWelcomes?: boolean }) {
const [open, setOpen] = useState(false);
const [teacherComposing, setTeacherComposing] = useState(false);
const companion = useTeacherCompanion({ projectId, sourceId, sourceBusy: busy, sourceArchived: archived, teacherOpen: open, teacherComposing });
return <><TeacherCompanion companion={companion} open={open} onOpen={(id) => { if (id) companion.selectAgent(id); setOpen(true); }}
return <><TeacherCompanion companion={companion} open={open} showWelcomes={showWelcomes} onOpen={(id) => { if (id) companion.selectAgent(id); setOpen(true); }}
onToggle={(id) => {
if (open && (!id || id === companion.selectedAgent?.teacher_id)) setOpen(false);
else { if (id) companion.selectAgent(id); setOpen(true); }
@@ -75,6 +75,26 @@ describe('grouped teacher introductions', () => {
}));
}
it('temporarily hides introductions without consuming them and keeps counting completed operation rounds', async () => {
api.catalog.mockResolvedValue({ items: agents });
const view = await mount();
await operation('one');
await operation('two');
const saved = { ...localStorage };
await act(async () => view.rerender(<Harness showWelcomes={false} />));
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '与方法老师聊天' })).toBeVisible();
expect({ ...localStorage }).toEqual(saved);
await act(async () => view.rerender(<Harness />));
expect(screen.getByTestId('teacher-welcomes').querySelectorAll('.teacher-welcome-card')).toHaveLength(3);
await act(async () => view.rerender(<Harness showWelcomes={false} />));
await operation('three');
await act(async () => view.rerender(<Harness />));
expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument();
expect(api.sendConversation).not.toHaveBeenCalled();
expect(api.checkIn).not.toHaveBeenCalled();
});
it('leaves introductions until the third completed operation round and preserves hover and unread receipts', async () => {
api.catalog.mockResolvedValue({ items: agents });
localStorage.setItem('makelore-teacher-presence:kid:seen', JSON.stringify(['keep-seen']));
@@ -715,6 +735,32 @@ describe('teacher presence and periodic guidance', () => {
expect(screen.queryByTestId('teacher-invitation')).not.toBeInTheDocument();
});
it('temporarily hides the legacy welcome without marking it dismissed', async () => {
api.config.mockResolvedValue({ enabled: true, definition: { ...definition, welcome_message: '欢迎一起创作' } });
const view = await mount();
expect(screen.getByTestId('teacher-invitation')).toHaveTextContent('欢迎一起创作');
const saved = { ...localStorage };
await act(async () => view.rerender(<Harness showWelcomes={false} />));
expect(screen.queryByTestId('teacher-presence')).not.toBeInTheDocument();
expect({ ...localStorage }).toEqual(saved);
await act(async () => view.rerender(<Harness />));
expect(screen.getByTestId('teacher-invitation')).toHaveTextContent('欢迎一起创作');
});
it('keeps real unread replies visible while welcomes are temporarily hidden', async () => {
api.config.mockResolvedValue({ enabled: true, definition: { ...definition, welcome_message: '欢迎一起创作' } });
api.list.mockResolvedValue({ items: [{ id: 'topic-project' }], lastSelectedTopicId: 'topic-project' });
api.read.mockResolvedValue(checkInTopic());
const view = await mount();
const saved = { ...localStorage };
await act(async () => view.rerender(<Harness showWelcomes={false} />));
expect(screen.getByTestId('teacher-invitation')).toHaveAttribute('data-bubble-kind', 'check-in');
expect(screen.getByTestId('teacher-invitation')).toHaveTextContent('排行榜');
expect(view.container.querySelector('.teacher-companion-unread')).toBeInTheDocument();
expect({ ...localStorage }).toEqual(saved);
expect(api.seenConversation).not.toHaveBeenCalled();
});
it('gives a real unread reply priority over the greeting and does not show the greeting after acknowledgment', async () => {
api.config.mockResolvedValue({ enabled: true, definition: { ...definition, welcome_message: '欢迎一起创作' } });
api.list.mockResolvedValue({ items: [{ id: 'topic-project' }], lastSelectedTopicId: 'topic-project' });