@lobehub/lobehub 2.0.0-next.232 → 2.0.0-next.234

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 (112) hide show
  1. package/.github/workflows/bundle-analyzer.yml +1 -1
  2. package/.github/workflows/e2e.yml +62 -53
  3. package/.github/workflows/manual-build-desktop.yml +5 -5
  4. package/.github/workflows/pr-build-desktop.yml +4 -4
  5. package/.github/workflows/pr-build-docker.yml +2 -2
  6. package/.github/workflows/release-desktop-beta.yml +4 -4
  7. package/.github/workflows/release-docker.yml +2 -2
  8. package/.github/workflows/test.yml +44 -7
  9. package/CHANGELOG.md +59 -0
  10. package/CLAUDE.md +1 -1
  11. package/changelog/v1.json +14 -0
  12. package/docs/development/basic/feature-development.mdx +4 -5
  13. package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
  14. package/docs/self-hosting/environment-variables/auth.mdx +7 -0
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
  16. package/e2e/README.md +6 -6
  17. package/e2e/src/features/community/detail-pages.feature +9 -9
  18. package/e2e/src/features/community/interactions.feature +13 -13
  19. package/e2e/src/features/community/smoke.feature +6 -6
  20. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
  21. package/e2e/src/steps/agent/conversation.steps.ts +58 -0
  22. package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
  23. package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
  24. package/e2e/src/steps/community/interactions.steps.ts +145 -32
  25. package/e2e/src/steps/hooks.ts +12 -2
  26. package/locales/en-US/setting.json +3 -0
  27. package/locales/zh-CN/file.json +4 -0
  28. package/locales/zh-CN/setting.json +3 -0
  29. package/package.json +5 -5
  30. package/packages/business/config/src/llm.ts +6 -1
  31. package/packages/const/src/index.ts +1 -0
  32. package/packages/const/src/lobehubSkill.ts +55 -0
  33. package/packages/const/src/settings/image.ts +1 -1
  34. package/packages/model-bank/src/aiModels/azure.ts +2 -2
  35. package/packages/model-bank/src/aiModels/google.ts +1 -0
  36. package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
  37. package/packages/model-bank/src/aiModels/openai.ts +21 -4
  38. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
  39. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
  40. package/packages/ssrf-safe-fetch/index.test.ts +5 -34
  41. package/packages/ssrf-safe-fetch/index.ts +12 -2
  42. package/packages/types/package.json +1 -1
  43. package/packages/types/src/files/upload.ts +11 -1
  44. package/packages/types/src/message/common/tools.ts +1 -1
  45. package/packages/types/src/serverConfig.ts +1 -0
  46. package/public/not-compatible.html +1296 -0
  47. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
  48. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
  49. package/src/app/[variants]/(main)/image/index.tsx +1 -1
  50. package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
  51. package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
  52. package/src/app/[variants]/layout.tsx +50 -1
  53. package/src/envs/auth.ts +15 -0
  54. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
  55. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
  56. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
  57. package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
  58. package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
  59. package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
  60. package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
  61. package/src/features/FileViewer/index.tsx +135 -24
  62. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
  63. package/src/features/PageEditor/store/initialState.ts +2 -1
  64. package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
  65. package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
  66. package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
  67. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
  68. package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
  69. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
  70. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
  71. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
  72. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
  73. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
  74. package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
  75. package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
  76. package/src/features/ResourceManager/index.tsx +1 -0
  77. package/src/helpers/toolEngineering/index.test.ts +3 -0
  78. package/src/helpers/toolEngineering/index.ts +12 -1
  79. package/src/hooks/useFetchAiImageConfig.ts +54 -10
  80. package/src/libs/trpc/utils/internalJwt.ts +2 -2
  81. package/src/locales/default/file.ts +4 -0
  82. package/src/locales/default/setting.ts +3 -0
  83. package/src/server/globalConfig/index.ts +1 -0
  84. package/src/server/modules/ModelRuntime/index.test.ts +214 -1
  85. package/src/server/modules/ModelRuntime/index.ts +43 -7
  86. package/src/server/routers/lambda/document.ts +44 -0
  87. package/src/server/routers/tools/market.ts +261 -0
  88. package/src/server/services/document/index.ts +22 -0
  89. package/src/services/document/index.ts +4 -0
  90. package/src/services/upload.ts +22 -2
  91. package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
  92. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
  93. package/src/store/file/slices/fileManager/action.test.ts +9 -3
  94. package/src/store/file/slices/fileManager/action.ts +165 -70
  95. package/src/store/file/slices/upload/action.ts +3 -0
  96. package/src/store/global/actions/general.ts +15 -0
  97. package/src/store/global/initialState.ts +13 -0
  98. package/src/store/image/slices/generationConfig/initialState.ts +5 -5
  99. package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
  100. package/src/store/serverConfig/selectors.ts +1 -0
  101. package/src/store/tool/initialState.ts +11 -2
  102. package/src/store/tool/selectors/index.ts +1 -0
  103. package/src/store/tool/selectors/tool.ts +3 -1
  104. package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
  105. package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
  106. package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
  107. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
  108. package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
  109. package/src/store/tool/store.ts +8 -2
  110. package/vitest.config.mts +11 -6
  111. package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
  112. package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
