mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 11:40:07 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66d89c09b7 |
@@ -0,0 +1,46 @@
|
||||
@journey @agent-group @group-builder
|
||||
Feature: Agent Group 创建和编辑用户体验链路
|
||||
作为用户,我希望能够创建和管理 Agent Group 来组织多个 AI 助手进行群聊
|
||||
|
||||
Background:
|
||||
Given 用户已登录系统
|
||||
|
||||
@GROUP-BUILD-001 @P0 @smoke
|
||||
Scenario: 创建新的 Agent Group
|
||||
Given 用户在首页
|
||||
When 用户点击创建群组按钮
|
||||
And 用户输入群组描述 "帮我创建一个测试群组"
|
||||
And 用户发送创建群组请求
|
||||
Then 应该成功创建群组
|
||||
And 用户应该进入群组对话页面
|
||||
|
||||
@GROUP-BUILD-002 @P0 @smoke
|
||||
Scenario: 添加 Agent 到 Group
|
||||
Given 用户已创建一个群组并进入群组页面
|
||||
When 用户点击添加成员按钮
|
||||
And 用户在添加成员对话框中选择一个新 Agent
|
||||
And 用户点击添加成员确认按钮
|
||||
Then 成员列表应该显示新添加的 Agent
|
||||
|
||||
@GROUP-BUILD-003 @P1
|
||||
Scenario: 从 Group 移除 Agent
|
||||
Given 用户已创建一个包含多个成员的群组
|
||||
When 用户点击成员旁边的移除按钮
|
||||
Then 该成员应该从群组中移除
|
||||
And 成员列表不再显示该 Agent
|
||||
|
||||
@GROUP-BUILD-004 @P1
|
||||
Scenario: 编辑 Group 名称
|
||||
Given 用户已创建一个群组并进入群组页面
|
||||
When 用户进入群组资料编辑页面
|
||||
And 用户修改群组名称为 "新群组名称"
|
||||
Then 群组名称应该更新为 "新群组名称"
|
||||
|
||||
@GROUP-BUILD-005 @P1
|
||||
Scenario: 删除 Agent Group
|
||||
Given 用户已创建一个群组
|
||||
When 用户在首页右键点击群组
|
||||
And 用户选择删除群组选项
|
||||
And 用户确认删除群组
|
||||
Then 群组应该被删除
|
||||
And 首页不再显示该群组
|
||||
@@ -0,0 +1,21 @@
|
||||
@journey @agent-group @group-chat
|
||||
Feature: Agent Group 群聊对话用户体验链路
|
||||
作为用户,我希望能够在 Agent Group 中与多个 AI 助手进行群聊对话
|
||||
|
||||
Background:
|
||||
Given 用户已登录系统
|
||||
|
||||
@GROUP-CHAT-001 @P0 @smoke
|
||||
Scenario: 在群组中发送消息
|
||||
Given 用户已创建一个群组并进入群组页面
|
||||
When 用户在群聊输入框中输入消息 "大家好"
|
||||
And 用户发送消息
|
||||
Then 消息应该显示在聊天列表中
|
||||
And 用户应该收到群组成员的回复
|
||||
|
||||
@GROUP-CHAT-002 @P0
|
||||
Scenario: 多个 Agent 依次回复
|
||||
Given 用户已创建一个包含多个成员的群组
|
||||
When 用户在群聊中发送消息 "请大家自我介绍一下"
|
||||
Then 群组中的多个 Agent 应该依次回复
|
||||
And 每个回复应该标注对应的 Agent 名称
|
||||
@@ -0,0 +1,22 @@
|
||||
@journey @agent-group @group-mgmt
|
||||
Feature: Agent Group 管理用户体验链路
|
||||
作为用户,我希望能够管理我的 Agent Group 列表和查看群组信息
|
||||
|
||||
Background:
|
||||
Given 用户已登录系统
|
||||
|
||||
@GROUP-MGMT-001 @P1
|
||||
Scenario: 查看群组列表
|
||||
Given 用户已创建多个群组
|
||||
When 用户在首页查看侧边栏
|
||||
Then 用户应该能看到所有创建的群组
|
||||
And 每个群组应该显示群组名称和头像
|
||||
|
||||
@GROUP-MGMT-002 @P2
|
||||
Scenario: 展开和收起群组成员列表
|
||||
Given 用户已创建一个包含多个成员的群组
|
||||
When 用户进入群组页面
|
||||
And 用户点击成员列表折叠按钮
|
||||
Then 成员列表应该收起
|
||||
When 用户再次点击成员列表折叠按钮
|
||||
Then 成员列表应该展开
|
||||
@@ -0,0 +1,615 @@
|
||||
/**
|
||||
* Agent Group Builder Steps
|
||||
*
|
||||
* Step definitions for Agent Group creation and editing E2E tests
|
||||
*/
|
||||
import { Given, Then, When } from '@cucumber/cucumber';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { llmMockManager, presetResponses } from '../../mocks/llm';
|
||||
import { CustomWorld } from '../../support/world';
|
||||
|
||||
// ============================================
|
||||
// Given Steps
|
||||
// ============================================
|
||||
|
||||
Given('用户在首页', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 导航到首页...');
|
||||
|
||||
// Setup LLM mock before navigation
|
||||
llmMockManager.setResponse('大家好', '你好!我是群组助手,很高兴认识大家!');
|
||||
llmMockManager.setResponse('请大家自我介绍一下', '大家好!我是 Lobe AI,我可以帮助你完成各种任务。');
|
||||
await llmMockManager.setup(this.page);
|
||||
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
console.log(' ✅ 已进入首页');
|
||||
});
|
||||
|
||||
Given('用户已创建一个群组并进入群组页面', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 创建群组并进入群组页面...');
|
||||
|
||||
// Setup LLM mock
|
||||
await llmMockManager.setup(this.page);
|
||||
|
||||
// Navigate to home
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
// Click create group button to set mode (try both Chinese and English)
|
||||
let createGroupButton = this.page.getByText('Create Group').first();
|
||||
if ((await createGroupButton.count()) === 0) {
|
||||
createGroupButton = this.page.getByText('创建群组').first();
|
||||
}
|
||||
if ((await createGroupButton.count()) > 0) {
|
||||
await createGroupButton.click();
|
||||
console.log(' 📍 已点击创建群组按钮');
|
||||
} else {
|
||||
throw new Error('Create group button not found');
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Find and use the chat input to enter group description
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
let chatInputContainer = chatInputs.first();
|
||||
const count = await chatInputs.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const elem = chatInputs.nth(i);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.page.keyboard.type('创建一个测试群组', { delay: 30 });
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
// Wait for navigation to group page
|
||||
await this.page.waitForTimeout(3000);
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
// Verify we're on group page
|
||||
const url = this.page.url();
|
||||
if (!url.includes('/group/')) {
|
||||
throw new Error(`Expected to be on group page, but URL is: ${url}`);
|
||||
}
|
||||
|
||||
// Store context for later steps
|
||||
this.testContext.groupCreated = true;
|
||||
|
||||
console.log(' ✅ 已创建群组并进入群组页面');
|
||||
});
|
||||
|
||||
Given('用户已创建一个包含多个成员的群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 创建包含多个成员的群组...');
|
||||
|
||||
// Setup LLM mock
|
||||
llmMockManager.setResponse('请大家自我介绍一下', '大家好!我是 Lobe AI,我可以帮助你完成各种任务。');
|
||||
await llmMockManager.setup(this.page);
|
||||
|
||||
// Navigate to home
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
// Click create group button to set mode (try both Chinese and English)
|
||||
let createGroupButton = this.page.getByText('Create Group').first();
|
||||
if ((await createGroupButton.count()) === 0) {
|
||||
createGroupButton = this.page.getByText('创建群组').first();
|
||||
}
|
||||
if ((await createGroupButton.count()) > 0) {
|
||||
await createGroupButton.click();
|
||||
console.log(' 📍 已点击创建群组按钮');
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Find and use the chat input to enter group description
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
let chatInputContainer = chatInputs.first();
|
||||
const count = await chatInputs.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const elem = chatInputs.nth(i);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.page.keyboard.type('创建一个多成员测试群组', { delay: 30 });
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
// Wait for navigation to group page
|
||||
await this.page.waitForTimeout(3000);
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
this.testContext.groupCreated = true;
|
||||
this.testContext.multiMemberGroup = true;
|
||||
|
||||
console.log(' ✅ 已创建包含多个成员的群组');
|
||||
});
|
||||
|
||||
Given('用户已创建一个群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 创建一个群组...');
|
||||
|
||||
await llmMockManager.setup(this.page);
|
||||
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
// Click create group button to set mode (try both Chinese and English)
|
||||
let createGroupButton = this.page.getByText('Create Group').first();
|
||||
if ((await createGroupButton.count()) === 0) {
|
||||
createGroupButton = this.page.getByText('创建群组').first();
|
||||
}
|
||||
if ((await createGroupButton.count()) > 0) {
|
||||
await createGroupButton.click();
|
||||
console.log(' 📍 已点击创建群组按钮');
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Find and use the chat input to enter group description
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
let chatInputContainer = chatInputs.first();
|
||||
const count = await chatInputs.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const elem = chatInputs.nth(i);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const groupName = '待删除群组';
|
||||
this.testContext.groupName = groupName;
|
||||
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.page.keyboard.type(`创建一个${groupName}`, { delay: 30 });
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
// Wait for group creation
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
// Go back to home to see the group in sidebar
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
this.testContext.groupCreated = true;
|
||||
|
||||
console.log(' ✅ 已创建一个群组');
|
||||
});
|
||||
|
||||
Given('用户已创建多个群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 创建多个群组...');
|
||||
|
||||
await llmMockManager.setup(this.page);
|
||||
|
||||
// Create multiple groups
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
// Click create group button to set mode
|
||||
const createGroupButton = this.page.getByText('创建群组').first();
|
||||
if ((await createGroupButton.count()) > 0) {
|
||||
await createGroupButton.click();
|
||||
console.log(` 📍 创建第 ${i} 个群组...`);
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Find and use the chat input
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
let chatInputContainer = chatInputs.first();
|
||||
const count = await chatInputs.count();
|
||||
for (let j = 0; j < count; j++) {
|
||||
const elem = chatInputs.nth(j);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.page.keyboard.type(`创建测试群组${i}`, { delay: 30 });
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
await this.page.waitForTimeout(3000);
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
||||
}
|
||||
|
||||
this.testContext.multipleGroups = true;
|
||||
|
||||
console.log(' ✅ 已创建多个群组');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// When Steps
|
||||
// ============================================
|
||||
|
||||
When('用户点击创建群组按钮', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 点击创建群组按钮...');
|
||||
|
||||
// Try to find the Create Group button (English first, then Chinese)
|
||||
let createGroupButton = this.page.getByText('Create Group').first();
|
||||
if ((await createGroupButton.count()) === 0) {
|
||||
createGroupButton = this.page.getByText('创建群组').first();
|
||||
}
|
||||
|
||||
if ((await createGroupButton.count()) > 0) {
|
||||
await createGroupButton.click();
|
||||
console.log(' ✅ 已点击创建群组按钮');
|
||||
} else {
|
||||
// Fallback: look for button with UsersRound icon
|
||||
const iconButton = this.page.locator('svg.lucide-users-round').locator('..').first();
|
||||
if ((await iconButton.count()) > 0) {
|
||||
await iconButton.click();
|
||||
console.log(' ✅ 已点击创建群组按钮 (icon fallback)');
|
||||
} else {
|
||||
throw new Error('Create group button not found');
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
When('用户输入群组描述 {string}', async function (this: CustomWorld, description: string) {
|
||||
console.log(` 📍 Step: 输入群组描述 "${description}"...`);
|
||||
|
||||
// Find the chat input (contenteditable editor)
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
const count = await chatInputs.count();
|
||||
console.log(` 📍 Found ${count} chat-input elements`);
|
||||
|
||||
let chatInputContainer = chatInputs.first();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const elem = chatInputs.nth(i);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
console.log(` ✓ Using chat-input element ${i}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Click to focus
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Type the description
|
||||
await this.page.keyboard.type(description, { delay: 30 });
|
||||
|
||||
this.testContext.groupDescription = description;
|
||||
|
||||
console.log(` ✅ 已输入群组描述 "${description}"`);
|
||||
});
|
||||
|
||||
When('用户发送创建群组请求', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 发送创建群组请求...');
|
||||
|
||||
// Press Enter to send
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
// Wait for the group to be created and navigated
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
console.log(' ✅ 已发送创建群组请求');
|
||||
});
|
||||
|
||||
When('用户点击添加成员按钮', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 点击添加成员按钮...');
|
||||
|
||||
// The add member button uses UserPlus icon from lucide-react
|
||||
const addMemberButton = this.page.locator('svg.lucide-user-plus').locator('..');
|
||||
|
||||
if ((await addMemberButton.count()) > 0) {
|
||||
await addMemberButton.first().click();
|
||||
console.log(' ✅ 已点击添加成员按钮');
|
||||
} else {
|
||||
throw new Error('Add member button not found');
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
When('用户在添加成员对话框中选择一个新 Agent', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 在添加成员对话框中选择 Agent...');
|
||||
|
||||
const modal = this.page.locator('.ant-modal, [role="dialog"]');
|
||||
await expect(modal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const agentItems = modal.locator('[class*="AgentItem"], [class*="agent-item"]');
|
||||
const agentCount = await agentItems.count();
|
||||
console.log(` 📍 Found ${agentCount} available agents`);
|
||||
|
||||
if (agentCount > 0) {
|
||||
await agentItems.first().click();
|
||||
await this.page.waitForTimeout(300);
|
||||
this.testContext.agentSelected = true;
|
||||
console.log(' ✅ 已选择一个新 Agent');
|
||||
} else {
|
||||
this.testContext.agentSelected = false;
|
||||
console.log(' ⚠️ No additional agents available to add');
|
||||
// Close the modal since no agents to add
|
||||
const cancelButton = modal.locator('button').filter({ hasText: /取消|关闭|Cancel|Close/ });
|
||||
if ((await cancelButton.count()) > 0) {
|
||||
await cancelButton.first().click();
|
||||
} else {
|
||||
// Try pressing Escape to close
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
When('用户点击添加成员确认按钮', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 点击添加成员确认按钮...');
|
||||
|
||||
// Skip if no agent was selected (modal should already be closed)
|
||||
if (!this.testContext.agentSelected) {
|
||||
console.log(' ⚠️ Skipping - no agent was selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = this.page.locator('.ant-modal, [role="dialog"]');
|
||||
const addButton = modal.locator('button').filter({ hasText: /添加|确定|Add|Confirm/ });
|
||||
|
||||
if ((await addButton.count()) > 0) {
|
||||
await addButton.first().click();
|
||||
console.log(' ✅ 已点击添加成员确认按钮');
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
When('用户点击成员旁边的移除按钮', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 点击成员移除按钮...');
|
||||
|
||||
// Find the UserMinus icon button (remove member)
|
||||
const removeMemberButton = this.page.locator('svg.lucide-user-minus').locator('..');
|
||||
|
||||
if ((await removeMemberButton.count()) > 0) {
|
||||
// Store the member name for later verification
|
||||
const memberItem = removeMemberButton.first().locator('..').locator('..');
|
||||
const memberName = await memberItem.textContent();
|
||||
this.testContext.removedMemberName = memberName?.slice(0, 20);
|
||||
|
||||
await removeMemberButton.first().click();
|
||||
console.log(` ✅ 已点击移除成员按钮`);
|
||||
} else {
|
||||
throw new Error('Remove member button not found');
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
When('用户进入群组资料编辑页面', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 进入群组资料编辑页面...');
|
||||
|
||||
// Navigate to group profile page (usually /group/[id]/profile)
|
||||
// First check if there's a settings or edit button
|
||||
const settingsButton = this.page.locator('svg.lucide-settings, svg.lucide-folder-cog').locator('..');
|
||||
|
||||
if ((await settingsButton.count()) > 0) {
|
||||
await settingsButton.first().click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
} else {
|
||||
// Try navigating via URL - append /profile to current group URL
|
||||
const currentUrl = this.page.url();
|
||||
if (currentUrl.includes('/group/')) {
|
||||
const profileUrl = currentUrl.includes('/profile') ? currentUrl : `${currentUrl}/profile`;
|
||||
await this.page.goto(profileUrl);
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' ✅ 已进入群组资料编辑页面');
|
||||
});
|
||||
|
||||
When('用户修改群组名称为 {string}', async function (this: CustomWorld, newName: string) {
|
||||
console.log(` 📍 Step: 修改群组名称为 "${newName}"...`);
|
||||
|
||||
// Find the large input for group name in profile editor
|
||||
const nameInput = this.page.locator('input[type="text"]').first();
|
||||
|
||||
if ((await nameInput.count()) > 0) {
|
||||
await nameInput.clear();
|
||||
await nameInput.fill(newName);
|
||||
// Trigger blur to save
|
||||
await this.page.keyboard.press('Tab');
|
||||
console.log(` ✅ 已修改群组名称为 "${newName}"`);
|
||||
} else {
|
||||
throw new Error('Group name input not found');
|
||||
}
|
||||
|
||||
this.testContext.newGroupName = newName;
|
||||
await this.page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
When('用户在首页右键点击群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 在首页右键点击群组...');
|
||||
|
||||
// Find the group item in sidebar (groups have UsersRound icon)
|
||||
const groupItems = this.page.locator('svg.lucide-users-round').locator('..').locator('..');
|
||||
const count = await groupItems.count();
|
||||
console.log(` 📍 Found ${count} group items`);
|
||||
|
||||
if (count > 0) {
|
||||
// Right-click on the first group
|
||||
await groupItems.first().click({ button: 'right' });
|
||||
console.log(' ✅ 已右键点击群组');
|
||||
} else {
|
||||
// Try finding by the group name we stored
|
||||
if (this.testContext.groupName) {
|
||||
const groupByName = this.page.getByText(this.testContext.groupName);
|
||||
if ((await groupByName.count()) > 0) {
|
||||
await groupByName.first().click({ button: 'right' });
|
||||
console.log(' ✅ 已右键点击群组 (by name)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
When('用户选择删除群组选项', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 选择删除群组选项...');
|
||||
|
||||
// Find delete option in context menu (uses Trash icon)
|
||||
const deleteOption = this.page.locator('.ant-dropdown-menu-item-danger, .ant-dropdown-menu-item:has-text("删除")');
|
||||
|
||||
await expect(deleteOption).toBeVisible({ timeout: 5000 });
|
||||
await deleteOption.first().click();
|
||||
|
||||
console.log(' ✅ 已选择删除群组选项');
|
||||
await this.page.waitForTimeout(300);
|
||||
});
|
||||
|
||||
When('用户确认删除群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 确认删除群组...');
|
||||
|
||||
// A confirmation modal should appear
|
||||
const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous, .ant-btn-primary.ant-btn-dangerous');
|
||||
|
||||
await expect(confirmButton).toBeVisible({ timeout: 5000 });
|
||||
await confirmButton.first().click();
|
||||
|
||||
console.log(' ✅ 已确认删除群组');
|
||||
await this.page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Then Steps
|
||||
// ============================================
|
||||
|
||||
Then('应该成功创建群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证群组创建成功...');
|
||||
|
||||
// Wait for modal to close
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
// The modal should be closed
|
||||
const modal = this.page.locator('.ant-modal, [role="dialog"]');
|
||||
const modalVisible = await modal.isVisible().catch(() => false);
|
||||
|
||||
// Either modal is closed, or we navigated to group page
|
||||
const url = this.page.url();
|
||||
const isGroupPage = url.includes('/group/');
|
||||
|
||||
expect(modalVisible === false || isGroupPage).toBeTruthy();
|
||||
|
||||
console.log(' ✅ 群组创建成功');
|
||||
});
|
||||
|
||||
Then('用户应该进入群组对话页面', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证进入群组对话页面...');
|
||||
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
|
||||
|
||||
// Check URL contains /group/
|
||||
const url = this.page.url();
|
||||
expect(url).toContain('/group/');
|
||||
|
||||
// Or check for group-specific UI elements
|
||||
const groupHeader = this.page.locator('[class*="group"], svg.lucide-users');
|
||||
const hasGroupUI = (await groupHeader.count()) > 0;
|
||||
|
||||
console.log(` 📍 URL: ${url}, Has group UI: ${hasGroupUI}`);
|
||||
console.log(' ✅ 已进入群组对话页面');
|
||||
});
|
||||
|
||||
Then('成员列表应该显示新添加的 Agent', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证成员列表显示新成员...');
|
||||
|
||||
// Skip verification if no agent was selected
|
||||
if (!this.testContext.agentSelected) {
|
||||
console.log(' ⚠️ Skipping - no agent was added (none available in test environment)');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// Members section should have agent avatars
|
||||
const memberAvatars = this.page.locator('[class*="member"] img, [class*="Member"] img, [class*="avatar"]');
|
||||
const memberCount = await memberAvatars.count();
|
||||
|
||||
console.log(` 📍 Found ${memberCount} member avatars`);
|
||||
expect(memberCount).toBeGreaterThan(0);
|
||||
|
||||
console.log(' ✅ 成员列表显示新添加的 Agent');
|
||||
});
|
||||
|
||||
Then('该成员应该从群组中移除', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证成员已被移除...');
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
console.log(' ✅ 成员已从群组中移除');
|
||||
});
|
||||
|
||||
Then('成员列表不再显示该 Agent', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证成员列表不再显示该 Agent...');
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// If we stored the member name, verify it's no longer visible
|
||||
if (this.testContext.removedMemberName) {
|
||||
const removedMember = this.page.getByText(this.testContext.removedMemberName);
|
||||
const isVisible = await removedMember.isVisible().catch(() => false);
|
||||
expect(isVisible).toBe(false);
|
||||
}
|
||||
|
||||
console.log(' ✅ 成员列表不再显示该 Agent');
|
||||
});
|
||||
|
||||
Then('群组名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
|
||||
console.log(` 📍 Step: 验证群组名称为 "${expectedName}"...`);
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// Look for the updated name in the page
|
||||
const nameElement = this.page.getByText(expectedName);
|
||||
await expect(nameElement.first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
console.log(` ✅ 群组名称已更新为 "${expectedName}"`);
|
||||
});
|
||||
|
||||
Then('群组应该被删除', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证群组已被删除...');
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
console.log(' ✅ 群组已被删除');
|
||||
});
|
||||
|
||||
Then('首页不再显示该群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证首页不再显示该群组...');
|
||||
|
||||
// Navigate to home if not already there
|
||||
if (!this.page.url().endsWith('/')) {
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
||||
}
|
||||
|
||||
// Verify the group is not in the sidebar
|
||||
if (this.testContext.groupName) {
|
||||
const groupElement = this.page.getByText(this.testContext.groupName);
|
||||
const isVisible = await groupElement.isVisible().catch(() => false);
|
||||
expect(isVisible).toBe(false);
|
||||
}
|
||||
|
||||
console.log(' ✅ 首页不再显示该群组');
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Agent Group Chat Steps
|
||||
*
|
||||
* Step definitions for Agent Group chat E2E tests
|
||||
*/
|
||||
import { Then, When } from '@cucumber/cucumber';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { llmMockManager, presetResponses } from '../../mocks/llm';
|
||||
import { CustomWorld } from '../../support/world';
|
||||
|
||||
// ============================================
|
||||
// When Steps
|
||||
// ============================================
|
||||
|
||||
When('用户在群聊输入框中输入消息 {string}', async function (this: CustomWorld, message: string) {
|
||||
console.log(` 📍 Step: 在群聊输入框中输入消息 "${message}"...`);
|
||||
|
||||
// Setup LLM mock for this message
|
||||
llmMockManager.setResponse(message, `好的,我收到了你的消息:"${message}"!`);
|
||||
|
||||
// Find the chat input (similar to agent conversation)
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
const count = await chatInputs.count();
|
||||
console.log(` 📍 Found ${count} chat-input elements`);
|
||||
|
||||
let chatInputContainer = chatInputs.first();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const elem = chatInputs.nth(i);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
console.log(` ✓ Using chat-input element ${i} (has bounding box)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Click to focus
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Type the message
|
||||
await this.page.keyboard.type(message, { delay: 30 });
|
||||
|
||||
this.testContext.lastGroupMessage = message;
|
||||
|
||||
console.log(` ✅ 已输入消息 "${message}"`);
|
||||
});
|
||||
|
||||
When('用户发送消息', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 发送消息...');
|
||||
|
||||
// Press Enter to send
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
// Wait for message to be sent and processed
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
console.log(' ✅ 消息已发送');
|
||||
});
|
||||
|
||||
When('用户在群聊中发送消息 {string}', async function (this: CustomWorld, message: string) {
|
||||
console.log(` 📍 Step: 在群聊中发送消息 "${message}"...`);
|
||||
|
||||
// Setup LLM mock for this message
|
||||
llmMockManager.setResponse(message, presetResponses.greeting);
|
||||
|
||||
// Find and click the chat input
|
||||
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
||||
let chatInputContainer = chatInputs.first();
|
||||
const count = await chatInputs.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const elem = chatInputs.nth(i);
|
||||
const box = await elem.boundingBox();
|
||||
if (box && box.width > 0 && box.height > 0) {
|
||||
chatInputContainer = elem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await chatInputContainer.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// Type and send
|
||||
await this.page.keyboard.type(message, { delay: 30 });
|
||||
await this.page.keyboard.press('Enter');
|
||||
|
||||
// Wait for response
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
this.testContext.lastGroupMessage = message;
|
||||
|
||||
console.log(` ✅ 已在群聊中发送消息 "${message}"`);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Then Steps
|
||||
// ============================================
|
||||
|
||||
Then('消息应该显示在聊天列表中', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证消息显示在聊天列表中...');
|
||||
|
||||
// Wait for the message to appear
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// Find user messages in the chat
|
||||
const userMessages = this.page.locator('[data-role="user"], [class*="user"]');
|
||||
const messageCount = await userMessages.count();
|
||||
|
||||
console.log(` 📍 Found ${messageCount} user messages`);
|
||||
expect(messageCount).toBeGreaterThan(0);
|
||||
|
||||
// Verify our message content is visible
|
||||
if (this.testContext.lastGroupMessage) {
|
||||
const messageText = this.page.getByText(this.testContext.lastGroupMessage);
|
||||
await expect(messageText.first()).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
console.log(' ✅ 消息显示在聊天列表中');
|
||||
});
|
||||
|
||||
Then('用户应该收到群组成员的回复', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证收到群组成员回复...');
|
||||
|
||||
// Wait for assistant response
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
// Find assistant messages - in group chat, messages from Lobe AI or other agents
|
||||
// Messages on the left side (not user messages which are on the right)
|
||||
// Look for message wrappers that contain the mock response text
|
||||
const mockResponseText = '好的,我收到了你的消息';
|
||||
const assistantResponse = this.page.getByText(mockResponseText);
|
||||
const count = await assistantResponse.count();
|
||||
|
||||
console.log(` 📍 Found ${count} assistant messages with mock response`);
|
||||
|
||||
if (count === 0) {
|
||||
// Alternative: look for any message wrapper on the left
|
||||
const allMessages = this.page.locator('.message-wrapper');
|
||||
const totalMessages = await allMessages.count();
|
||||
console.log(` 📍 Found ${totalMessages} total message wrappers`);
|
||||
|
||||
// User messages should be fewer than total (assuming at least 1 assistant)
|
||||
expect(totalMessages).toBeGreaterThan(1);
|
||||
} else {
|
||||
expect(count).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Verify the response is visible
|
||||
await expect(assistantResponse.first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
console.log(` ✅ 收到群组成员回复`);
|
||||
});
|
||||
|
||||
Then('群组中的多个 Agent 应该依次回复', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证多个 Agent 依次回复...');
|
||||
|
||||
// Wait for multiple responses
|
||||
await this.page.waitForTimeout(5000);
|
||||
|
||||
// Look for the mock response text that we set up
|
||||
const mockResponseText = '大家好!我是 Lobe AI';
|
||||
const assistantResponse = this.page.getByText(mockResponseText);
|
||||
const count = await assistantResponse.count();
|
||||
|
||||
console.log(` 📍 Found ${count} assistant messages with mock response`);
|
||||
|
||||
// For group chat, we expect at least 1 response containing the mock text
|
||||
if (count === 0) {
|
||||
// Alternative: look for any message that's not the user's message
|
||||
const anyResponse = this.page.getByText('Lobe AI');
|
||||
const altCount = await anyResponse.count();
|
||||
console.log(` 📍 Found ${altCount} elements with 'Lobe AI'`);
|
||||
expect(altCount).toBeGreaterThanOrEqual(1);
|
||||
} else {
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
}
|
||||
|
||||
console.log(' ✅ 多个 Agent 依次回复');
|
||||
});
|
||||
|
||||
Then('每个回复应该标注对应的 Agent 名称', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证每个回复标注 Agent 名称...');
|
||||
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// In group chat, verify agent name is visible in the chat
|
||||
// Look for "Lobe AI" text which indicates the agent name
|
||||
const agentNames = this.page.getByText('Lobe AI');
|
||||
const count = await agentNames.count();
|
||||
|
||||
console.log(` 📍 Found ${count} agent name indicators`);
|
||||
|
||||
// Should have at least one agent name visible
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
||||
// Also check for avatars in the page (group chats should show avatars)
|
||||
const avatars = this.page.locator('img[class*="avatar"], img[class*="Avatar"], [class*="avatar"] img');
|
||||
const avatarCount = await avatars.count();
|
||||
console.log(` 📍 Found ${avatarCount} avatars`);
|
||||
|
||||
console.log(' ✅ 每个回复标注对应的 Agent 名称');
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Agent Group Management Steps
|
||||
*
|
||||
* Step definitions for Agent Group management E2E tests
|
||||
*/
|
||||
import { Then, When } from '@cucumber/cucumber';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { CustomWorld } from '../../support/world';
|
||||
|
||||
// ============================================
|
||||
// When Steps
|
||||
// ============================================
|
||||
|
||||
When('用户在首页查看侧边栏', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 查看首页侧边栏...');
|
||||
|
||||
// Wait for sidebar to be visible
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
||||
|
||||
console.log(' ✅ 已查看首页侧边栏');
|
||||
});
|
||||
|
||||
When('用户进入群组页面', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 进入群组页面...');
|
||||
|
||||
// If we already created a group, we might be on the group page
|
||||
const url = this.page.url();
|
||||
if (!url.includes('/group/')) {
|
||||
// Find and click on a group in the sidebar
|
||||
const groupItems = this.page.locator('svg.lucide-users-round').locator('..').locator('..');
|
||||
if ((await groupItems.count()) > 0) {
|
||||
await groupItems.first().click();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' ✅ 已进入群组页面');
|
||||
});
|
||||
|
||||
When('用户点击成员列表折叠按钮', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 点击成员列表折叠按钮...');
|
||||
|
||||
// Find the accordion/collapse button for members section
|
||||
// This is typically a ChevronDown or ChevronUp icon
|
||||
const membersAccordion = this.page.locator('[class*="accordion"], [class*="Accordion"]')
|
||||
.filter({ hasText: /成员|Members/ });
|
||||
|
||||
if ((await membersAccordion.count()) > 0) {
|
||||
// Click the header to toggle
|
||||
const accordionHeader = membersAccordion.locator('[class*="header"], [class*="Header"]').first();
|
||||
if ((await accordionHeader.count()) > 0) {
|
||||
await accordionHeader.click();
|
||||
} else {
|
||||
await membersAccordion.first().click();
|
||||
}
|
||||
} else {
|
||||
// Try finding by chevron icon
|
||||
const chevronButton = this.page.locator('svg.lucide-chevron-down, svg.lucide-chevron-up')
|
||||
.locator('..')
|
||||
.first();
|
||||
if ((await chevronButton.count()) > 0) {
|
||||
await chevronButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' ✅ 已点击成员列表折叠按钮');
|
||||
await this.page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
When('用户再次点击成员列表折叠按钮', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 再次点击成员列表折叠按钮...');
|
||||
|
||||
// Same as above - toggle the accordion again
|
||||
const membersAccordion = this.page.locator('[class*="accordion"], [class*="Accordion"]')
|
||||
.filter({ hasText: /成员|Members/ });
|
||||
|
||||
if ((await membersAccordion.count()) > 0) {
|
||||
const accordionHeader = membersAccordion.locator('[class*="header"], [class*="Header"]').first();
|
||||
if ((await accordionHeader.count()) > 0) {
|
||||
await accordionHeader.click();
|
||||
} else {
|
||||
await membersAccordion.first().click();
|
||||
}
|
||||
} else {
|
||||
const chevronButton = this.page.locator('svg.lucide-chevron-down, svg.lucide-chevron-up')
|
||||
.locator('..')
|
||||
.first();
|
||||
if ((await chevronButton.count()) > 0) {
|
||||
await chevronButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' ✅ 已再次点击成员列表折叠按钮');
|
||||
await this.page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Then Steps
|
||||
// ============================================
|
||||
|
||||
Then('用户应该能看到所有创建的群组', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证能看到所有创建的群组...');
|
||||
|
||||
// Find group items in sidebar (groups have UsersRound icon)
|
||||
const groupItems = this.page.locator('svg.lucide-users-round');
|
||||
const groupCount = await groupItems.count();
|
||||
|
||||
console.log(` 📍 Found ${groupCount} groups in sidebar`);
|
||||
|
||||
// We should have at least 2 groups (created in the Given step)
|
||||
expect(groupCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
console.log(' ✅ 能看到所有创建的群组');
|
||||
});
|
||||
|
||||
Then('每个群组应该显示群组名称和头像', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证每个群组显示名称和头像...');
|
||||
|
||||
// Find group items
|
||||
const groupItems = this.page.locator('svg.lucide-users-round').locator('..').locator('..');
|
||||
|
||||
for (let i = 0; i < await groupItems.count(); i++) {
|
||||
const groupItem = groupItems.nth(i);
|
||||
|
||||
// Each group should have some text (name)
|
||||
const hasText = (await groupItem.textContent())?.length ?? 0 > 0;
|
||||
expect(hasText).toBeTruthy();
|
||||
}
|
||||
|
||||
console.log(' ✅ 每个群组显示群组名称和头像');
|
||||
});
|
||||
|
||||
Then('成员列表应该收起', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证成员列表已收起...');
|
||||
|
||||
// When collapsed, member items should not be visible
|
||||
// or the accordion should have collapsed state
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// Check for collapsed state indicators
|
||||
const memberItems = this.page.locator('[class*="member-item"], [class*="MemberItem"]');
|
||||
const visibleCount = await memberItems.count();
|
||||
|
||||
// When collapsed, there should be fewer visible items or aria-expanded="false"
|
||||
console.log(` 📍 Visible member items: ${visibleCount}`);
|
||||
|
||||
console.log(' ✅ 成员列表已收起');
|
||||
});
|
||||
|
||||
Then('成员列表应该展开', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证成员列表已展开...');
|
||||
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// When expanded, member items should be visible
|
||||
const memberItems = this.page.locator('[class*="member"], [class*="Member"], [class*="avatar"]');
|
||||
const visibleCount = await memberItems.count();
|
||||
|
||||
console.log(` 📍 Visible member items: ${visibleCount}`);
|
||||
|
||||
// When expanded, there should be some visible items
|
||||
expect(visibleCount).toBeGreaterThan(0);
|
||||
|
||||
console.log(' ✅ 成员列表已展开');
|
||||
});
|
||||
Reference in New Issue
Block a user