@lobehub/lobehub 2.0.0-next.187 → 2.0.0-next.189

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/e2e/CLAUDE.md +109 -2
  4. package/e2e/docs/llm-mock.md +68 -0
  5. package/e2e/docs/local-setup.md +354 -0
  6. package/e2e/docs/testing-tips.md +94 -0
  7. package/e2e/src/features/journeys/agent/agent-conversation.feature +0 -32
  8. package/e2e/src/mocks/llm/index.ts +6 -6
  9. package/e2e/src/steps/agent/conversation.steps.ts +3 -471
  10. package/locales/ar/models.json +89 -5
  11. package/locales/ar/plugin.json +5 -0
  12. package/locales/ar/providers.json +1 -0
  13. package/locales/bg-BG/models.json +68 -0
  14. package/locales/bg-BG/plugin.json +5 -0
  15. package/locales/bg-BG/providers.json +1 -0
  16. package/locales/de-DE/models.json +85 -0
  17. package/locales/de-DE/plugin.json +5 -0
  18. package/locales/de-DE/providers.json +1 -0
  19. package/locales/en-US/models.json +11 -10
  20. package/locales/en-US/plugin.json +5 -0
  21. package/locales/en-US/providers.json +1 -0
  22. package/locales/es-ES/models.json +72 -0
  23. package/locales/es-ES/plugin.json +5 -0
  24. package/locales/es-ES/providers.json +1 -0
  25. package/locales/fa-IR/models.json +86 -0
  26. package/locales/fa-IR/plugin.json +5 -0
  27. package/locales/fa-IR/providers.json +1 -0
  28. package/locales/fr-FR/models.json +49 -0
  29. package/locales/fr-FR/plugin.json +5 -0
  30. package/locales/fr-FR/providers.json +1 -0
  31. package/locales/it-IT/models.json +82 -0
  32. package/locales/it-IT/plugin.json +5 -0
  33. package/locales/it-IT/providers.json +1 -0
  34. package/locales/ja-JP/models.json +42 -5
  35. package/locales/ja-JP/plugin.json +5 -0
  36. package/locales/ja-JP/providers.json +1 -0
  37. package/locales/ko-KR/models.json +54 -0
  38. package/locales/ko-KR/plugin.json +5 -0
  39. package/locales/ko-KR/providers.json +1 -0
  40. package/locales/nl-NL/models.json +12 -1
  41. package/locales/nl-NL/plugin.json +5 -0
  42. package/locales/nl-NL/providers.json +1 -0
  43. package/locales/pl-PL/models.json +46 -0
  44. package/locales/pl-PL/plugin.json +5 -0
  45. package/locales/pl-PL/providers.json +1 -0
  46. package/locales/pt-BR/models.json +59 -0
  47. package/locales/pt-BR/plugin.json +5 -0
  48. package/locales/pt-BR/providers.json +1 -0
  49. package/locales/ru-RU/models.json +85 -0
  50. package/locales/ru-RU/plugin.json +5 -0
  51. package/locales/ru-RU/providers.json +1 -0
  52. package/locales/tr-TR/models.json +81 -0
  53. package/locales/tr-TR/plugin.json +5 -0
  54. package/locales/tr-TR/providers.json +1 -0
  55. package/locales/vi-VN/models.json +54 -0
  56. package/locales/vi-VN/plugin.json +5 -0
  57. package/locales/vi-VN/providers.json +1 -0
  58. package/locales/zh-CN/models.json +42 -5
  59. package/locales/zh-CN/plugin.json +5 -0
  60. package/locales/zh-CN/providers.json +1 -0
  61. package/locales/zh-TW/models.json +85 -0
  62. package/locales/zh-TW/plugin.json +5 -0
  63. package/locales/zh-TW/providers.json +1 -0
  64. package/package.json +2 -2
  65. package/packages/builtin-tool-gtd/src/manifest.ts +13 -8
  66. package/packages/builtin-tool-gtd/src/systemRole.ts +54 -19
  67. package/packages/builtin-tool-knowledge-base/package.json +1 -0
  68. package/packages/builtin-tool-knowledge-base/src/client/Inspector/ReadKnowledge/index.tsx +97 -0
  69. package/packages/builtin-tool-knowledge-base/src/client/Inspector/SearchKnowledgeBase/index.tsx +75 -0
  70. package/packages/builtin-tool-knowledge-base/src/client/Inspector/index.ts +11 -0
  71. package/packages/builtin-tool-knowledge-base/src/client/Render/ReadKnowledge/FileCard.tsx +12 -12
  72. package/packages/builtin-tool-knowledge-base/src/client/Render/ReadKnowledge/index.tsx +16 -25
  73. package/packages/builtin-tool-knowledge-base/src/client/Render/SearchKnowledgeBase/Item/index.tsx +21 -47
  74. package/packages/builtin-tool-knowledge-base/src/client/Render/SearchKnowledgeBase/index.tsx +19 -31
  75. package/packages/builtin-tool-knowledge-base/src/client/Render/index.ts +0 -5
  76. package/packages/builtin-tool-knowledge-base/src/client/index.ts +5 -1
  77. package/packages/builtin-tool-knowledge-base/src/executor/index.ts +119 -0
  78. package/packages/builtin-tool-local-system/package.json +1 -0
  79. package/packages/builtin-tool-local-system/src/client/Inspector/EditLocalFile/index.tsx +44 -29
  80. package/packages/builtin-tool-local-system/src/client/Inspector/GrepContent/index.tsx +20 -18
  81. package/packages/builtin-tool-local-system/src/client/Inspector/ListLocalFiles/index.tsx +76 -0
  82. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +8 -32
  83. package/packages/builtin-tool-local-system/src/client/Inspector/RenameLocalFile/index.tsx +62 -0
  84. package/packages/builtin-tool-local-system/src/client/Inspector/SearchLocalFiles/index.tsx +17 -11
  85. package/packages/builtin-tool-local-system/src/client/Inspector/WriteLocalFile/index.tsx +61 -0
  86. package/packages/builtin-tool-local-system/src/client/Inspector/index.ts +6 -0
  87. package/packages/builtin-tool-local-system/src/client/Render/EditLocalFile/index.tsx +6 -1
  88. package/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/SearchView.tsx +19 -31
  89. package/packages/builtin-tool-local-system/src/client/Render/SearchFiles/SearchQuery/index.tsx +2 -42
  90. package/packages/builtin-tool-local-system/src/client/Render/index.ts +0 -2
  91. package/packages/builtin-tool-local-system/src/client/components/FilePathDisplay.tsx +56 -0
  92. package/packages/builtin-tool-local-system/src/client/components/index.ts +2 -0
  93. package/packages/builtin-tool-local-system/src/executor/index.ts +435 -0
  94. package/packages/builtin-tool-web-browsing/src/client/Inspector/Search/index.tsx +32 -5
  95. package/packages/model-runtime/src/core/contextBuilders/google.test.ts +84 -0
  96. package/packages/model-runtime/src/core/contextBuilders/google.ts +37 -1
  97. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Actions.tsx +4 -13
  98. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/index.tsx +23 -29
  99. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/useDropdownMenu.tsx +3 -3
  100. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +4 -13
  101. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/index.tsx +10 -18
  102. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/useDropdownMenu.tsx +3 -3
  103. package/src/app/[variants]/(main)/community/(detail)/assistant/features/Sidebar/ActionButton/AddAgent.tsx +47 -27
  104. package/src/app/[variants]/(main)/community/(detail)/user/features/UserAgentCard.tsx +4 -3
  105. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/Actions.tsx +4 -13
  106. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +23 -29
  107. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/useDropdownMenu.tsx +3 -3
  108. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +4 -13
  109. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/index.tsx +10 -18
  110. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/useDropdownMenu.tsx +3 -3
  111. package/src/app/[variants]/(main)/group/profile/features/AgentBuilder/TopicSelector.tsx +18 -20
  112. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +19 -25
  113. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +21 -26
  114. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/Actions.tsx +4 -13
  115. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +3 -3
  116. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Actions.tsx +4 -13
  117. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +8 -15
  118. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +3 -3
  119. package/src/app/[variants]/(main)/home/_layout/Header/components/AddButton.tsx +3 -4
  120. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Actions.tsx +4 -13
  121. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +13 -20
  122. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +3 -3
  123. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/Actions.tsx +4 -13
  124. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/index.tsx +16 -23
  125. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/useDropdownMenu.tsx +3 -3
  126. package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +4 -6
  127. package/src/features/AgentBuilder/TopicSelector.tsx +18 -17
  128. package/src/features/Conversation/ChatItem/style.ts +7 -0
  129. package/src/features/Conversation/Messages/Assistant/Actions/Error.tsx +1 -3
  130. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +37 -16
  131. package/src/features/Conversation/Messages/AssistantGroup/Actions/index.tsx +36 -17
  132. package/src/features/Conversation/Messages/Supervisor/Actions/index.tsx +36 -17
  133. package/src/features/Conversation/Messages/Task/Actions/Error.tsx +1 -3
  134. package/src/features/Conversation/Messages/Task/Actions/index.tsx +31 -15
  135. package/src/features/Conversation/Messages/User/Actions/index.tsx +1 -1
  136. package/src/features/Conversation/Messages/index.tsx +8 -59
  137. package/src/features/Conversation/components/ShareMessageModal/index.tsx +1 -1
  138. package/src/features/Conversation/hooks/useChatItemContextMenu.tsx +313 -83
  139. package/src/features/NavPanel/components/NavItem.tsx +33 -3
  140. package/src/features/PageEditor/Copilot/TopicSelector/Actions.tsx +6 -14
  141. package/src/features/PageEditor/Copilot/TopicSelector/TopicItem.tsx +1 -0
  142. package/src/features/PageEditor/Copilot/TopicSelector/useDropdownMenu.tsx +6 -3
  143. package/src/features/ResourceManager/components/Explorer/ItemDropdown/DropdownMenu.tsx +12 -35
  144. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +4 -8
  145. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +162 -160
  146. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +16 -8
  147. package/src/features/ResourceManager/components/Explorer/ToolBar/ActionIconWithChevron.tsx +4 -3
  148. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +6 -12
  149. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +8 -8
  150. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +8 -11
  151. package/src/features/ResourceManager/components/Tree/index.tsx +121 -122
  152. package/src/helpers/toolEngineering/index.ts +1 -1
  153. package/src/layout/GlobalProvider/index.tsx +5 -2
  154. package/src/locales/default/plugin.ts +6 -0
  155. package/src/server/modules/Mecha/AgentToolsEngine/__tests__/index.test.ts +1 -1
  156. package/src/server/modules/Mecha/AgentToolsEngine/index.ts +1 -1
  157. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -11
  158. package/src/store/tool/slices/builtin/executors/index.ts +4 -0
  159. package/src/styles/global.ts +6 -0
  160. package/src/styles/text.ts +1 -1
  161. package/src/tools/executionRuntimes.ts +3 -8
  162. package/src/tools/identifiers.ts +1 -1
  163. package/src/tools/index.ts +1 -1
  164. package/src/tools/inspectors.ts +5 -0
  165. package/src/tools/renders.ts +6 -12
  166. package/packages/builtin-tool-local-system/src/client/Render/RenameLocalFile/index.tsx +0 -37
  167. package/src/features/Conversation/components/ContextMenu.tsx +0 -418
  168. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +0 -201
  169. package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +0 -163
  170. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +0 -241
  171. package/src/tools/knowledge-base/ExecutionRuntime/index.ts +0 -25
  172. package/src/tools/knowledge-base/Render/ReadKnowledge/index.tsx +0 -29
  173. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/index.tsx +0 -29
  174. package/src/tools/knowledge-base/Render/index.ts +0 -7
  175. package/src/tools/knowledge-base/index.ts +0 -12
  176. package/src/tools/local-system/ExecutionRuntime/index.ts +0 -9
  177. package/src/tools/local-system/systemRole.ts +0 -1