@@ -1,4 +1,4 @@
1
- @discover @interactions
1
+ @community @interactions
2
2
  Feature: Discover Interactions
3
3
  Tests for user interactions within the discover module
4
4
 
@@ -9,14 +9,14 @@ Feature: Discover Interactions
9
9
  # Assistant Page Interactions
10
10
  # ============================================
11
11
 
12
- @DISCOVER-INTERACT-001 @P1
12
+ @COMMUNITY-INTERACT-001 @P1
13
13
  Scenario: Search for assistants
14
14
  Given I navigate to "/community/assistant"
15
15
  When I type "developer" in the search bar
16
16
  And I wait for the search results to load
17
17
  Then I should see filtered assistant cards
18
18
 
19
- @DISCOVER-INTERACT-002 @P1
19
+ @COMMUNITY-INTERACT-002 @P1
20
20
  Scenario: Filter assistants by category
21
21
  Given I navigate to "/community/assistant"
22
22
  When I click on a category in the category menu
@@ -24,7 +24,7 @@ Feature: Discover Interactions
24
24
  Then I should see assistant cards filtered by the selected category
25
25
  And the URL should contain the category parameter
26
26
 
27
- @DISCOVER-INTERACT-003 @P1
27
+ @COMMUNITY-INTERACT-003 @P1
28
28
  Scenario: Navigate to next page of assistants
29
29
  Given I navigate to "/community/assistant"
30
30
  When I click the next page button
@@ -32,7 +32,7 @@ Feature: Discover Interactions
32
32
  Then I should see different assistant cards
33
33
  And the URL should contain the page parameter
34
34
 
35
- @DISCOVER-INTERACT-004 @P1
35
+ @COMMUNITY-INTERACT-004 @P1
36
36
  Scenario: Navigate to assistant detail page
37
37
  Given I navigate to "/community/assistant"
38
38
  When I click on the first assistant card
@@ -43,7 +43,7 @@ Feature: Discover Interactions
43
43
  # Model Page Interactions
44
44
  # ============================================
45
45
 
46
- @DISCOVER-INTERACT-005 @P1
46
+ @COMMUNITY-INTERACT-005 @P1
47
47
  Scenario: Sort models
48
48
  Given I navigate to "/community/model"
49
49
  When I click on the sort dropdown
@@ -51,7 +51,7 @@ Feature: Discover Interactions
51
51
  And I wait for the sorted results to load
52
52
  Then I should see model cards in the sorted order
53
53
 
