From 3a175101c979ee5f76bb9ff0bc7700649160c635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=A8=E9=B1=BC=E8=BE=A3=E6=A4=92?= Date: Tue, 29 Sep 2026 17:16:37 +0800 Subject: [PATCH] fix: show teacher introductions as standalone clickable bubbles --- .../tasks/20260929-community-welcome-c724e5a1.md | 12 +++++++++++- README.md | 2 +- src/pages/Chat/TeacherCompanion.tsx | 3 --- src/pages/Chat/classroom-workspace.css | 14 ++++++-------- src/pages/Chat/use-teacher-companion.ts | 2 -- tests/e2e/community-welcome.spec.ts | 4 ++-- tests/e2e/fixtures/community-welcome.tsx | 1 - tests/e2e/fixtures/teacher-preview-layout.tsx | 1 - tests/e2e/pi-coding-first-chat.spec.ts | 3 +++ tests/e2e/teacher-preview-layout.spec.ts | 16 +++++++++++++--- tests/unit/teacher-companion.test.tsx | 11 +++++++---- 11 files changed, 43 insertions(+), 26 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md b/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md index c1ac784a..62a865aa 100644 --- a/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md +++ b/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md @@ -23,7 +23,7 @@ - Warm welcome heading, existing composer and a compact inspiration invitation share the empty-chat layout. Both the invitation and persistent sidebar button open only https://square.nianxx.cn/#explore in the system browser on explicit click. - No public project feed, preview cards, community store or iframe remains in this feature. Existing unrelated Main routes are unchanged. The composer remains mounted across the first send; teacher introductions keep their prior three-successful-operation-round behavior and receive responsive clearance. - Preserved the concurrent live help-label overlay (task 20260929-teacher-help-label-83a2f67c): the two buttons and suggestions text remain 帮我整体看🧠. Ported only its three source edits and synchronized existing expectations; peer checkout untouched. -- Temporary Renderer now loads this checkout's index-Cgp0q7I7.js. Existing Main PID 62668 remains running; formal installation, cloud configuration, user history and remote branches unchanged. Kept this worktree for the live trial. +- Temporary Renderer now loads this checkout's index-BmLachPE.js. Existing Main PID 62668 remains running; formal installation, cloud configuration, user history and remote branches unchanged. Kept this worktree for the live trial. ## Verification @@ -99,3 +99,13 @@ All three steps completed. Earlier public-feed investigation was feasibility wor - Live Renderer index-Cgp0q7I7.js on unchanged Main PID 62668: background click sequence verified expanded/collapsed labels and aria state, dock removal/reopening, preserved current tab/teacher and both current draft values, unchanged operation history, all 13 stored drafts and 帮我整体看🧠. Current live teacher has no messages; nonempty teacher-history preservation is covered by the integration unit regression. Restored the initial closed dock. Opening the dock only reads existing data; no test prompt was submitted. - Native background timers initially delayed the click-verification script. Temporarily disabled webContents background throttling to finish the pending verification, then restored the original throttling setting. No window activation; focus monitor and live checks remained unfocused. Temporary inspector is closed after finalization. Evidence: trial teacher-dock-toggle-integrated-20260929/. - Welcome centering remains 0px error on both axes, invitation gap 12px and composer bottom gap 16px; auth identity and route preserved, no page alerts. Empty-work commit 0167d36 remains separate and was not live before this update. Keep this trial worktree; no main merge/push or cloud/prompt edits. + +## Follow-up: Standalone Clickable Teacher Bubbles + +- Same owned task resumed at 2922f35; ownership start/status verified. Existing project context and peer scope assessment remain applicable; no new dependency or semantic conflict. Planning Gate: Passed. +- Latest user asks to remove the introduction group's shared background, title and dismiss-all entry. Keep only independent avatar/speech-bubble cards, make the entire bubble open its corresponding teacher, and retain each separate close button. Preserve welcome receipts/three-operation-round lifecycle and current layout. +- Plan: remove shared chrome and unused dismiss-all callback, put all speech-bubble visuals on the native open button with the close control as a sibling, update click/dismiss/accessibility and native-preview geometry regressions, then refresh the trial without activation. + +- Completed: removed shared welcome background/shadow, heading and dismiss-all entry/callback. Each native button owns its entire colored speech-bubble surface (including the tail), while the close button is a separate sibling. Avatar opening, teacher-specific dismissal, persisted introduction receipts and three-operation-round auto-dismiss are retained. Keyboard focus is visible. +- Verification: 78 related unit tests, 21 headless welcome/teacher-preview layout cases, typecheck, lint:check (0 errors, 8 existing warnings), scoped changed-test ESLint, build:vite, native E2E --list (20 cases), ownership and whitespace checks passed. Tests cover padding clicks opening the correct teacher, close not opening consultation, Enter opening, many-teacher scroll, no shared chrome, individual dismissal persistence and the previous three-round lifecycle. Reviewed the desktop screenshot. Native headed fixture was not run to avoid focus changes. +- Trial refreshed in the background to index-BmLachPE.js on unchanged Main PID 62668. Live DOM confirms transparent outer container, no shadow/header/dismiss-all, preserved independent close button and hit-testing of bubble padding to the open button. Preserved the already-dismissed introduction receipts (one remaining Alice introduction), login identity, route and all 13 saved drafts. Welcome centering error remains 0px and bottom gap 16px. No alert or focus activation. Background capture was unavailable; visual evidence comes from the headless screenshot plus actual live DOM verification. Original background throttling restored and inspector closed after completion. Evidence: trial standalone-teacher-bubbles-20260929/. diff --git a/README.md b/README.md index ebb913b1..c7660c89 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p - 鼠标悬停或键盘聚焦顶部智能体入口时,以奶油色圆角卡片显示该智能体的发布介绍;离开、Escape 或点击入口后收起,没有介绍则不显示空卡片。咨询栏不再显示重复的头像、名称标题条,也不显示介绍及新话题、展开、关闭三个图标;老师身份保留在顶部入口及消息署名中。智能体生成的追加问题以淡蓝、奶油黄、淡紫卡片依次承载,正文 16px、行高 1.8,保留整卡点击发送与忙碌时禁用。回复下方不再显示重复的输入引导或省略早期消息条数;输入框提示为“和老师聊聊”。再次点击当前入口或在咨询栏按 Escape 可收起,切换其他入口仍直接打开对应讨论;拖动边缘调宽保留,已下发智能体仅保留只读旧记录入口。悬浮介绍不会创建话题、调用模型或确认未读消息,作品原生预览同时避让介绍卡和真实主动气泡。 - 主动发言采用紧贴小头像的短气泡;只有存在真实主动消息或运营欢迎语时才显示这组浮层,收起消息时头像和气泡一起消失,顶部入口仍保留。长消息在气泡中最多显示三行,点击接回原智能体对话查看全文。不提供“智能体偶尔来看看”开关或本地模拟巡看控制,既有自动跟进与真实消息接收逻辑保留。作品原生预览对整组气泡和头像测量避让,避免图片被原生页面遮住。 - 新的单会话入口停止旧版前端定时跟进派发;项目主动观察由独立功能衔接,不能重新创建可见话题。未下发目录的旧版入口仍保留原跟进合同:前端每 5 分钟请求一次智能体跟进;窗口隐藏、来源归档、主对话正在执行、智能体正在回复或学生正在智能体栏写草稿时延后。Main 再检查在线启停、来源归属、项目级冷却和已完成文本指纹,未变上下文按下述十五分钟冷却处理。跟进沿用运营模型、已发布 Skills 与当前智能体话题,生成符合所选配置的具体建议或引导;以主动智能体发言持久化,不伪造学生消息。模型调用沿用智能体计费规则。 -- 进入项目时,每位已下发智能体各显示一个浅色自我介绍气泡,优先使用云端介绍,缺省使用发布欢迎语;不调用模型、不声称已检查项目。气泡进入项目即依次出现,不点击时保留到操作 Agent 第 3 轮完整回复结束后自动收起,也可分别关闭或用“先自己试试”整组收起;按账号、项目、智能体保存计数及已介绍状态,刷新与操作对话切换延续进度。旧历史、老师咨询、工具调用、重试中间状态、失败/中止和其他项目不计入,新增智能体单独补上并从零计数。点击气泡或头像进入对应咨询,只记住这一位已介绍;咨询期间收起介绍组,关闭后其余未查看介绍仍可见,悬停头像仍可查看完整介绍。没有下发目录的旧入口保留原欢迎语展示兼容。真实的未读主动建议优先替换欢迎语,打开咨询不会自动将这些消息标为已读。主动发言在头像旁直接显示气泡,不抢焦点、不盖住作品;作品区按气泡与原生预览的实际交叠范围避让,已有工具栏高度计入计算,气泡位于独立智能体栏时不额外留白,窗口/分栏尺寸变化和收起气泡时实时归还空间;“和智能体聊聊”接回同一段咨询,“等会儿聊”收起并保留历史。已有未处理气泡时不堆叠新邀请;咨询已展开时仍显示真实未读主动消息气泡,直到学生主动点击查看或收起,期间的其他回复不会吞掉未读邀请。五分钟检查计时在刷新和项目切换后保留;智能体咨询里的新讨论也算进展,即使操作对话没有变化也会继续跟进;上下文完全未变时放缓到十五分钟一次,不永久停止。事件连接丢失时,每十五秒只读同步未完成回复,收到结果后停止轮询,不重复调用模型。失败如实提示,不以本地文案替代模型回复。已接受请求恢复使用原身份,退出账号和来源删除仍沿用 Main 中止边界。 +- 进入项目时,每位已下发智能体各显示一个浅色自我介绍气泡,外层透明,不显示公共底板、标题或整组关闭入口;整张气泡可点击,独立关闭按钮只收起该老师介绍,优先使用云端介绍,缺省使用发布欢迎语;不调用模型、不声称已检查项目。气泡进入项目即依次出现,不点击时保留到操作 Agent 第 3 轮完整回复结束后自动收起,也可分别关闭;按账号、项目、智能体保存计数及已介绍状态,刷新与操作对话切换延续进度。旧历史、老师咨询、工具调用、重试中间状态、失败/中止和其他项目不计入,新增智能体单独补上并从零计数。点击气泡或头像进入对应咨询,只记住这一位已介绍;咨询期间收起介绍组,关闭后其余未查看介绍仍可见,悬停头像仍可查看完整介绍。没有下发目录的旧入口保留原欢迎语展示兼容。真实的未读主动建议优先替换欢迎语,打开咨询不会自动将这些消息标为已读。主动发言在头像旁直接显示气泡,不抢焦点、不盖住作品;作品区按气泡与原生预览的实际交叠范围避让,已有工具栏高度计入计算,气泡位于独立智能体栏时不额外留白,窗口/分栏尺寸变化和收起气泡时实时归还空间;“和智能体聊聊”接回同一段咨询,“等会儿聊”收起并保留历史。已有未处理气泡时不堆叠新邀请;咨询已展开时仍显示真实未读主动消息气泡,直到学生主动点击查看或收起,期间的其他回复不会吞掉未读邀请。五分钟检查计时在刷新和项目切换后保留;智能体咨询里的新讨论也算进展,即使操作对话没有变化也会继续跟进;上下文完全未变时放缓到十五分钟一次,不永久停止。事件连接丢失时,每十五秒只读同步未完成回复,收到结果后停止轮询,不重复调用模型。失败如实提示,不以本地文案替代模型回复。已接受请求恢复使用原身份,退出账号和来源删除仍沿用 Main 中止边界。 - 智能体支持自由提问;输入框为空时,底部显示暖黄色小按钮“不知道问什么,你先帮我看看👀”,输入文字后收起,清空后重现,发送按钮保持原位。只有学生点击才调用模型,程序说明学生主动求助且尚未提出具体问题,智能体结合可用上下文按云端配置回应。点击快捷回复会主动开始讨论;回复下方不再附加固定的二次求助链接;失败或中断后,可点暖黄色小按钮“不知道问什么,你先帮我看看👀”重试。这些快捷求助保留已有输入草稿,网络结果不确定时复用请求身份,已确认终态的请求不重复执行;解析失败可重新求助,不用固定问题伪装模型结果。服务端下发的推荐问题只填入草稿,由用户确认发送。界面不提供独立示范页面、“记一下”、共识或自动待办,智能体的回复下不再展示“我去试一试”和“复制”按钮,学生通过已有的操作对话/作品入口继续创作。 - 老师咨询只展示老师正文和可点击的快捷回复卡片,学生也可在“和老师聊聊”输入框自由输入。点击卡片原样发送该回复,保留当前输入草稿;旧话题的数据不会恢复想法板、结构图、流程图或对照表及其操作。 - 客户端只提供入口事实、可用上下文和界面协议。讨论切入点、教学方式、是否提问、正文长度、前缀和表达风格由所选智能体的云端配置决定;不在“不知道问什么,你先帮我看看👀”、引导开口或后台检查时附加教学策略。当前新回复允许 0–3 条快捷回复,没有时用空数组,不要求凑满。 diff --git a/src/pages/Chat/TeacherCompanion.tsx b/src/pages/Chat/TeacherCompanion.tsx index b9d53171..bc6b7188 100644 --- a/src/pages/Chat/TeacherCompanion.tsx +++ b/src/pages/Chat/TeacherCompanion.tsx @@ -62,9 +62,6 @@ export function TeacherCompanion({ companion, open, onOpen, onToggle, bubbleRef, {refreshMessage} {!invitation && welcomes.length > 0 &&
-
来认识一下 - -
{welcomes.map((item, index) =>
diff --git a/src/pages/Chat/classroom-workspace.css b/src/pages/Chat/classroom-workspace.css index 267c32d6..0968d8b1 100644 --- a/src/pages/Chat/classroom-workspace.css +++ b/src/pages/Chat/classroom-workspace.css @@ -113,20 +113,18 @@ .teacher-companion-actions button:hover { text-decoration: underline; text-underline-offset: 4px; } .teacher-companion-speaker { flex-shrink: 0; width: 48px; height: 48px; margin-top: 5px; border-radius: 8px; } .teacher-companion-speaker img { width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; filter: drop-shadow(0 3px 1px #33485820); } -.teacher-welcome-presence { position: absolute; top: calc(100% + 12px); right: 0; z-index: 90; width: 340px; max-width: calc(100vw - 40px); padding: 6px; border-radius: 18px; background: #fffdf8; box-shadow: 0 8px 24px #30465f12; } -.teacher-welcome-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 8px 4px; color: #6a7463; font-size: 12px; } -.teacher-welcome-heading button { min-height: 32px; padding: 0 4px; color: #4d663c; border-radius: 8px; cursor: pointer; } -.teacher-welcome-heading button:hover { background: #eef1e8; } +.teacher-welcome-presence { position: absolute; top: calc(100% + 12px); right: 0; z-index: 90; width: 340px; max-width: calc(100vw - 40px); } .teacher-welcome-list { display: grid; gap: 10px; max-height: min(440px, calc(100vh - 200px)); overflow-y: auto; overscroll-behavior: contain; padding: 4px 6px 8px; scrollbar-width: thin; } .teacher-welcome-card { --welcome-fill: #fff3ce; --welcome-edge: #efddb0; display: flex; align-items: flex-start; gap: 10px; min-width: 0; animation: teacher-welcome-arrive 220ms ease-out backwards; animation-delay: var(--welcome-delay, 0ms); } .teacher-welcome-card[data-tone="1"] { --welcome-fill: #edf5ff; --welcome-edge: #d7e5f5; } .teacher-welcome-card[data-tone="2"] { --welcome-fill: #f3edfc; --welcome-edge: #e5d9f5; } .teacher-welcome-avatar { flex-shrink: 0; width: 42px; height: 44px; margin-top: 8px; padding: 2px; border: 1px solid var(--welcome-edge); border-radius: 14px; background: #fff; box-shadow: 0 3px 0 var(--welcome-edge); cursor: pointer; } .teacher-welcome-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 11px; } -.teacher-welcome-bubble { position: relative; flex: 1; min-width: 0; border: 1px solid var(--welcome-edge); border-radius: 17px 17px 17px 6px; background: var(--welcome-fill); box-shadow: 0 2px 0 var(--welcome-edge); } -.teacher-welcome-bubble::before { content: ''; position: absolute; top: 23px; left: -5px; width: 8px; height: 8px; transform: rotate(45deg); border-left: 1px solid var(--welcome-edge); border-bottom: 1px solid var(--welcome-edge); background: var(--welcome-fill); } -.teacher-welcome-open { display: flex; flex-direction: column; gap: 6px; width: 100%; min-width: 0; padding: 12px; border-radius: inherit; color: #34465c; text-align: left; cursor: pointer; } -.teacher-welcome-open:hover { background: #ffffff40; } +.teacher-welcome-bubble { position: relative; flex: 1; min-width: 0; } +.teacher-welcome-open::before { content: ''; position: absolute; top: 23px; left: -5px; width: 8px; height: 8px; transform: rotate(45deg); border-left: 1px solid var(--welcome-edge); border-bottom: 1px solid var(--welcome-edge); background: var(--welcome-fill); } +.teacher-welcome-open { position: relative; display: flex; flex-direction: column; gap: 6px; width: 100%; min-width: 0; padding: 12px; border: 1px solid var(--welcome-edge); border-radius: 17px 17px 17px 6px; background: var(--welcome-fill); box-shadow: 0 2px 0 var(--welcome-edge); color: #34465c; text-align: left; cursor: pointer; } +.teacher-welcome-open:hover { filter: brightness(1.015); } +.teacher-welcome-open:focus-visible, .teacher-welcome-dismiss:focus-visible, .teacher-welcome-avatar:focus-visible { outline: 2px solid #547ab0; outline-offset: 3px; } .teacher-welcome-name { padding-right: 20px; font-size: 13px; font-weight: 600; line-height: 19px; overflow-wrap: anywhere; } .teacher-welcome-text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; white-space: pre-wrap; font-size: 13px; line-height: 1.6; } .teacher-welcome-action { display: inline-flex; align-items: center; gap: 3px; color: #596b55; font-size: 11px; line-height: 18px; } diff --git a/src/pages/Chat/use-teacher-companion.ts b/src/pages/Chat/use-teacher-companion.ts index 1e83407a..d49d0426 100644 --- a/src/pages/Chat/use-teacher-companion.ts +++ b/src/pages/Chat/use-teacher-companion.ts @@ -358,7 +358,6 @@ export function useTeacherCompanion(options: Options) { || `我是${agent.definition.name},一起聊聊你的想法吧。`, tone: index % 3, }]) : []; - const dismissWelcomes = () => dismissIntroductions(welcomes.map(item => item.agentId)); const welcome = !visibleState.agents.length && topic?.role !== 'friend' && !options.teacherOpen && !visibleState.loading && visibleState.enabled && !visibleState.welcomeDismissed ? selectedDefinition?.welcome_message.trim() || null : null; return { @@ -375,7 +374,6 @@ export function useTeacherCompanion(options: Options) { welcome, welcomes, dismissWelcome, - dismissWelcomes, bubbleVisible: Boolean(invitation || welcome || welcomes.length), checking: last?.intent === 'check-in' && ['preparing', 'running'].includes(last.status), dismiss, diff --git a/tests/e2e/community-welcome.spec.ts b/tests/e2e/community-welcome.spec.ts index 1623678e..b2a99fd0 100644 --- a/tests/e2e/community-welcome.spec.ts +++ b/tests/e2e/community-welcome.spec.ts @@ -65,7 +65,7 @@ for (const width of [375, 768, 1280, 1470]) { 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(); + while (await teachers.count()) await teachers.getByRole('button', { name: /^收起.*的介绍$/ }).first().click(); await heading.scrollIntoViewIfNeeded(); const composer = page.getByTestId('coding-message-composer'); await expect(composer).toBeInViewport({ ratio: 1 }); @@ -125,7 +125,7 @@ for (const width of [1280, 1470]) { }).toBeLessThan(2); const inputBeforeDismiss = await composer.boundingBox(); const headingBeforeDismiss = await heading.boundingBox(); - await teachers.getByRole('button', { name: '先自己试试' }).click(); + while (await teachers.count()) await teachers.getByRole('button', { name: /^收起.*的介绍$/ }).first().click(); await expect(composer.getByRole('textbox')).toHaveValue('保留这份草稿'); expect(await composer.boundingBox()).toEqual(inputBeforeDismiss); expect(await heading.boundingBox()).toEqual(headingBeforeDismiss); diff --git a/tests/e2e/fixtures/community-welcome.tsx b/tests/e2e/fixtures/community-welcome.tsx index 71712223..baf21c41 100644 --- a/tests/e2e/fixtures/community-welcome.tsx +++ b/tests/e2e/fixtures/community-welcome.tsx @@ -25,7 +25,6 @@ export function Layout() { 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['companion']; return
{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 50ebed75..36edd026 100644 --- a/tests/e2e/fixtures/teacher-preview-layout.tsx +++ b/tests/e2e/fixtures/teacher-preview-layout.tsx @@ -33,7 +33,6 @@ export function Layout() { 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])]), - dismissWelcomes: () => setDismissed(agents.map(agent => agent.teacher_id)), 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 4259caa4..75f468bb 100644 --- a/tests/e2e/pi-coding-first-chat.spec.ts +++ b/tests/e2e/pi-coding-first-chat.spec.ts @@ -2221,6 +2221,9 @@ test('teacher introductions collapse after three operation replies and remain av await page.evaluate(() => { window.location.hash = '/chat'; }); const introductions = page.getByTestId('teacher-welcomes'); await expect(introductions.locator('.teacher-welcome-card')).toHaveCount(2); + await expect(introductions).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await expect(introductions.getByText('来认识一下')).toHaveCount(0); + await expect(introductions.getByRole('button', { name: '先自己试试' })).toHaveCount(0); const startedAt = await page.evaluate(() => Date.now()); for (let round = 1; round <= 3; round++) { for (const status of ['running', 'idle'] as const) { diff --git a/tests/e2e/teacher-preview-layout.spec.ts b/tests/e2e/teacher-preview-layout.spec.ts index a7850600..5b98549e 100644 --- a/tests/e2e/teacher-preview-layout.spec.ts +++ b/tests/e2e/teacher-preview-layout.spec.ts @@ -41,6 +41,10 @@ for (const width of [375, 768, 1280]) { await page.goto(baseUrl + '?agents=3&welcomes'); const group = page.getByTestId('teacher-welcomes'); 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'); + await expect(group.getByText('来认识一下')).toHaveCount(0); + await expect(group.getByRole('button', { name: '先自己试试' })).toHaveCount(0); await expect(group).toBeInViewport({ ratio: 1 }); for (const card of await group.locator('.teacher-welcome-card').all()) { await expect(card).toBeInViewport({ ratio: 1 }); @@ -53,12 +57,13 @@ for (const width of [375, 768, 1280]) { 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: '和智能体3聊聊' }).click(); + await expect(page.locator('#coding-consultation-dock')).toHaveCount(0); + 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); - await group.getByRole('button', { name: '先自己试试' }).click(); + while (await group.count()) await group.getByRole('button', { name: /^收起.*的介绍$/ }).first().click(); await expect(group).toHaveCount(0); }); } @@ -75,7 +80,12 @@ test('many teacher introductions remain scrollable and honor reduced motion', as await last.scrollIntoViewIfNeeded(); await expect(last).toBeInViewport({ ratio: 1 }); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(520); - await group.getByRole('button', { name: '先自己试试' }).click(); + await last.focus(); + 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 989f8347..a4bcb953 100644 --- a/tests/unit/teacher-companion.test.tsx +++ b/tests/unit/teacher-companion.test.tsx @@ -193,10 +193,13 @@ describe('individual teacher introductions', () => { api.catalog.mockResolvedValue({ items: agents }); await mount(); const group = screen.getByTestId('teacher-welcomes'); + expect(within(group).queryByText('来认识一下')).not.toBeInTheDocument(); + 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(); + expect(screen.queryByTestId('teacher-chat-panel')).not.toBeInTheDocument(); expect(within(group).getByText('方法老师的自我介绍')).toBeVisible(); expect(within(group).getByText('故事老师的自我介绍')).toBeVisible(); await advance(); @@ -217,7 +220,7 @@ describe('individual teacher introductions', () => { expect(screen.getByRole('button', { name: '收起体验老师的介绍' })).toBeVisible(); await act(async () => next.rerender()); expect(screen.getByRole('button', { name: '收起方法老师的介绍' })).toBeVisible(); - await act(async () => fireEvent.click(screen.getByRole('button', { name: '先自己试试' }))); + for (const agent of agents) await act(async () => fireEvent.click(screen.getByRole('button', { name: `收起${agent.definition.name}的介绍` }))); expect(screen.queryByTestId('teacher-welcomes')).not.toBeInTheDocument(); await act(async () => next.rerender()); expect(screen.queryByRole('button', { name: '收起方法老师的介绍' })).not.toBeInTheDocument(); @@ -230,7 +233,7 @@ describe('individual teacher introductions', () => { it('opens the clicked teacher and consumes only their introduction, preserving the others for later', async () => { api.catalog.mockResolvedValue({ items: agents }); await mount(); - await act(async () => fireEvent.click(screen.getByRole('button', { name: '和故事老师聊聊' }))); + await act(async () => fireEvent.click(within(screen.getByTestId('teacher-welcomes')).getByText('故事老师的自我介绍'))); expect(screen.getByRole('button', { name: '与故事老师聊天' })).toHaveAttribute('aria-pressed', 'true'); expect(screen.getByTestId('teacher-chat-panel')).toHaveTextContent('故事老师的聊天欢迎语'); expect(screen.getByTestId('teacher-chat-panel')).not.toHaveTextContent('故事老师的自我介绍'); @@ -243,11 +246,11 @@ describe('individual teacher introductions', () => { expect(api.sendConversation).not.toHaveBeenCalled(); }); - it('introduces only newly delivered teachers after dismiss-all, without resetting seen replies or existing receipts on refresh', async () => { + it('introduces only newly delivered teachers after individually dismissing every introduction, without resetting seen replies or existing receipts on refresh', async () => { localStorage.setItem('makelore-teacher-presence:kid:seen', JSON.stringify(['keep-seen'])); api.catalog.mockResolvedValue({ items: agents }); await mount(); - await act(async () => fireEvent.click(screen.getByRole('button', { name: '先自己试试' }))); + for (const agent of agents) await act(async () => fireEvent.click(screen.getByRole('button', { name: `收起${agent.definition.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: '刷新智能体' })));