@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.
- package/.github/workflows/bundle-analyzer.yml +1 -1
- package/.github/workflows/e2e.yml +62 -53
- package/.github/workflows/manual-build-desktop.yml +5 -5
- package/.github/workflows/pr-build-desktop.yml +4 -4
- package/.github/workflows/pr-build-docker.yml +2 -2
- package/.github/workflows/release-desktop-beta.yml +4 -4
- package/.github/workflows/release-docker.yml +2 -2
- package/.github/workflows/test.yml +44 -7
- package/CHANGELOG.md +59 -0
- package/CLAUDE.md +1 -1
- package/changelog/v1.json +14 -0
- package/docs/development/basic/feature-development.mdx +4 -5
- package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
- package/docs/self-hosting/environment-variables/auth.mdx +7 -0
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
- package/e2e/README.md +6 -6
- package/e2e/src/features/community/detail-pages.feature +9 -9
- package/e2e/src/features/community/interactions.feature +13 -13
- package/e2e/src/features/community/smoke.feature +6 -6
- package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
- package/e2e/src/steps/agent/conversation.steps.ts +58 -0
- package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
- package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
- package/e2e/src/steps/community/interactions.steps.ts +145 -32
- package/e2e/src/steps/hooks.ts +12 -2
- package/locales/en-US/setting.json +3 -0
- package/locales/zh-CN/file.json +4 -0
- package/locales/zh-CN/setting.json +3 -0
- package/package.json +5 -5
- package/packages/business/config/src/llm.ts +6 -1
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/lobehubSkill.ts +55 -0
- package/packages/const/src/settings/image.ts +1 -1
- package/packages/model-bank/src/aiModels/azure.ts +2 -2
- package/packages/model-bank/src/aiModels/google.ts +1 -0
- package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
- package/packages/model-bank/src/aiModels/openai.ts +21 -4
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
- package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
- package/packages/ssrf-safe-fetch/index.test.ts +5 -34
- package/packages/ssrf-safe-fetch/index.ts +12 -2
- package/packages/types/package.json +1 -1
- package/packages/types/src/files/upload.ts +11 -1
- package/packages/types/src/message/common/tools.ts +1 -1
- package/packages/types/src/serverConfig.ts +1 -0
- package/public/not-compatible.html +1296 -0
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
- package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
- package/src/app/[variants]/(main)/image/index.tsx +1 -1
- package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
- package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
- package/src/app/[variants]/layout.tsx +50 -1
- package/src/envs/auth.ts +15 -0
- package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
- package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
- package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
- package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
- package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
- package/src/features/FileViewer/index.tsx +135 -24
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
- package/src/features/PageEditor/store/initialState.ts +2 -1
- package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
- package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
- package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
- package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
- package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
- package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
- package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
- package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
- package/src/features/ResourceManager/index.tsx +1 -0
- package/src/helpers/toolEngineering/index.test.ts +3 -0
- package/src/helpers/toolEngineering/index.ts +12 -1
- package/src/hooks/useFetchAiImageConfig.ts +54 -10
- package/src/libs/trpc/utils/internalJwt.ts +2 -2
- package/src/locales/default/file.ts +4 -0
- package/src/locales/default/setting.ts +3 -0
- package/src/server/globalConfig/index.ts +1 -0
- package/src/server/modules/ModelRuntime/index.test.ts +214 -1
- package/src/server/modules/ModelRuntime/index.ts +43 -7
- package/src/server/routers/lambda/document.ts +44 -0
- package/src/server/routers/tools/market.ts +261 -0
- package/src/server/services/document/index.ts +22 -0
- package/src/services/document/index.ts +4 -0
- package/src/services/upload.ts +22 -2
- package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
- package/src/store/file/slices/fileManager/action.test.ts +9 -3
- package/src/store/file/slices/fileManager/action.ts +165 -70
- package/src/store/file/slices/upload/action.ts +3 -0
- package/src/store/global/actions/general.ts +15 -0
- package/src/store/global/initialState.ts +13 -0
- package/src/store/image/slices/generationConfig/initialState.ts +5 -5
- package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/tool/initialState.ts +11 -2
- package/src/store/tool/selectors/index.ts +1 -0
- package/src/store/tool/selectors/tool.ts +3 -1
- package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
- package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
- package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
- package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
- package/src/store/tool/store.ts +8 -2
- package/vitest.config.mts +11 -6
- package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
- package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
//
|
|
218
|
-
this.page.
|
|
219
|
-
|
|
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
|
|
223
|
-
await this.page.waitForTimeout(
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 (
|
|
231
|
-
|
|
232
|
-
await
|
|
233
|
-
await
|
|
234
|
-
await
|
|
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
|
-
//
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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 (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|