54
- @DISCOVER-INTERACT-006 @P1
54
+ @COMMUNITY-INTERACT-006 @P1
55
55
  Scenario: Navigate to model detail page
56
56
  Given I navigate to "/community/model"
57
57
  When I click on the first model card
@@ -62,7 +62,7 @@ Feature: Discover Interactions
62
62
  # Provider Page Interactions
63
63
  # ============================================
64
64
 
65
- @DISCOVER-INTERACT-007 @P1
65
+ @COMMUNITY-INTERACT-007 @P1
66
66
  Scenario: Navigate to provider detail page
67
67
  Given I navigate to "/community/provider"
68
68
  When I click on the first provider card
@@ -73,14 +73,14 @@ Feature: Discover Interactions
73
73
  # MCP Page Interactions
74
74
  # ============================================
75
75
 
76
- @DISCOVER-INTERACT-008 @P1
76
+ @COMMUNITY-INTERACT-008 @P1
77
77
  Scenario: Filter MCP tools by category
78
78
  Given I navigate to "/community/mcp"
79
79
  When I click on a category in the category filter
80
80
  And I wait for the filtered results to load
81
81
  Then I should see MCP cards filtered by the selected category
82
82
 
83
- @DISCOVER-INTERACT-009 @P1
83
+ @COMMUNITY-INTERACT-009 @P1
84
84
  Scenario: Navigate to MCP detail page
85
85
  Given I navigate to "/community/mcp"
86
86
  When I click on the first MCP card
@@ -91,21 +91,21 @@ Feature: Discover Interactions
91
91
  # Home Page Interactions
92
92
  # ============================================
93
93
 
94
- @DISCOVER-INTERACT-010 @P1
94
+ @COMMUNITY-INTERACT-010 @P1
95
95
  Scenario: Navigate from home to assistant list
96
96
  Given I navigate to "/community"
97
97
  When I click on the "more" link in the featured assistants section
98
98
  Then I should be navigated to "/community/assistant"
99
99
  And I should see the page body
100
100
 
101
- @DISCOVER-INTERACT-011 @P1
101
+ @COMMUNITY-INTERACT-011 @P1
102
102
  Scenario: Navigate from home to MCP list
103
103
  Given I navigate to "/community"
104
104
  When I click on the "more" link in the featured MCP tools section
105
105
  Then I should be navigated to "/community/mcp"
106
106
  And I should see the page body
107
107
 
108
- @DISCOVER-INTERACT-012 @P1
108
+ @COMMUNITY-INTERACT-012 @P1
109
109
  Scenario: Click featured assistant from home
110
110
  Given I navigate to "/community"
111
111
  When I click on the first featured assistant card
@@ -1,8 +1,8 @@
1
- @discover @smoke
1
+ @community @smoke
2
2
  Feature: Community Smoke Tests
3
3
  Critical path tests to ensure the community/discover module is functional
4
4
 
5
- @DISCOVER-SMOKE-001 @P0
5
+ @COMMUNITY-SMOKE-001 @P0
6
6
  Scenario: Load Community Home Page
7
7
  Given I navigate to "/community"
8
8
  Then the page should load without errors
@@ -10,7 +10,7 @@ Feature: Community Smoke Tests
10
10
  And I should see the featured assistants section
11
11
  And I should see the featured MCP tools section
12
12
 
13
- @DISCOVER-SMOKE-002 @P0
13
+ @COMMUNITY-SMOKE-002 @P0
14
14
  Scenario: Load Assistant List Page
15
15
  Given I navigate to "/community/assistant"
16
16
  Then the page should load without errors
@@ -20,7 +20,7 @@ Feature: Community Smoke Tests
20
20
  And I should see assistant cards
21
21
  And I should see pagination controls
22
22
 
23
- @DISCOVER-SMOKE-003 @P0
23
+ @COMMUNITY-SMOKE-003 @P0
24
24
  Scenario: Load Model List Page
25
25
  Given I navigate to "/community/model"
