@lobehub/lobehub 2.0.0-next.266 → 2.0.0-next.267
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/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/e2e/CLAUDE.md +34 -73
- package/e2e/docs/local-setup.md +67 -219
- package/e2e/scripts/setup.ts +529 -0
- package/e2e/src/features/home/sidebarAgent.feature +62 -0
- package/e2e/src/features/home/sidebarGroup.feature +62 -0
- package/e2e/src/steps/home/sidebarAgent.steps.ts +373 -0
- package/e2e/src/steps/home/sidebarGroup.steps.ts +168 -0
- package/e2e/src/steps/hooks.ts +2 -0
- package/package.json +3 -3
- package/packages/utils/src/multimodalContent.test.ts +302 -0
- package/packages/utils/src/server/__tests__/sse.test.ts +353 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx +4 -11
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +3 -3
- package/src/features/ChatInput/ActionBar/Params/Controls.tsx +42 -7
- package/src/store/home/slices/sidebarUI/action.ts +9 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Sidebar Agent Steps
|
|
3
|
+
*
|
|
4
|
+
* Step definitions for Home page Agent management E2E tests
|
|
5
|
+
* - Rename
|
|
6
|
+
* - Pin/Unpin
|
|
7
|
+
* - Delete
|
|
8
|
+
*/
|
|
9
|
+
import { Given, Then, When } from '@cucumber/cucumber';
|
|
10
|
+
import { expect } from '@playwright/test';
|
|
11
|
+
|
|
12
|
+
import { TEST_USER } from '../../support/seedTestUser';
|
|
13
|
+
import { CustomWorld } from '../../support/world';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a test agent directly in database
|
|
17
|
+
*/
|
|
18
|
+
async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
|
|
19
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
20
|
+
if (!databaseUrl) throw new Error('DATABASE_URL not set');
|
|
21
|
+
|
|
22
|
+
const { default: pg } = await import('pg');
|
|
23
|
+
const client = new pg.Client({ connectionString: databaseUrl });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await client.connect();
|
|
27
|
+
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
const agentId = `agent_e2e_test_${Date.now()}`;
|
|
30
|
+
const slug = `test-agent-${Date.now()}`;
|
|
31
|
+
|
|
32
|
+
await client.query(
|
|
33
|
+
`INSERT INTO agents (id, slug, title, user_id, created_at, updated_at)
|
|
34
|
+
VALUES ($1, $2, $3, $4, $5, $5)
|
|
35
|
+
ON CONFLICT DO NOTHING`,
|
|
36
|
+
[agentId, slug, title, TEST_USER.id, now],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
console.log(` 📍 Created test agent in DB: ${agentId}`);
|
|
40
|
+
return agentId;
|
|
41
|
+
} finally {
|
|
42
|
+
await client.end();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================
|
|
47
|
+
// Given Steps
|
|
48
|
+
// ============================================
|
|
49
|
+
|
|
50
|
+
Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld) {
|
|
51
|
+
console.log(' 📍 Step: 在数据库中创建测试 Agent...');
|
|
52
|
+
const agentId = await createTestAgent('E2E Test Agent');
|
|
53
|
+
this.testContext.createdAgentId = agentId;
|
|
54
|
+
|
|
55
|
+
console.log(' 📍 Step: 导航到 Home 页面...');
|
|
56
|
+
await this.page.goto('/');
|
|
57
|
+
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
|
58
|
+
await this.page.waitForTimeout(1000);
|
|
59
|
+
|
|
60
|
+
console.log(' 📍 Step: 查找新创建的 Agent...');
|
|
61
|
+
// Look for the newly created agent in the sidebar by its specific ID
|
|
62
|
+
const agentItem = this.page.locator(`a[href="/agent/${agentId}"]`).first();
|
|
63
|
+
await expect(agentItem).toBeVisible({ timeout: 10_000 });
|
|
64
|
+
|
|
65
|
+
// Store agent reference for later use
|
|
66
|
+
const agentLabel = await agentItem.getAttribute('aria-label');
|
|
67
|
+
this.testContext.targetItemId = agentLabel || agentId;
|
|
68
|
+
this.testContext.targetItemSelector = `a[href="/agent/${agentId}"]`;
|
|
69
|
+
this.testContext.targetType = 'agent';
|
|
70
|
+
|
|
71
|
+
console.log(` ✅ 找到 Agent: ${agentLabel}, id: ${agentId}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
Given('该 Agent 未被置顶', async function (this: CustomWorld) {
|
|
75
|
+
console.log(' 📍 Step: 检查 Agent 未被置顶...');
|
|
76
|
+
// Check if the agent has a pin icon - if so, unpin it first
|
|
77
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
78
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
79
|
+
|
|
80
|
+
if ((await pinIcon.count()) > 0) {
|
|
81
|
+
// Unpin it first
|
|
82
|
+
await targetItem.click({ button: 'right' });
|
|
83
|
+
await this.page.waitForTimeout(300);
|
|
84
|
+
const unpinOption = this.page.getByRole('menuitem', { name: /取消置顶|Unpin/i });
|
|
85
|
+
if ((await unpinOption.count()) > 0) {
|
|
86
|
+
await unpinOption.click();
|
|
87
|
+
await this.page.waitForTimeout(500);
|
|
88
|
+
}
|
|
89
|
+
// Close menu if still open
|
|
90
|
+
await this.page.click('body', { position: { x: 10, y: 10 } });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(' ✅ Agent 未被置顶');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
Given('该 Agent 已被置顶', async function (this: CustomWorld) {
|
|
97
|
+
console.log(' 📍 Step: 确保 Agent 已被置顶...');
|
|
98
|
+
// Check if the agent has a pin icon - if not, pin it first
|
|
99
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
100
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
101
|
+
|
|
102
|
+
if ((await pinIcon.count()) === 0) {
|
|
103
|
+
// Pin it first
|
|
104
|
+
await targetItem.click({ button: 'right' });
|
|
105
|
+
await this.page.waitForTimeout(300);
|
|
106
|
+
const pinOption = this.page.getByRole('menuitem', { name: /置顶|Pin/i });
|
|
107
|
+
if ((await pinOption.count()) > 0) {
|
|
108
|
+
await pinOption.click();
|
|
109
|
+
await this.page.waitForTimeout(500);
|
|
110
|
+
}
|
|
111
|
+
// Close menu if still open
|
|
112
|
+
await this.page.click('body', { position: { x: 10, y: 10 } });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(' ✅ Agent 已被置顶');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ============================================
|
|
119
|
+
// When Steps
|
|
120
|
+
// ============================================
|
|
121
|
+
|
|
122
|
+
When('用户右键点击该 Agent', async function (this: CustomWorld) {
|
|
123
|
+
console.log(' 📍 Step: 右键点击 Agent...');
|
|
124
|
+
|
|
125
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
126
|
+
|
|
127
|
+
// Right-click on the inner content (the NavItem Block component)
|
|
128
|
+
// The ContextMenuTrigger wraps the Block, not the Link
|
|
129
|
+
const innerBlock = targetItem.locator('> div').first();
|
|
130
|
+
if ((await innerBlock.count()) > 0) {
|
|
131
|
+
await innerBlock.click({ button: 'right' });
|
|
132
|
+
} else {
|
|
133
|
+
await targetItem.click({ button: 'right' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.page.waitForTimeout(800);
|
|
137
|
+
|
|
138
|
+
// Debug: check what menus are visible
|
|
139
|
+
const menuItems = await this.page.locator('[role="menuitem"]').count();
|
|
140
|
+
console.log(` 📍 Debug: Found ${menuItems} menu items after right-click`);
|
|
141
|
+
|
|
142
|
+
console.log(' ✅ 已右键点击 Agent');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
When('用户悬停在该 Agent 上', async function (this: CustomWorld) {
|
|
146
|
+
console.log(' 📍 Step: 悬停在 Agent 上...');
|
|
147
|
+
|
|
148
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
149
|
+
await targetItem.hover();
|
|
150
|
+
await this.page.waitForTimeout(500);
|
|
151
|
+
|
|
152
|
+
console.log(' ✅ 已悬停在 Agent 上');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
When('用户点击更多操作按钮', async function (this: CustomWorld) {
|
|
156
|
+
console.log(' 📍 Step: 点击更多操作按钮...');
|
|
157
|
+
|
|
158
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
159
|
+
const moreButton = targetItem.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal').first();
|
|
160
|
+
|
|
161
|
+
if ((await moreButton.count()) > 0) {
|
|
162
|
+
await moreButton.click();
|
|
163
|
+
} else {
|
|
164
|
+
// Fallback: find any visible ellipsis button
|
|
165
|
+
const allEllipsis = this.page.locator('svg.lucide-ellipsis');
|
|
166
|
+
for (let i = 0; i < (await allEllipsis.count()); i++) {
|
|
167
|
+
const ellipsis = allEllipsis.nth(i);
|
|
168
|
+
if (await ellipsis.isVisible()) {
|
|
169
|
+
await ellipsis.click();
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await this.page.waitForTimeout(500);
|
|
176
|
+
console.log(' ✅ 已点击更多操作按钮');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
When('用户在菜单中选择重命名', async function (this: CustomWorld) {
|
|
180
|
+
console.log(' 📍 Step: 选择重命名选项...');
|
|
181
|
+
|
|
182
|
+
const renameOption = this.page.getByRole('menuitem', { name: /^(Rename|重命名)$/i });
|
|
183
|
+
await expect(renameOption).toBeVisible({ timeout: 5000 });
|
|
184
|
+
await renameOption.click();
|
|
185
|
+
await this.page.waitForTimeout(500);
|
|
186
|
+
|
|
187
|
+
console.log(' ✅ 已选择重命名选项');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
When('用户在菜单中选择置顶', async function (this: CustomWorld) {
|
|
191
|
+
console.log(' 📍 Step: 选择置顶选项...');
|
|
192
|
+
|
|
193
|
+
const pinOption = this.page.getByRole('menuitem', { name: /^(Pin|置顶)$/i });
|
|
194
|
+
await expect(pinOption).toBeVisible({ timeout: 5000 });
|
|
195
|
+
await pinOption.click();
|
|
196
|
+
await this.page.waitForTimeout(500);
|
|
197
|
+
|
|
198
|
+
console.log(' ✅ 已选择置顶选项');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
When('用户在菜单中选择取消置顶', async function (this: CustomWorld) {
|
|
202
|
+
console.log(' 📍 Step: 选择取消置顶选项...');
|
|
203
|
+
|
|
204
|
+
const unpinOption = this.page.getByRole('menuitem', { name: /^(Unpin|取消置顶)$/i });
|
|
205
|
+
await expect(unpinOption).toBeVisible({ timeout: 5000 });
|
|
206
|
+
await unpinOption.click();
|
|
207
|
+
await this.page.waitForTimeout(500);
|
|
208
|
+
|
|
209
|
+
console.log(' ✅ 已选择取消置顶选项');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
When('用户在菜单中选择删除', async function (this: CustomWorld) {
|
|
213
|
+
console.log(' 📍 Step: 选择删除选项...');
|
|
214
|
+
|
|
215
|
+
const deleteOption = this.page.getByRole('menuitem', { name: /^(Delete|删除)$/i });
|
|
216
|
+
await expect(deleteOption).toBeVisible({ timeout: 5000 });
|
|
217
|
+
await deleteOption.click();
|
|
218
|
+
await this.page.waitForTimeout(300);
|
|
219
|
+
|
|
220
|
+
console.log(' ✅ 已选择删除选项');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
When('用户在弹窗中确认删除', async function (this: CustomWorld) {
|
|
224
|
+
console.log(' 📍 Step: 确认删除...');
|
|
225
|
+
|
|
226
|
+
const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous');
|
|
227
|
+
await expect(confirmButton).toBeVisible({ timeout: 5000 });
|
|
228
|
+
await confirmButton.click();
|
|
229
|
+
await this.page.waitForTimeout(500);
|
|
230
|
+
|
|
231
|
+
console.log(' ✅ 已确认删除');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
When('用户输入新的名称 {string}', async function (this: CustomWorld, newName: string) {
|
|
235
|
+
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
|
|
236
|
+
await inputNewName.call(this, newName, false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
When(
|
|
240
|
+
'用户输入新的名称 {string} 并按 Enter',
|
|
241
|
+
async function (this: CustomWorld, newName: string) {
|
|
242
|
+
console.log(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
|
|
243
|
+
await inputNewName.call(this, newName, true);
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ============================================
|
|
248
|
+
// Then Steps
|
|
249
|
+
// ============================================
|
|
250
|
+
|
|
251
|
+
Then('该项名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
|
|
252
|
+
console.log(` 📍 Step: 验证名称为 "${expectedName}"...`);
|
|
253
|
+
|
|
254
|
+
await this.page.waitForTimeout(1000);
|
|
255
|
+
const renamedItem = this.page.getByText(expectedName, { exact: true }).first();
|
|
256
|
+
await expect(renamedItem).toBeVisible({ timeout: 5000 });
|
|
257
|
+
|
|
258
|
+
console.log(` ✅ 名称已更新为 "${expectedName}"`);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
Then('Agent 应该显示置顶图标', async function (this: CustomWorld) {
|
|
262
|
+
console.log(' 📍 Step: 验证显示置顶图标...');
|
|
263
|
+
|
|
264
|
+
await this.page.waitForTimeout(500);
|
|
265
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
266
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
267
|
+
await expect(pinIcon).toBeVisible({ timeout: 5000 });
|
|
268
|
+
|
|
269
|
+
console.log(' ✅ 置顶图标已显示');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
Then('Agent 不应该显示置顶图标', async function (this: CustomWorld) {
|
|
273
|
+
console.log(' 📍 Step: 验证不显示置顶图标...');
|
|
274
|
+
|
|
275
|
+
await this.page.waitForTimeout(500);
|
|
276
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
277
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
278
|
+
await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
|
|
279
|
+
|
|
280
|
+
console.log(' ✅ 置顶图标未显示');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
|
|
284
|
+
console.log(' 📍 Step: 验证 Agent 已移除...');
|
|
285
|
+
|
|
286
|
+
await this.page.waitForTimeout(500);
|
|
287
|
+
|
|
288
|
+
if (this.testContext.targetItemId) {
|
|
289
|
+
const deletedItem = this.page.locator(`a[aria-label="${this.testContext.targetItemId}"]`);
|
|
290
|
+
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log(' ✅ Agent 已从列表中移除');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ============================================
|
|
297
|
+
// Helper Functions
|
|
298
|
+
// ============================================
|
|
299
|
+
|
|
300
|
+
async function inputNewName(
|
|
301
|
+
this: CustomWorld,
|
|
302
|
+
newName: string,
|
|
303
|
+
pressEnter: boolean,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
await this.page.waitForTimeout(300);
|
|
306
|
+
|
|
307
|
+
// Try to find the popover input
|
|
308
|
+
const popoverInputSelectors = [
|
|
309
|
+
'.ant-popover-inner input',
|
|
310
|
+
'.ant-popover-content input',
|
|
311
|
+
'.ant-popover input',
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
let renameInput = null;
|
|
315
|
+
|
|
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
|
+
break;
|
|
322
|
+
} catch {
|
|
323
|
+
// Try next selector
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!renameInput) {
|
|
328
|
+
// Fallback: find any visible input
|
|
329
|
+
const allInputs = this.page.locator('input:visible');
|
|
330
|
+
const count = await allInputs.count();
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < count; i++) {
|
|
333
|
+
const input = allInputs.nth(i);
|
|
334
|
+
const placeholder = (await input.getAttribute('placeholder').catch(() => '')) || '';
|
|
335
|
+
if (placeholder.includes('Search') || placeholder.includes('搜索')) continue;
|
|
336
|
+
|
|
337
|
+
const isInPopover = await input.evaluate((el) => {
|
|
338
|
+
return el.closest('.ant-popover') !== null || el.closest('[class*="popover"]') !== null;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (isInPopover || count <= 2) {
|
|
342
|
+
renameInput = input;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (renameInput) {
|
|
349
|
+
await renameInput.click();
|
|
350
|
+
await renameInput.clear();
|
|
351
|
+
await renameInput.fill(newName);
|
|
352
|
+
|
|
353
|
+
if (pressEnter) {
|
|
354
|
+
await renameInput.press('Enter');
|
|
355
|
+
} else {
|
|
356
|
+
await this.page.click('body', { position: { x: 10, y: 10 } });
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// Keyboard fallback
|
|
360
|
+
await this.page.keyboard.press('Meta+A');
|
|
361
|
+
await this.page.waitForTimeout(50);
|
|
362
|
+
await this.page.keyboard.type(newName, { delay: 20 });
|
|
363
|
+
|
|
364
|
+
if (pressEnter) {
|
|
365
|
+
await this.page.keyboard.press('Enter');
|
|
366
|
+
} else {
|
|
367
|
+
await this.page.click('body', { position: { x: 10, y: 10 } });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await this.page.waitForTimeout(1000);
|
|
372
|
+
console.log(` ✅ 已输入新名称 "${newName}"`);
|
|
373
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Sidebar Agent Group Steps
|
|
3
|
+
*
|
|
4
|
+
* Step definitions for Home page Agent Group management E2E tests
|
|
5
|
+
* - Rename
|
|
6
|
+
* - Pin/Unpin
|
|
7
|
+
* - Delete
|
|
8
|
+
*/
|
|
9
|
+
import { Given, Then, When } from '@cucumber/cucumber';
|
|
10
|
+
import { expect } from '@playwright/test';
|
|
11
|
+
|
|
12
|
+
import { TEST_USER } from '../../support/seedTestUser';
|
|
13
|
+
import { CustomWorld } from '../../support/world';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a test chat group directly in database
|
|
17
|
+
*/
|
|
18
|
+
async function createTestGroup(title: string = 'Test Group'): Promise<string> {
|
|
19
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
20
|
+
if (!databaseUrl) throw new Error('DATABASE_URL not set');
|
|
21
|
+
|
|
22
|
+
const { default: pg } = await import('pg');
|
|
23
|
+
const client = new pg.Client({ connectionString: databaseUrl });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await client.connect();
|
|
27
|
+
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
const groupId = `group_e2e_test_${Date.now()}`;
|
|
30
|
+
|
|
31
|
+
await client.query(
|
|
32
|
+
`INSERT INTO chat_groups (id, title, user_id, created_at, updated_at)
|
|
33
|
+
VALUES ($1, $2, $3, $4, $4)
|
|
34
|
+
ON CONFLICT DO NOTHING`,
|
|
35
|
+
[groupId, title, TEST_USER.id, now],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
console.log(` 📍 Created test group in DB: ${groupId}`);
|
|
39
|
+
return groupId;
|
|
40
|
+
} finally {
|
|
41
|
+
await client.end();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// Given Steps
|
|
47
|
+
// ============================================
|
|
48
|
+
|
|
49
|
+
Given('用户在 Home 页面有一个 Agent Group', async function (this: CustomWorld) {
|
|
50
|
+
console.log(' 📍 Step: 在数据库中创建测试 Agent Group...');
|
|
51
|
+
const groupId = await createTestGroup('E2E Test Group');
|
|
52
|
+
this.testContext.createdGroupId = groupId;
|
|
53
|
+
|
|
54
|
+
console.log(' 📍 Step: 导航到 Home 页面...');
|
|
55
|
+
await this.page.goto('/');
|
|
56
|
+
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
|
57
|
+
await this.page.waitForTimeout(1000);
|
|
58
|
+
|
|
59
|
+
console.log(' 📍 Step: 查找新创建的 Agent Group...');
|
|
60
|
+
const groupItem = this.page.locator(`a[href="/group/${groupId}"]`).first();
|
|
61
|
+
await expect(groupItem).toBeVisible({ timeout: 10_000 });
|
|
62
|
+
|
|
63
|
+
const groupLabel = await groupItem.getAttribute('aria-label');
|
|
64
|
+
this.testContext.targetItemId = groupLabel || groupId;
|
|
65
|
+
this.testContext.targetItemSelector = `a[href="/group/${groupId}"]`;
|
|
66
|
+
this.testContext.targetType = 'group';
|
|
67
|
+
|
|
68
|
+
console.log(` ✅ 找到 Agent Group: ${groupLabel}, id: ${groupId}`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
Given('该 Agent Group 未被置顶', async function (this: CustomWorld) {
|
|
72
|
+
console.log(' 📍 Step: 检查 Agent Group 未被置顶...');
|
|
73
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
74
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
75
|
+
|
|
76
|
+
if ((await pinIcon.count()) > 0) {
|
|
77
|
+
await targetItem.click({ button: 'right' });
|
|
78
|
+
await this.page.waitForTimeout(300);
|
|
79
|
+
const unpinOption = this.page.getByRole('menuitem', { name: /取消置顶|Unpin/i });
|
|
80
|
+
if ((await unpinOption.count()) > 0) {
|
|
81
|
+
await unpinOption.click();
|
|
82
|
+
await this.page.waitForTimeout(500);
|
|
83
|
+
}
|
|
84
|
+
await this.page.click('body', { position: { x: 10, y: 10 } });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(' ✅ Agent Group 未被置顶');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
|
|
91
|
+
console.log(' 📍 Step: 确保 Agent Group 已被置顶...');
|
|
92
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
93
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
94
|
+
|
|
95
|
+
if ((await pinIcon.count()) === 0) {
|
|
96
|
+
await targetItem.click({ button: 'right' });
|
|
97
|
+
await this.page.waitForTimeout(300);
|
|
98
|
+
const pinOption = this.page.getByRole('menuitem', { name: /置顶|Pin/i });
|
|
99
|
+
if ((await pinOption.count()) > 0) {
|
|
100
|
+
await pinOption.click();
|
|
101
|
+
await this.page.waitForTimeout(500);
|
|
102
|
+
}
|
|
103
|
+
await this.page.click('body', { position: { x: 10, y: 10 } });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(' ✅ Agent Group 已被置顶');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ============================================
|
|
110
|
+
// When Steps
|
|
111
|
+
// ============================================
|
|
112
|
+
|
|
113
|
+
When('用户右键点击该 Agent Group', async function (this: CustomWorld) {
|
|
114
|
+
console.log(' 📍 Step: 右键点击 Agent Group...');
|
|
115
|
+
|
|
116
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
117
|
+
await targetItem.click({ button: 'right' });
|
|
118
|
+
await this.page.waitForTimeout(500);
|
|
119
|
+
|
|
120
|
+
console.log(' ✅ 已右键点击 Agent Group');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
When('用户悬停在该 Agent Group 上', async function (this: CustomWorld) {
|
|
124
|
+
console.log(' 📍 Step: 悬停在 Agent Group 上...');
|
|
125
|
+
|
|
126
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
127
|
+
await targetItem.hover();
|
|
128
|
+
await this.page.waitForTimeout(500);
|
|
129
|
+
|
|
130
|
+
console.log(' ✅ 已悬停在 Agent Group 上');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ============================================
|
|
134
|
+
// Then Steps
|
|
135
|
+
// ============================================
|
|
136
|
+
|
|
137
|
+
Then('Agent Group 应该显示置顶图标', async function (this: CustomWorld) {
|
|
138
|
+
console.log(' 📍 Step: 验证显示置顶图标...');
|
|
139
|
+
|
|
140
|
+
await this.page.waitForTimeout(500);
|
|
141
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
142
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
143
|
+
await expect(pinIcon).toBeVisible({ timeout: 5000 });
|
|
144
|
+
|
|
145
|
+
console.log(' ✅ 置顶图标已显示');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
Then('Agent Group 不应该显示置顶图标', async function (this: CustomWorld) {
|
|
149
|
+
console.log(' 📍 Step: 验证不显示置顶图标...');
|
|
150
|
+
|
|
151
|
+
await this.page.waitForTimeout(500);
|
|
152
|
+
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
|
|
153
|
+
const pinIcon = targetItem.locator('svg.lucide-pin');
|
|
154
|
+
await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
|
|
155
|
+
|
|
156
|
+
console.log(' ✅ 置顶图标未显示');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
Then('Agent Group 应该从列表中移除', async function (this: CustomWorld) {
|
|
160
|
+
console.log(' 📍 Step: 验证 Agent Group 已移除...');
|
|
161
|
+
|
|
162
|
+
await this.page.waitForTimeout(500);
|
|
163
|
+
|
|
164
|
+
const deletedItem = this.page.locator(this.testContext.targetItemSelector);
|
|
165
|
+
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
|
|
166
|
+
|
|
167
|
+
console.log(' ✅ Agent Group 已从列表中移除');
|
|
168
|
+
});
|
package/e2e/src/steps/hooks.ts
CHANGED
|
@@ -84,6 +84,7 @@ Before(async function (this: CustomWorld, { pickle }) {
|
|
|
84
84
|
(tag) =>
|
|
85
85
|
tag.name.startsWith('@COMMUNITY-') ||
|
|
86
86
|
tag.name.startsWith('@AGENT-') ||
|
|
87
|
+
tag.name.startsWith('@HOME-') ||
|
|
87
88
|
tag.name.startsWith('@ROUTES-'),
|
|
88
89
|
);
|
|
89
90
|
console.log(`\n📝 Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
|
|
@@ -104,6 +105,7 @@ After(async function (this: CustomWorld, { pickle, result }) {
|
|
|
104
105
|
(tag) =>
|
|
105
106
|
tag.name.startsWith('@COMMUNITY-') ||
|
|
106
107
|
tag.name.startsWith('@AGENT-') ||
|
|
108
|
+
tag.name.startsWith('@HOME-') ||
|
|
107
109
|
tag.name.startsWith('@ROUTES-'),
|
|
108
110
|
)
|
|
109
111
|
?.name.replace('@', '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.267",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -359,7 +359,7 @@
|
|
|
359
359
|
"zustand-utils": "^2.1.1"
|
|
360
360
|
},
|
|
361
361
|
"devDependencies": {
|
|
362
|
-
"@ast-grep/napi": "^0.40.
|
|
362
|
+
"@ast-grep/napi": "^0.40.5",
|
|
363
363
|
"@commitlint/cli": "^19.8.1",
|
|
364
364
|
"@edge-runtime/vm": "^5.0.0",
|
|
365
365
|
"@huggingface/tasks": "^0.19.74",
|
|
@@ -418,7 +418,7 @@
|
|
|
418
418
|
"eslint-plugin-mdx": "^3.6.2",
|
|
419
419
|
"fake-indexeddb": "^6.2.5",
|
|
420
420
|
"fs-extra": "^11.3.3",
|
|
421
|
-
"glob": "^
|
|
421
|
+
"glob": "^13.0.0",
|
|
422
422
|
"happy-dom": "^20.0.11",
|
|
423
423
|
"husky": "^9.1.7",
|
|
424
424
|
"import-in-the-middle": "^2.0.1",
|