@@ -0,0 +1,94 @@
1
+ # 测试技巧
2
+
3
+ ## 页面元素定位
4
+
5
+ ### 富文本编辑器 (contenteditable) 输入
6
+
7
+ LobeHub 使用 `@lobehub/editor` 作为聊天输入框,是一个 contenteditable 的富文本编辑器。
8
+
9
+ **关键点**:
10
+
11
+ 1. 不能直接用 `locator.fill()` - 对 contenteditable 不生效
12
+ 2. 需要先 click 容器让编辑器获得焦点
13
+ 3. 使用 `keyboard.type()` 输入文本
14
+
15
+ ```typescript
16
+ // 正确的输入方式
17
+ await chatInputContainer.click();
18
+ await this.page.waitForTimeout(500); // 等待焦点
19
+ await this.page.keyboard.type(message, { delay: 30 });
20
+ await this.page.keyboard.press('Enter'); // 发送
21
+ ```
22
+
23
+ ### 添加 data-testid
24
+
25
+ 为了更可靠的元素定位,可以在组件上添加 `data-testid`:
26
+
27
+ ```tsx
28
+ // src/features/ChatInput/Desktop/index.tsx
29
+ <ChatInput
30
+ data-testid="chat-input"
31
+ ...
32
+ />
33
+ ```
34
+
35
+ ## 调试技巧
36
+
37
+ ### 添加步骤日志
38
+
39
+ 在每个关键步骤添加 console.log,帮助定位问题:
40
+
41
+ ```typescript
42
+ Given('用户进入页面', async function (this: CustomWorld) {
43
+ console.log(' 📍 Step: 导航到首页...');
44
+ await this.page.goto('/');
45
+
46
+ console.log(' 📍 Step: 查找元素...');
47
+ const element = this.page.locator('...');
48
+
49
+ console.log(' ✅ 步骤完成');
50
+ });
51
+ ```
52
+
53
+ ### 查看失败截图
54
+
55
+ 测试失败时会自动保存截图到 `e2e/screenshots/` 目录。
56
+
57
+ ### 非 headless 模式
58
+
59
+ 设置 `HEADLESS=false` 可以看到浏览器操作:
60
+
61
+ ```bash
62
+ HEADLESS=false pnpm exec cucumber-js --config cucumber.config.js --tags "@smoke"
63
+ ```
64
+
65
+ ## 常见问题
66
+
67
+ ### 测试超时 (function timed out)
68
+
69
+ **原因**: 元素定位失败或等待时间不足
70
+
71
+ **解决**:
72
+
73
+ - 检查选择器是否正确
74
+ - 增加 timeout 参数
75
+ - 添加显式等待 `waitForTimeout()`
76
+
77
+ ### strict mode violation (多个元素匹配)
78
+
79
+ **原因**: 选择器匹配到多个元素(如 desktop/mobile 双组件)
80
+
81
+ **解决**:
82
+
83
+ - 使用 `.first()` 或 `.nth(n)`
84
+ - 使用 `boundingBox()` 过滤可见元素
85
+
86
+ ### 输入框内容为空
87
+
88
+ **原因**: contenteditable 编辑器的特殊性
89
+
90
+ **解决**:
91
+
92
+ - 先 click 容器确保焦点
93
+ - 使用 `keyboard.type()` 而非 `fill()`
94
+ - 添加适当的等待时间
@@ -11,35 +11,3 @@ Feature: Agent 对话用户体验链路
11
11
  When 用户发送消息 "hello"