26
26
  Then the page should load without errors
@@ -28,14 +28,14 @@ Feature: Community Smoke Tests
28
28
  And I should see model cards
29
29
  And I should see the sort dropdown
30
30
 
31
- @DISCOVER-SMOKE-004 @P0
31
+ @COMMUNITY-SMOKE-004 @P0
32
32
  Scenario: Load Provider List Page
33
33
  Given I navigate to "/community/provider"
34
34
  Then the page should load without errors
35
35
  And I should see the page body
36
36
  And I should see provider cards
37
37
 
38
- @DISCOVER-SMOKE-005 @P0
38
+ @COMMUNITY-SMOKE-005 @P0
39
39
  Scenario: Load MCP List Page
40
40
  Given I navigate to "/community/mcp"
41
41
  Then the page should load without errors
@@ -200,56 +200,191 @@ When('用户右键点击一个对话', async function (this: CustomWorld) {
200
200
  When('用户选择重命名选项', async function (this: CustomWorld) {
201
201
  console.log(' 📍 Step: 选择重命名选项...');
202
202
 
203
- // The context menu should be visible with "rename" option
204
- // Use exact match to avoid matching "智能重命名"
205
- const renameOption = this.page.getByRole('menuitem', { exact: true, name: '重命名' });
203
+ // First, close any open context menu by clicking elsewhere
204
+ await this.page.click('body', { position: { x: 500, y: 300 } });
205
+ await this.page.waitForTimeout(300);
206
+
207
+ // Instead of using right-click context menu, use the "..." dropdown menu
208
+ // which appears when hovering over a topic item
209
+ const topicItems = this.page.locator('svg.lucide-star').locator('..').locator('..');
210
+ const topicCount = await topicItems.count();
211
+ console.log(` 📍 Found ${topicCount} topic items`);
212
+
213
+ if (topicCount > 0) {
214
+ // Hover on the first topic to reveal the "..." action button
215
+ const firstTopic = topicItems.first();
216
+ await firstTopic.hover();
217
+ console.log(' 📍 Hovering on topic item...');
218
+ await this.page.waitForTimeout(500);
219
+
220
+ // The "..." button should now be visible INSIDE the topic item
221
+ // Important: we must find the icon WITHIN the hovered topic, not the global one
222
+ // The topic item has a specific structure with nav-item-actions
223
+ const moreButtonInTopic = firstTopic.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal');
224
+ let moreButtonCount = await moreButtonInTopic.count();
225
+ console.log(` 📍 Found ${moreButtonCount} more buttons inside topic`);
226
+
227
+ if (moreButtonCount > 0) {
228
+ // Click the "..." button to open dropdown menu
229
+ await moreButtonInTopic.first().click();
230
+ console.log(' 📍 Clicked ... button inside topic');
231
+ await this.page.waitForTimeout(500);
232
+ } else {
233
+ // Fallback: try to find it by looking at the actions container
234
+ console.log(' 📍 Trying alternative: looking for actions container...');
235
+
236
+ // Debug: print the topic item HTML structure
237
+ const topicHTML = await firstTopic.evaluate((el) => el.outerHTML.slice(0, 500));
238
+ console.log(` 📍 Topic HTML: ${topicHTML}`);
239
+
240
+ // The actions might be in a sibling or parent element
241
+ // Try finding any ellipsis icon that's near the topic
242
+ const allEllipsis = this.page.locator('svg.lucide-ellipsis');
243
+ const ellipsisCount = await allEllipsis.count();
244
+ console.log(` 📍 Total ellipsis icons on page: ${ellipsisCount}`);
245
+
246
+ // Skip the first one (which is the global topic list menu)
247
+ // and click the second one (which should be in the topic item)
248
+ if (ellipsisCount > 1) {
249
+ await allEllipsis.nth(1).click();
250
+ console.log(' 📍 Clicked second ellipsis icon');
251
+ await this.page.waitForTimeout(500);
252
+ }
253
+ }
254
+ }
255
+
256
+ // Now find the rename option in the dropdown menu
257
+ const renameOption = this.page.getByRole('menuitem', { exact: true, name: /^(Rename|重命名)$/ });
206
258
 
207
259
  await expect(renameOption).toBeVisible({ timeout: 5000 });
260
+ console.log(' 📍 Found rename menu item');
261
+
262
+ // Click the rename option
208
263
  await renameOption.click();
264
+ console.log(' 📍 Clicked rename menu item');
265
+
266
+ // Wait for the popover/input to appear
267
+ await this.page.waitForTimeout(500);
268
+
269
+ // Check if input appeared
270
+ const inputCount = await this.page.locator('input').count();
271
+ console.log(` 📍 After click: ${inputCount} inputs on page`);
209
272
 
210
273
  console.log(' ✅ 已选择重命名选项');
211
- await this.page.waitForTimeout(300);
212
274
  });
213
275
 
214
276
  When('用户输入新的对话名称 {string}', async function (this: CustomWorld, newName: string) {
215
277
  console.log(` 📍 Step: 输入新名称 "${newName}"...`);
216
278
 
217
- // The topic should now be in editing mode with an input field
218
- this.page.locator('input[type="text"]').filter({
219
- has: this.page.locator(':focus'),
279
+ // Debug: check what's on the page
280
+ const debugInfo = await this.page.evaluate(() => {
281
+ const allInputs = document.querySelectorAll('input');
282
+ const allPopovers = document.querySelectorAll('[class*="popover"], .ant-popover');
283
+ const focusedElement = document.activeElement;
284
+ return {
285
+ focusedClass: focusedElement?.className,
286
+ focusedTag: focusedElement?.tagName,
287
+ inputCount: allInputs.length,
288
+ inputTags: Array.from(allInputs).map((i) => ({
289
+ className: i.className,
290
+ placeholder: i.placeholder,
291
+ type: i.type,
292
+ visible: i.offsetParent !== null,
293
+ })),
294
+ popoverCount: allPopovers.length,
295
+ };
220
296
  });
297
+ console.log(' 📍 Debug info:', JSON.stringify(debugInfo, null, 2));
221
298
 
222
- // Wait for input to appear
223
- await this.page.waitForTimeout(500);
299
+ // Wait a short moment for the popover to render
300
+ await this.page.waitForTimeout(300);
301
+
302
+ // Try to find the popover input using various selectors
303
+ // @lobehub/ui Popover uses antd's Popover internally
304
+ const popoverInputSelectors = [
305
+ // antd popover structure
306
+ '.ant-popover-inner input',
307
+ '.ant-popover-content input',
308
+ '.ant-popover input',
309
+ // Generic input that's visible and not the chat input
310
+ 'input:not([data-testid="chat-input"] input)',
311
+ ];
312
+
313
+ let renameInput = null;
314
+
315
+ // Wait for any popover input to appear
316
+ for (const selector of popoverInputSelectors) {
317
+ try {
318
+ const locator = this.page.locator(selector).first();
319
+ await locator.waitFor({ state: 'visible', timeout: 2000 });
320
+ renameInput = locator;
321
+ console.log(` 📍 Found input with selector: ${selector}`);
322
+ break;
323
+ } catch {
324
+ // Try next selector
325
+ }
326
+ }
224
327
 
225
- // Find the visible input in the sidebar area
226
- const sidebarInput = this.page.locator('[class*="NavItem"] input, .ant-input');
227
- const inputCount = await sidebarInput.count();
228
- console.log(` 📍 Found ${inputCount} input fields`);
328
+ if (!renameInput) {
329
+ // Fallback: find any visible input that's not the search or chat input
330
+ console.log(' 📍 Trying fallback: finding any visible input...');
331
+ const allInputs = this.page.locator('input:visible');
332
+ const count = await allInputs.count();
333
+ console.log(` 📍 Found ${count} visible inputs`);
334
+
335
+ for (let i = 0; i < count; i++) {
336
+ const input = allInputs.nth(i);
337
+ const placeholder = await input.getAttribute('placeholder').catch(() => '');
338
+ const testId = await input.dataset.testid.catch(() => '');
339
+
340
+ // Skip search inputs and chat inputs
341
+ if (placeholder?.includes('Search') || placeholder?.includes('搜索')) continue;
342
+ if (testId === 'chat-input') continue;
343
+
344
+ // Check if it's inside a popover-like container
345
+ const isInPopover = await input.evaluate((el) => {
346
+ return el.closest('.ant-popover') !== null || el.closest('[class*="popover"]') !== null;
347
+ });
348
+
349
+ if (isInPopover || count === 1) {
350
+ renameInput = input;
351
+ console.log(` 📍 Found candidate input at index ${i}`);
352
+ break;
353
+ }
354
+ }
355
+ }
229
356
 
230
- if (inputCount > 0) {
231
- const input = sidebarInput.first();
232
- await input.clear();
233
- await input.fill(newName);
234
- await this.page.keyboard.press('Enter');
357
+ if (renameInput) {
358
+ // Clear and fill the input
359
+ await renameInput.click();
360
+ await renameInput.clear();
361
+ await renameInput.fill(newName);
362
+ console.log(` 📍 Filled input with "${newName}"`);
363
+
364
+ // Press Enter to confirm
365
+ await renameInput.press('Enter');
235
366
  console.log(` ✅ 已输入新名称 "${newName}"`);
236
367
  } else {
237
- // Try finding by focused element
238
- await this.page.keyboard.type(newName, { delay: 30 });
368
+ // Last resort: the input should have autoFocus, so keyboard should work
369
+ console.log(' ⚠️ Could not find rename input element, using keyboard fallback...');
370
+ // Select all and replace
371
+ await this.page.keyboard.press('Meta+A');
372
+ await this.page.waitForTimeout(50);
373
+ await this.page.keyboard.type(newName, { delay: 20 });
239
374
  await this.page.keyboard.press('Enter');
240
375
  console.log(` ✅ 已通过键盘输入新名称 "${newName}"`);
241
376
  }
242
377
 
243
- await this.page.waitForTimeout(500);
378
+ // Wait for the rename to be saved
379
+ await this.page.waitForTimeout(1000);
244
380
  });
245
381
 
246
382
  When('用户选择删除选项', async function (this: CustomWorld) {
247
383
  console.log(' 📍 Step: 选择删除选项...');
248
384
 
249
385
  // The context menu should be visible with "delete" option
250
- const deleteOption = this.page.locator(
251
- '.ant-dropdown-menu-item:has-text("删除"), .ant-dropdown-menu-item-danger',
252
- );
386
+ // Support both English and Chinese
387
+ const deleteOption = this.page.getByRole('menuitem', { exact: true, name: /^(Delete|删除)$/ });
253
388
 
254
389
  await expect(deleteOption).toBeVisible({ timeout: 5000 });
255
390
  await deleteOption.click();
@@ -276,7 +411,10 @@ When('用户在搜索框中输入 {string}', async function (this: CustomWorld,
276
411
  console.log(` 📍 Step: 在搜索框中输入 "${searchText}"...`);
277
412
 
278
413
  // Find the search input in the sidebar
279
- const searchInput = this.page.locator('input[placeholder*="搜索"], [data-testid="search-input"]');
414
+ // Support both English and Chinese placeholders
415
+ const searchInput = this.page.locator(
416
+ 'input[placeholder*="Search"], input[placeholder*="搜索"], [data-testid="search-input"]',
417
+ );
280
418
 
281
419
  if ((await searchInput.count()) > 0) {
282
420
  await searchInput.first().click();
@@ -321,6 +459,39 @@ Then('应该创建一个新的空白对话', async function (this: CustomWorld)
321
459
  console.log(' ✅ 新对话已创建');
322
460
  });
323
461
 
462
+ Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
463
+ console.log(' 📍 Step: 验证页面显示欢迎界面...');
464
+
465
+ // Wait for the page to update
466
+ await this.page.waitForTimeout(500);
467
+
468
+ // New conversation typically shows a welcome/empty state
469
+ // Check for visible chat input (there may be 2 - desktop and mobile, find the visible one)
470
+ const chatInputs = this.page.locator('[data-testid="chat-input"]');
471
+ const count = await chatInputs.count();
472
+
473
+ let foundVisible = false;
474
+ for (let i = 0; i < count; i++) {
475
+ const elem = chatInputs.nth(i);
476
+ const box = await elem.boundingBox();
477
+ if (box && box.width > 0 && box.height > 0) {
478
+ foundVisible = true;
479
+ console.log(` 📍 Found visible chat-input at index ${i}`);
480
+ break;
481
+ }
482
+ }
483
+
484
+ // Just verify the page is loaded properly by checking URL or any content
485
+ if (!foundVisible) {
486
+ // Fallback: just verify we're still on the chat page
487
+ const currentUrl = this.page.url();
488
+ expect(currentUrl).toContain('/chat');
489
+ console.log(' 📍 Fallback: verified we are on chat page');
490
+ }
491
+
492
+ console.log(' ✅ 欢迎界面已显示');
493
+ });
494
+
324
495
  Then('应该切换到该对话', async function (this: CustomWorld) {
325
496
  console.log(' 📍 Step: 验证已切换对话...');
326
497
 
@@ -81,6 +81,64 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
81
81
  // When Steps
82
82
  // ============================================
83
83
 
84
+ /**
85
+ * Given step for when user has already sent a message
86
+ * This sends a message and waits for the AI response
87
+ */
88
+ Given('用户已发送消息 {string}', async function (this: CustomWorld, message: string) {
89
+ console.log(` 📍 Step: 发送消息 "${message}" 并等待回复...`);
90
+
91
+ // Find visible chat input container first
92
+ const chatInputs = this.page.locator('[data-testid="chat-input"]');
93
+ const count = await chatInputs.count();
94
+
95
+ let chatInputContainer = chatInputs.first();
96
+ for (let i = 0; i < count; i++) {
97
+ const elem = chatInputs.nth(i);
98
+ const box = await elem.boundingBox();
99
+ if (box && box.width > 0 && box.height > 0) {
100
+ chatInputContainer = elem;
101
+ break;
102
+ }
103
+ }
104
+
105
+ // Click the container to ensure focus is on the input area
106
+ await chatInputContainer.click();
107
+ await this.page.waitForTimeout(500);
108
+
109
+ // Type the message
110
+ await this.page.keyboard.type(message, { delay: 30 });
111
+ await this.page.waitForTimeout(300);
112
+
113
+ // Send the message
114
+ await this.page.keyboard.press('Enter');
115
+
116
+ // Wait for the message to be sent
117
+ await this.page.waitForTimeout(1000);
118
+
119
+ // Wait for the assistant response to appear
120
+ // Assistant messages are left-aligned .message-wrapper elements that contain "Lobe AI" title
121
+ console.log(' 📍 Step: 等待助手回复...');
122
+
123
+ // Wait for any new message wrapper to appear (there should be at least 2 - user + assistant)
124
+ const messageWrappers = this.page.locator('.message-wrapper');
125
+ await expect(messageWrappers)
126
+ .toHaveCount(2, { timeout: 15_000 })
127
+ .catch(() => {
128
+ // Fallback: just wait for at least one message wrapper
129
+ console.log(' 📍 Fallback: checking for any message wrapper');
130
+ });
131
+
132
+ // Verify the assistant message contains expected content
133
+ const assistantMessage = this.page.locator('.message-wrapper').filter({
134
+ has: this.page.locator('text=Lobe AI'),
135
+ });
136
+ await expect(assistantMessage).toBeVisible({ timeout: 5000 });
137
+
138
+ this.testContext.lastMessage = message;
139
+ console.log(` ✅ 消息已发送并收到回复`);
140
+ });
141
+
84
142
  When('用户发送消息 {string}', async function (this: CustomWorld, message: string) {
85
143
  console.log(` 📍 Step: 查找输入框...`);
86
144
 
@@ -259,15 +259,19 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
259
259
  for (let i = 0; i < svgButtonCount; i++) {
260
260
  const btn = allSvgButtons.nth(i);
261
261
  const box = await btn.boundingBox();
262
- if (box && box.width > 0 && box.height > 0 && box.width < 50 && // Only consider small buttons (action icons are small)
263
-
264
- box.x > 320 &&
265
- box.y >= messageBox.y &&
266
- box.y <= messageBox.y + messageBox.height + 50
267
- && box.x > maxX) {
268
- maxX = box.x;
269
- rightmostBtn = btn;
270
- }
262
+ if (
263
+ box &&
264
+ box.width > 0 &&
265
+ box.height > 0 &&
266
+ box.width < 50 && // Only consider small buttons (action icons are small)
267
+ box.x > 320 &&
268
+ box.y >= messageBox.y &&
269
+ box.y <= messageBox.y + messageBox.height + 50 &&
270
+ box.x > maxX
271
+ ) {
272
+ maxX = box.x;
273
+ rightmostBtn = btn;
274
+ }
271
275
  }
272
276
 
273
277
  if (rightmostBtn) {
@@ -284,8 +288,9 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
284
288
  When('用户选择删除消息选项', async function (this: CustomWorld) {
285
289
  console.log(' 📍 Step: 选择删除消息选项...');
286
290
 
287
- // Find and click delete option (exact match to avoid "删除并重新生成")
288
- const deleteOption = this.page.getByRole('menuitem', { exact: true, name: '删除' });
291
+ // Find and click delete option (exact match to avoid "Delete and Regenerate")
292
+ // Support both English and Chinese
293
+ const deleteOption = this.page.getByRole('menuitem', { exact: true, name: /^(Delete|删除)$/ });
289
294
  await expect(deleteOption).toBeVisible({ timeout: 5000 });
290
295
  await deleteOption.click();
291
296
 
@@ -313,8 +318,8 @@ When('用户确认删除消息', async function (this: CustomWorld) {
313
318
  When('用户选择折叠消息选项', async function (this: CustomWorld) {
314
319
  console.log(' 📍 Step: 选择折叠消息选项...');
315
320
 
316
- // The collapse option is "收起消息" in the menu
317
- const collapseOption = this.page.getByRole('menuitem', { name: /收起消息/ });
321
+ // The collapse option is "Collapse Message" or "收起消息" in the menu
322
+ const collapseOption = this.page.getByRole('menuitem', { name: /Collapse Message|收起消息/ });
318
323
  await expect(collapseOption).toBeVisible({ timeout: 5000 });
319
324
  await collapseOption.click();
320
325
 
@@ -325,8 +330,8 @@ When('用户选择折叠消息选项', async function (this: CustomWorld) {
325
330
  When('用户选择展开消息选项', async function (this: CustomWorld) {
326
331
  console.log(' 📍 Step: 选择展开消息选项...');
327
332
 
328
- // The expand option is "展开消息" in the menu
329
- const expandOption = this.page.getByRole('menuitem', { name: /展开消息/ });
333
+ // The expand option is "Expand Message" or "展开消息" in the menu
334
+ const expandOption = this.page.getByRole('menuitem', { name: /Expand Message|展开消息/ });
330
335
  await expect(expandOption).toBeVisible({ timeout: 5000 });
331
336
  await expandOption.click();
332
337