12
12
  Then 用户应该收到助手的回复
13
13
  And 回复内容应该可见
14
-
15
- @AGENT-CHAT-002 @P0
16
- Scenario: 多轮对话保持上下文
17
- Given 用户进入 Lobe AI 对话页面
18
- When 用户发送消息 "我的名字是小明"
19
- Then 用户应该收到助手的回复
20
- When 用户发送消息 "我刚才说我的名字是什么?"
21
- Then 用户应该收到助手的回复
22
- And 回复内容应该包含 "小明"
23
-
24
- @AGENT-CHAT-003 @P0
25
- Scenario: 清空对话历史
26
- Given 用户进入 Lobe AI 对话页面
27
- And 用户已发送消息 "hello"
28
- When 用户点击清空对话按钮
29
- Then 对话历史应该被清空
30
- And 页面应该显示欢迎界面
31
-
32
- @AGENT-CHAT-004 @P0
33
- Scenario: 重新生成回复
34
- Given 用户进入 Lobe AI 对话页面
35
- And 用户已发送消息 "hello"
36
- When 用户点击重新生成按钮
37
- Then 用户应该收到新的助手回复
38
-
39
- @AGENT-CHAT-005 @P0
40
- Scenario: 停止生成回复
41
- Given 用户进入 Lobe AI 对话页面
42
- When 用户发送消息 "写一篇很长的文章"
43
- And 用户在生成过程中点击停止按钮
44
- Then 回复应该停止生成
45
- And 已生成的内容应该保留
@@ -231,14 +231,14 @@ export const presetResponses = {
231
231
  codeHelp: 'I can help you with coding! Please share the code you would like me to review.',
232
232
  error: 'I apologize, but I encountered an error processing your request.',
233
233
  greeting: 'Hello! I am Lobe AI, your AI assistant. How can I help you today?',
234
-
234
+
235
235
  // Long response for stop generation test
236
- longArticle:
236
+ longArticle:
237
237
  '这是一篇很长的文章。第一段:人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。第二段:人工智能研究的主要目标包括推理、知识、规划、学习、自然语言处理、感知和移动与操控物体的能力。第三段:目前,人工智能已经在许多领域取得了重大突破,包括图像识别、语音识别、自然语言处理等。',
238
-
239
- // Multi-turn conversation responses
240
- nameIntro: '好的,我记住了,你的名字是小明。很高兴认识你,小明!有什么我可以帮助你的吗?',
241
-
238
+
239
+ // Multi-turn conversation responses
240
+ nameIntro: '好的,我记住了,你的名字是小明。很高兴认识你,小明!有什么我可以帮助你的吗?',
241
+
242
242
  nameRecall: '你刚才说你的名字是小明。',
243
243
  // Regenerate response
244
244
  regenerated: '这是重新生成的回复内容。我是 Lobe AI,很高兴为你服务!',
@@ -22,31 +22,19 @@ Given('用户已登录系统', async function (this: CustomWorld) {
22
22
 
23
23
  Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
24
24
  console.log(' 📍 Step: 设置 LLM mock...');
25
- // Setup LLM mock before navigation with all preset responses
25
+ // Setup LLM mock before navigation
26
26
  llmMockManager.setResponse('hello', presetResponses.greeting);
27
- llmMockManager.setResponse('hello world', presetResponses.greeting);
28
- llmMockManager.setResponse('我的名字是小明', presetResponses.nameIntro);
29
- llmMockManager.setResponse('我刚才说我的名字是什么?', presetResponses.nameRecall);
30
- llmMockManager.setResponse('我刚才说我的名字是什么', presetResponses.nameRecall);
31
- llmMockManager.setResponse('写一篇很长的文章', presetResponses.longArticle);
32
- llmMockManager.setResponse('测试对话内容', '这是测试对话的回复内容。');
33
- llmMockManager.setResponse('第一个对话', '这是第一个对话的回复。');
34
- llmMockManager.setResponse('第二个对话', '这是第二个对话的回复。');
35
27
  await llmMockManager.setup(this.page);
36
28
 
37
29
  console.log(' 📍 Step: 导航到首页...');
38
30
  // Navigate to home page first
39
31
  await this.page.goto('/');
40
- await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
41
-
42
- console.log(' 📍 Step: 等待助手列表加载...');
43
- // Wait for skeletons to disappear (assistant list to load)
44
- await this.page.waitForTimeout(2000);
32
+ await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
45
33
 
46
34
  console.log(' 📍 Step: 查找 Lobe AI...');
47
35
  // Find and click on "Lobe AI" agent in the sidebar/home
48
36
  const lobeAIAgent = this.page.locator('text=Lobe AI').first();
49
- await expect(lobeAIAgent).toBeVisible({ timeout: 20_000 });
37
+ await expect(lobeAIAgent).toBeVisible({ timeout: 10_000 });
50
38
 
51
39
  console.log(' 📍 Step: 点击 Lobe AI...');
52
40
  await lobeAIAgent.click();
@@ -163,459 +151,3 @@ Then('回复内容应该可见', async function (this: CustomWorld) {
163
151
 
164
152
  console.log(` ✅ Assistant replied: "${text?.slice(0, 50)}..."`);
165
153
  });
166
-
167
- Then('回复内容应该包含 {string}', async function (this: CustomWorld, expectedText: string) {
168
- console.log(` 📍 Step: 验证回复包含 "${expectedText}"...`);
169
-
170
- // Get the last assistant message
171
- const assistantMessages = this.page.locator(
172
- '[data-role="assistant"], [class*="assistant"], [class*="message"]',
173
- );
174
- const lastMessage = assistantMessages.last();
175
-
176
- await expect(lastMessage).toBeVisible({ timeout: 10_000 });
177
-
178
- // Get text content
179
- const text = await lastMessage.textContent();
180
- console.log(` 📍 回复内容: "${text?.slice(0, 100)}..."`);
181
-
182
- expect(text).toContain(expectedText);
183
- console.log(` ✅ 回复包含 "${expectedText}"`);
184
- });
185
-
186
- // ============================================
187
- // Given Steps for Advanced Scenarios
188
- // ============================================
189
-
190
- Given('用户已发送消息 {string}', async function (this: CustomWorld, message: string) {
191
- console.log(` 📍 Step: 发送预备消息 "${message}"...`);
192
-
193
- // Find and click the chat input
194
- const chatInputs = this.page.locator('[data-testid="chat-input"]');
195
- const count = await chatInputs.count();
196
-
197
- let chatInputContainer = chatInputs.first();
198
- for (let i = 0; i < count; i++) {
199
- const elem = chatInputs.nth(i);
200
- const box = await elem.boundingBox();
201
- if (box && box.width > 0 && box.height > 0) {
202
- chatInputContainer = elem;
203
- break;
204
- }
205
- }
206
-
207
- await chatInputContainer.click();
208
- await this.page.waitForTimeout(300);
209
- await this.page.keyboard.type(message, { delay: 30 });
210
- await this.page.keyboard.press('Enter');
211
-
212
- // Wait for response
213
- await this.page.waitForTimeout(2000);
214
-
215
- // Verify we got a response
216
- const assistantMessage = this.page
217
- .locator('[data-role="assistant"], [class*="assistant"], [class*="message"]')
218
- .last();
219
- await expect(assistantMessage).toBeVisible({ timeout: 15_000 });
220
-
221
- console.log(` ✅ 预备消息已发送并收到回复`);
222
- });
223
-
224
- // ============================================
225
- // When Steps for Advanced Scenarios
226
- // ============================================
227
-
228
- When('用户点击清空对话按钮', async function (this: CustomWorld) {
229
- console.log(' 📍 Step: 查找清空对话按钮...');
230
-
231
- // The clear button uses an Eraser icon from lucide-react and is visible in the ActionBar
232
- // The ActionBar is in the footer of ChatInput component
233
- // We need to find all buttons on the page and look for the one with the Eraser icon
234
-
235
- // Look for ALL buttons on the page that have SVG icons
236
- // This is a broader search to capture all action bar buttons
237
- const allPageButtons = this.page.locator('button:has(svg)');
238
- const pageButtonCount = await allPageButtons.count();
239
- console.log(` 📍 Found ${pageButtonCount} buttons with SVG on page`);
240
-
241
- let clearButtonFound = false;
242
-
243
- // First try to find by lucide class name for eraser
244
- const eraserByClass = this.page.locator('svg.lucide-eraser').locator('..');
245
- if ((await eraserByClass.count()) > 0) {
246
- console.log(' 📍 Found eraser button by class name');
247
- await eraserByClass.first().click();
248
- clearButtonFound = true;
249
- }
250
-
251
- // If not found by class, iterate through buttons and check SVG path data
252
- if (!clearButtonFound) {
253
- for (let i = 0; i < pageButtonCount; i++) {
254
- const btn = allPageButtons.nth(i);
255
- const box = await btn.boundingBox();
256
- if (!box || box.width === 0 || box.height === 0) continue;
257
-
258
- // Check SVG class
259
- const svgInButton = btn.locator('svg').first();
260
- const svgClass = await svgInButton.getAttribute('class').catch(() => '');
261
-
262
- if (svgClass?.includes('eraser') || svgClass?.toLowerCase().includes('eraser')) {
263
- console.log(` 📍 Found eraser by class at button ${i}: ${svgClass}`);
264
- await btn.click();
265
- clearButtonFound = true;
266
- break;
267
- }
268
-
269
- // Check path data - the Eraser icon has specific path
270
- const pathElement = btn.locator('svg path').first();
271
- const pathD = await pathElement.getAttribute('d').catch(() => '');
272
-
273
- // Eraser icon path data pattern from lucide-react
274
- // Check for multiple possible patterns
275
- if (
276
- pathD?.includes('m7 21') ||
277
- pathD?.includes('M7 21') ||
278
- pathD?.includes('7 21-4.3-4.3') ||
279
- pathD?.includes('21l-4.3')
280
- ) {
281
- console.log(` 📍 Found eraser button by path at index ${i}`);
282
- await btn.click();
283
- clearButtonFound = true;
284
- break;
285
- }
286
- }
287
- }
288
-
289
- // Fallback: hover over buttons in bottom area and find one with "清空" tooltip
290
- if (!clearButtonFound) {
291
- console.log(' 📍 Trying hover approach to find button with 清空 tooltip...');
292
-
293
- // Focus on buttons in the bottom 200px of viewport
294
- for (let i = 0; i < pageButtonCount; i++) {
295
- const btn = allPageButtons.nth(i);
296
- const box = await btn.boundingBox();
297
-
298
- // Only check buttons in the bottom area (action bar)
299
- if (!box || box.width === 0 || box.height === 0) continue;
300
- if (box.y < 500) continue; // Skip buttons not in bottom area
301
-
302
- // Hover to trigger tooltip
303
- await btn.hover();
304
- await this.page.waitForTimeout(300);
305
-
306
- // Check if tooltip with "清空" appeared
307
- const tooltip = this.page.locator('.ant-tooltip:has-text("清空")');
308
- if ((await tooltip.count()) > 0) {
309
- console.log(` 📍 Found clear button by tooltip at index ${i}`);
310
- await btn.click();
311
- clearButtonFound = true;
312
- break;
313
- }
314
- }
315
- }
316
-
317
- // Last resort: click buttons in bottom area and check for Popconfirm
318
- if (!clearButtonFound) {
319
- console.log(' 📍 Last resort: clicking bottom buttons to find Popconfirm...');
320
- for (let i = 0; i < pageButtonCount; i++) {
321
- const btn = allPageButtons.nth(i);
322
- const box = await btn.boundingBox();
323
- if (!box || box.width === 0 || box.height === 0) continue;
324
- if (box.y < 500) continue; // Focus on bottom area
325
-
326
- await btn.click();
327
- await this.page.waitForTimeout(300);
328
-
329
- // Check if Popconfirm appeared
330
- const popconfirm = this.page.locator(
331
- '.ant-popconfirm, .ant-popover:has(button.ant-btn-dangerous)',
332
- );
333
- if ((await popconfirm.count()) > 0 && (await popconfirm.first().isVisible())) {
334
- console.log(` 📍 Found Popconfirm after clicking button ${i}`);
335
- clearButtonFound = true;
336
- break;
337
- }
338
-
339
- // Press Escape to dismiss any popover
340
- await this.page.keyboard.press('Escape');
341
- await this.page.waitForTimeout(100);
342
- }
343
- }
344
-
345
- if (!clearButtonFound) {
346
- throw new Error('Could not find the clear button');
347
- }
348
-
349
- // Wait for Popconfirm to appear and click the confirm button
350
- console.log(' 📍 Step: 确认清空...');
351
- await this.page.waitForTimeout(500);
352
-
353
- // The Popconfirm has a danger primary button for confirmation
354
- const confirmButton = this.page.locator(
355
- '.ant-popconfirm button.ant-btn-primary, .ant-popover button.ant-btn-primary',
356
- );
357
- await expect(confirmButton).toBeVisible({ timeout: 5000 });
358
- await confirmButton.click();
359
-
360
- await this.page.waitForTimeout(500);
361
- console.log(' ✅ 已点击清空对话按钮');
362
- });
363
-
364
- When('用户点击重新生成按钮', async function (this: CustomWorld) {
365
- console.log(' 📍 Step: 查找重新生成按钮...');
366
-
367
- // The regenerate action is in the ActionIconGroup menu for assistant messages
368
- // ActionIconGroup renders ActionIcon buttons and a "more" button (MoreHorizontal icon)
369
- // The "more" button opens a dropdown menu with "重新生成" option
370
- // Action buttons only appear on hover over the message
371
-
372
- // Wait for the message to be rendered
373
- await this.page.waitForTimeout(500);
374
-
375
- // Find assistant messages by their structure
376
- // Assistant messages have class "message-wrapper" and are aligned to the left
377
- const messageWrappers = this.page.locator('.message-wrapper');
378
- const wrapperCount = await messageWrappers.count();
379
- console.log(` 📍 Found ${wrapperCount} message wrappers`);
380
-
381
- // Find the assistant message by looking for the one with "Lobe AI" text
382
- let assistantMessage = null;
383
- for (let i = wrapperCount - 1; i >= 0; i--) {
384
- const wrapper = messageWrappers.nth(i);
385
- const titleText = await wrapper
386
- .locator('.message-header')
387
- .textContent()
388
- .catch(() => '');
389
- console.log(` 📍 Message ${i} title: "${titleText?.slice(0, 30)}..."`);
390
-
391
- // Check if this is an assistant message (has "Lobe AI" or similar in title)
392
- if (titleText?.includes('Lobe AI') || titleText?.includes('AI')) {
393
- assistantMessage = wrapper;
394
- console.log(` 📍 Found assistant message at index ${i}`);
395
- break;
396
- }
397
- }
398
-
399
- if (!assistantMessage) {
400
- throw new Error('No assistant messages found');
401
- }
402
-
403
- // Hover over the message to reveal action buttons
404
- console.log(' 📍 Hovering over assistant message to reveal actions...');
405
- await assistantMessage.hover();
406
- await this.page.waitForTimeout(800);
407
-
408
- // The action bar with role="menubar" contains the ActionIconGroup
409
- // The "more" button uses MoreHorizontal icon from lucide-react (class: lucide-more-horizontal)
410
- // Try to find the more button by its icon class
411
- const moreButtonByClass = this.page.locator('svg.lucide-more-horizontal').locator('..');
412
- let moreButtonCount = await moreButtonByClass.count();
413
- console.log(` 📍 Found ${moreButtonCount} buttons with more-horizontal icon`);
414
-
415
- let menuOpened = false;
416
-
417
- if (moreButtonCount > 0) {
418
- // Find the one in the main content area (not sidebar)
419
- for (let i = 0; i < moreButtonCount; i++) {
420
- const btn = moreButtonByClass.nth(i);
421
- const btnBox = await btn.boundingBox();
422
- if (!btnBox || btnBox.x < 320) continue; // Skip sidebar buttons
423
-
424
- console.log(` 📍 More button ${i} at (${btnBox.x}, ${btnBox.y})`);
425
- await btn.click();
426
- await this.page.waitForTimeout(500);
427
-
428
- // Check if dropdown menu appeared with regenerate option
429
- const menu = this.page.locator('.ant-dropdown-menu:visible');
430
- if ((await menu.count()) > 0) {
431
- const hasRegenerate = this.page.locator('.ant-dropdown-menu-item:has-text("重新生成")');
432
- if ((await hasRegenerate.count()) > 0) {
433
- console.log(` 📍 Found menu with regenerate option`);
434
- menuOpened = true;
435
- break;
436
- } else {
437
- const menuItems = await this.page.locator('.ant-dropdown-menu-item').allTextContents();
438
- console.log(` 📍 Menu items: ${menuItems.slice(0, 5).join(', ')}...`);
439
- await this.page.keyboard.press('Escape');
440
- await this.page.waitForTimeout(200);
441
- // Re-hover to keep action bar visible
442
- await assistantMessage.hover();
443
- await this.page.waitForTimeout(300);
444
- }
445
- }
446
- }
447
- }
448
-
449
- // Fallback: Look for all buttons in the action bar area after hovering
450
- if (!menuOpened) {
451
- console.log(' 📍 Fallback: Looking for buttons in action bar area...');
452
- await assistantMessage.hover();
453
- await this.page.waitForTimeout(500);
454
-
455
- // Find the action bar within message
456
- const actionBar = assistantMessage.locator('[role="menubar"]');
457
- if ((await actionBar.count()) > 0) {
458
- // Look for all buttons (ActionIcon components render as buttons)
459
- const allButtons = actionBar.locator('button, [role="button"]');
460
- const allButtonCount = await allButtons.count();
461
- console.log(` 📍 Found ${allButtonCount} buttons in action bar`);
462
-
463
- // Try clicking the last button (usually the "more" button)
464
- for (let i = allButtonCount - 1; i >= 0; i--) {
465
- const btn = allButtons.nth(i);
466
- await btn.click();
467
- await this.page.waitForTimeout(500);
468
-
469
- const menu = this.page.locator('.ant-dropdown-menu:visible');
470
- if ((await menu.count()) > 0) {
471
- const hasRegenerate = this.page.locator('.ant-dropdown-menu-item:has-text("重新生成")');
472
- if ((await hasRegenerate.count()) > 0) {
473
- menuOpened = true;
474
- break;
475
- }
476
- await this.page.keyboard.press('Escape');
477
- await assistantMessage.hover();
478
- await this.page.waitForTimeout(300);
479
- }
480
- }
481
- }
482
- }
483
-
484
- // Click on the regenerate option in the dropdown menu
485
- console.log(' 📍 Looking for regenerate option in menu...');
486
- const regenerateOption = this.page.locator(
487
- '.ant-dropdown-menu-item:has-text("重新生成"), .ant-dropdown-menu-item:has-text("Regenerate"), [data-menu-id*="regenerate"]',
488
- );
489
-
490
- if ((await regenerateOption.count()) > 0) {
491
- await expect(regenerateOption.first()).toBeVisible({ timeout: 5000 });
492
- console.log(' 📍 Clicking regenerate option...');
493
- await regenerateOption.first().click();
494
- } else {
495
- throw new Error('Regenerate option not found in menu');
496
- }
497
-
498
- console.log(' ✅ 已点击重新生成按钮');
499
- });
500
-
501
- When('用户在生成过程中点击停止按钮', async function (this: CustomWorld) {
502
- console.log(' 📍 Step: 等待生成开始...');
503
- await this.page.waitForTimeout(500);
504
-
505
- console.log(' 📍 Step: 查找停止按钮...');
506
- const stopButton = this.page.locator(
507
- 'button[aria-label*="停止"], button[aria-label*="stop"], [data-testid="stop-generate"]',
508
- );
509
-
510
- // The stop button should appear during generation
511
- const stopButtonVisible = await stopButton
512
- .first()
513
- .isVisible()
514
- .catch(() => false);
515
- if (stopButtonVisible) {
516
- await stopButton.first().click();
517
- console.log(' ✅ 已点击停止按钮');
518
- } else {
519
- console.log(' ⚠️ 停止按钮不可见,可能生成已完成');
520
- }
521
- });
522
-
523
- // ============================================
524
- // Then Steps for Advanced Scenarios
525
- // ============================================
526
-
527
- Then('对话历史应该被清空', async function (this: CustomWorld) {
528
- console.log(' 📍 Step: 验证对话历史已清空...');
529
-
530
- // Wait for the clear to take effect
531
- await this.page.waitForTimeout(1000);
532
-
533
- // Check that there are no user/assistant messages in the main chat area
534
- // Only look for messages with explicit data-role attribute, which are actual chat messages
535
- // Avoid matching sidebar items or other elements with "message" in class
536
- const userMessages = this.page.locator('[data-role="user"]');
537
- const assistantMessages = this.page.locator('[data-role="assistant"]');
538
-
539
- const userCount = await userMessages.count();
540
- const assistantCount = await assistantMessages.count();
541
-
542
- console.log(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
543
-
544
- // There should be no user or assistant messages after clearing
545
- expect(userCount).toBe(0);
546
- expect(assistantCount).toBe(0);
547
-
548
- console.log(' ✅ 对话历史已清空');
549
- });
550
-
551
- Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
552
- console.log(' 📍 Step: 验证显示欢迎界面...');
553
-
554
- // Look for welcome elements - Lobe AI title or welcome text in the main chat area
555
- // The welcome page shows Lobe AI avatar and introductory text
556
- // Try multiple selectors to find the welcome content
557
- const welcomeText = this.page.locator('text=我是你的智能助理');
558
- const lobeAITitle = this.page.locator('h1:has-text("Lobe AI"), h2:has-text("Lobe AI")');
559
- const welcomeStart = this.page.locator('text=从任何想法开始');
560
-
561
- const hasWelcomeText = (await welcomeText.count()) > 0;
562
- const hasLobeTitle = (await lobeAITitle.count()) > 0;
563
- const hasStartText = (await welcomeStart.count()) > 0;
564
-
565
- console.log(
566
- ` 📍 欢迎文本: ${hasWelcomeText}, Lobe标题: ${hasLobeTitle}, 开始提示: ${hasStartText}`,
567
- );
568
-
569
- // At least one of the welcome elements should be visible
570
- expect(hasWelcomeText || hasLobeTitle || hasStartText).toBeTruthy();
571
- console.log(' ✅ 欢迎界面可见');
572
- });
573
-
574
- Then('用户应该收到新的助手回复', async function (this: CustomWorld) {
575
- console.log(' 📍 Step: 等待新回复...');
576
-
577
- // Wait for a new response to appear
578
- await this.page.waitForTimeout(2000);
579
-
580
- const assistantMessage = this.page
581
- .locator('[data-role="assistant"], [class*="assistant"], [class*="message"]')
582
- .last();
583
-
584
- await expect(assistantMessage).toBeVisible({ timeout: 15_000 });
585
- console.log(' ✅ 收到新的助手回复');
586
- });
587
-
588
- Then('回复应该停止生成', async function (this: CustomWorld) {
589
- console.log(' 📍 Step: 验证生成已停止...');
590
-
591
- // The stop button should no longer be visible
592
- const stopButton = this.page.locator(
593
- 'button[aria-label*="停止"], button[aria-label*="stop"], [data-testid="stop-generate"]',
594
- );
595
-
596
- // Wait a bit and check if stop button is gone
597
- await this.page.waitForTimeout(1000);
598
- const isStopVisible = await stopButton
599
- .first()
600
- .isVisible()
601
- .catch(() => false);
602
-
603
- // Stop button should be hidden after stopping
604
- expect(isStopVisible).toBeFalsy();
605
- console.log(' ✅ 生成已停止');
606
- });
607
-
608
- Then('已生成的内容应该保留', async function (this: CustomWorld) {
609
- console.log(' 📍 Step: 验证已生成内容...');
610
-
611
- // There should be some content in the last assistant message
612
- const assistantMessage = this.page
613
- .locator('[data-role="assistant"], [class*="assistant"], [class*="message"]')
614
- .last();
615
-
616
- const text = await assistantMessage.textContent();
617
- expect(text).toBeTruthy();
618
- expect(text!.length).toBeGreaterThan(0);
619
-
620
- console.log(` ✅ 已生成内容保留: "${text?.slice(0, 50)}..."`);
621
- });