mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
b4b1205ee9
* ♻️ refactor(modal): migrate confirm modals to @lobehub/ui/base-ui Replace all `App.useApp().modal.confirm`, `Modal.confirm` and `AntModal.confirm` call sites with the headless `confirmModal` from `@lobehub/ui/base-ui`, dropping antd-only props (`centered`, `type`, `width`, `okButtonProps.type='primary'`, `okButtonProps.loading`, `classNames.root`) that the base-ui imperative API does not accept. - 82 files touched; `modal.confirm`/`Modal.confirm` call sites now zero - `PageEditor/store/action.ts`: drop `modal` arg from `handleDelete` - `ResourceManager/useUploadFolder`: replace dynamic `import('antd').Modal` - `Eval/DatasetsTab`: migrate `modal.success` to `confirmModal` Part of LOBE-9645 Phase 1. * ♻️ refactor(ui): migrate select/modal call sites to @lobehub/ui/base-ui - Convert imperative-modal factories (createXxxModal + Content split) for apikey, creds (Create/Edit/View), provider (CreateNewProvider), and messenger LinkModal. - Switch Select usages to base-ui Select (Messenger AgentSelect, provider sdkType). - Restructure CreateNewProvider form to vertical layout with manual section titles for tighter spacing; drop FormModal/Form group nesting. - Standardize small ActionIcon sizing via DESKTOP_HEADER_ICON_SMALL_SIZE (WideScreenButton, ToggleRightPanelButton, ContextDropdown, AddNewProvider). - Fix missing title on ResourceManager delete confirm modal so the header (title + close X) renders. - Update react skill and AGENTS.md to require base-ui priority over root @lobehub/ui / antd; expand component table and Common Mistakes with explicit base-ui rules. * ♻️ refactor(ui): swap antd Select to base-ui Select and migrate createStyles to createStaticStyles * ✅ test: update test mocks for base-ui confirmModal migration * ✅ test(e2e): switch delete confirm selector to base-ui dialog role
643 lines
23 KiB
TypeScript
643 lines
23 KiB
TypeScript
/**
|
|
* Agent Conversation Management Steps
|
|
*
|
|
* Step definitions for Agent conversation management E2E tests
|
|
* - Create new conversation
|
|
* - Switch conversations
|
|
* - Rename conversation
|
|
* - Delete conversation
|
|
* - Search conversations
|
|
*/
|
|
import { Given, Then, When } from '@cucumber/cucumber';
|
|
import { expect } from '@playwright/test';
|
|
|
|
import type { CustomWorld } from '../../support/world';
|
|
|
|
// ============================================
|
|
// Given Steps
|
|
// ============================================
|
|
|
|
Given('用户已有一个对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 创建一个对话...');
|
|
|
|
// Send a message to create a conversation
|
|
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
|
const count = await chatInputs.count();
|
|
|
|
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;
|
|
break;
|
|
}
|
|
}
|
|
|
|
await chatInputContainer.click();
|
|
await this.page.waitForTimeout(300);
|
|
await this.page.keyboard.type('hello', { delay: 30 });
|
|
await this.page.keyboard.press('Enter');
|
|
|
|
// Wait for response
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Store the current conversation title for later reference
|
|
const topicItems = this.page.locator('.ant-menu-item, [class*="NavItem"]');
|
|
const topicCount = await topicItems.count();
|
|
console.log(` 📍 Found ${topicCount} topic items after creating conversation`);
|
|
|
|
console.log(' ✅ 已创建一个对话');
|
|
});
|
|
|
|
Given('用户有多个对话历史', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 创建多个对话...');
|
|
|
|
// Create first conversation
|
|
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;
|
|
}
|
|
}
|
|
|
|
// First conversation - use "测试" content for search test
|
|
await chatInputContainer.click();
|
|
await this.page.waitForTimeout(300);
|
|
await this.page.keyboard.type('测试对话内容', { delay: 30 });
|
|
await this.page.keyboard.press('Enter');
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Store first conversation reference
|
|
this.testContext.firstConversation = 'first';
|
|
|
|
// Create new topic and second conversation
|
|
console.log(' 📍 Creating second conversation...');
|
|
const addTopicButton = this.page.locator('svg.lucide-message-square-plus').locator('..');
|
|
if ((await addTopicButton.count()) > 0) {
|
|
await addTopicButton.first().click();
|
|
await this.page.waitForTimeout(1000);
|
|
|
|
// Send message in second conversation - different content
|
|
await chatInputContainer.click();
|
|
await this.page.waitForTimeout(300);
|
|
await this.page.keyboard.type('hello world', { delay: 30 });
|
|
await this.page.keyboard.press('Enter');
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
console.log(' ✅ 已创建多个对话');
|
|
});
|
|
|
|
// ============================================
|
|
// When Steps
|
|
// ============================================
|
|
|
|
When('用户点击新建对话按钮', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 点击新建对话按钮...');
|
|
|
|
// The add topic button uses MessageSquarePlusIcon from lucide-react
|
|
const addTopicButton = this.page.locator('svg.lucide-message-square-plus').locator('..');
|
|
|
|
if ((await addTopicButton.count()) > 0) {
|
|
await addTopicButton.first().click();
|
|
console.log(' ✅ 已点击新建对话按钮');
|
|
} else {
|
|
// Fallback: look for button with "新建" or "add" in title
|
|
const addButton = this.page.locator('button[title*="新建"], button[title*="add"]');
|
|
if ((await addButton.count()) > 0) {
|
|
await addButton.first().click();
|
|
console.log(' ✅ 已点击新建对话按钮 (fallback)');
|
|
} else {
|
|
throw new Error('New topic button not found');
|
|
}
|
|
}
|
|
|
|
await this.page.waitForTimeout(500);
|
|
});
|
|
|
|
When('用户点击另一个对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 点击另一个对话...');
|
|
|
|
// Check if we're on the home page (has Recent Topics section)
|
|
const recentTopicsSection = this.page.locator('text=Recent Topics');
|
|
const isOnHomePage = (await recentTopicsSection.count()) > 0;
|
|
console.log(` 📍 Is on home page: ${isOnHomePage}`);
|
|
|
|
if (isOnHomePage) {
|
|
// Click the second topic card in Recent Topics section
|
|
// Cards are wrapped in Link components and contain "Hello! I am a mock AI" text from the mock
|
|
const recentTopicCards = this.page.locator('a[href*="topic="]');
|
|
const cardCount = await recentTopicCards.count();
|
|
console.log(` 📍 Found ${cardCount} recent topic cards (by href)`);
|
|
|
|
if (cardCount >= 2) {
|
|
// Click the second card (different from current topic)
|
|
await recentTopicCards.nth(1).click();
|
|
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话');
|
|
await this.page.waitForTimeout(2000);
|
|
return;
|
|
}
|
|
|
|
// Fallback: try to find by text content
|
|
const topicTextCards = this.page.locator('text=Hello! I am a mock AI');
|
|
const textCardCount = await topicTextCards.count();
|
|
console.log(` 📍 Found ${textCardCount} topic cards by text`);
|
|
|
|
if (textCardCount >= 2) {
|
|
await topicTextCards.nth(1).click();
|
|
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话 (by text)');
|
|
await this.page.waitForTimeout(2000);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback: try to find topic items in the sidebar
|
|
const sidebarTopics = this.page.locator('[data-testid="topic-item"]');
|
|
const topicCount = await sidebarTopics.count();
|
|
console.log(` 📍 Found ${topicCount} topic items`);
|
|
|
|
// Click the second topic (first one is current/active)
|
|
if (topicCount >= 2) {
|
|
await sidebarTopics.nth(1).click();
|
|
console.log(' ✅ 已点击另一个对话');
|
|
} else {
|
|
throw new Error('Not enough topics to switch');
|
|
}
|
|
|
|
await this.page.waitForTimeout(500);
|
|
});
|
|
|
|
When('用户右键点击对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 右键点击对话...');
|
|
|
|
const sidebarTopics = this.page.locator('[data-testid="topic-item"]');
|
|
const topicCount = await sidebarTopics.count();
|
|
console.log(` 📍 Found ${topicCount} topic items`);
|
|
|
|
if (topicCount > 0) {
|
|
await sidebarTopics.first().click({ button: 'right' });
|
|
console.log(' ✅ 已右键点击对话');
|
|
} else {
|
|
throw new Error('No topics found to right-click');
|
|
}
|
|
|
|
await this.page.waitForTimeout(500);
|
|
});
|
|
|
|
When('用户右键点击一个对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 右键点击一个对话...');
|
|
|
|
const sidebarTopics = this.page.locator('[data-testid="topic-item"]');
|
|
const topicCount = await sidebarTopics.count();
|
|
console.log(` 📍 Found ${topicCount} topic items`);
|
|
|
|
// Store the topic text for later verification
|
|
if (topicCount > 0) {
|
|
const topicText = await sidebarTopics.first().textContent();
|
|
this.testContext.deletedTopicTitle = topicText?.slice(0, 30);
|
|
await sidebarTopics.first().click({ button: 'right' });
|
|
console.log(` ✅ 已右键点击对话: "${topicText?.slice(0, 30)}..."`);
|
|
} else {
|
|
throw new Error('No topics found to right-click');
|
|
}
|
|
|
|
await this.page.waitForTimeout(500);
|
|
});
|
|
|
|
When('用户选择重命名选项', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 选择重命名选项...');
|
|
|
|
// First, close any open context menu by clicking elsewhere
|
|
await this.page.click('body', { position: { x: 500, y: 300 } });
|
|
await this.page.waitForTimeout(300);
|
|
|
|
// Instead of using right-click context menu, use the "..." dropdown menu
|
|
// which appears when hovering over a topic item
|
|
const topicItems = this.page.locator('[data-testid="topic-item"]');
|
|
const topicCount = await topicItems.count();
|
|
console.log(` 📍 Found ${topicCount} topic items`);
|
|
|
|
if (topicCount > 0) {
|
|
// Hover on the first topic to reveal the "..." action button
|
|
const firstTopic = topicItems.first();
|
|
await firstTopic.hover();
|
|
console.log(' 📍 Hovering on topic item...');
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// The "..." button should now be visible INSIDE the topic item
|
|
// Important: we must find the icon WITHIN the hovered topic, not the global one
|
|
// The topic item has a specific structure with nav-item-actions
|
|
const moreButtonInTopic = firstTopic.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal');
|
|
const moreButtonCount = await moreButtonInTopic.count();
|
|
console.log(` 📍 Found ${moreButtonCount} more buttons inside topic`);
|
|
|
|
if (moreButtonCount > 0) {
|
|
// Click the "..." button to open dropdown menu
|
|
await moreButtonInTopic.first().click();
|
|
console.log(' 📍 Clicked ... button inside topic');
|
|
await this.page.waitForTimeout(500);
|
|
} else {
|
|
// Fallback: try to find it by looking at the actions container
|
|
console.log(' 📍 Trying alternative: looking for actions container...');
|
|
|
|
// Debug: print the topic item HTML structure
|
|
const topicHTML = await firstTopic.evaluate((el) => el.outerHTML.slice(0, 500));
|
|
console.log(` 📍 Topic HTML: ${topicHTML}`);
|
|
|
|
// The actions might be in a sibling or parent element
|
|
// Try finding any ellipsis icon that's near the topic
|
|
const allEllipsis = this.page.locator('svg.lucide-ellipsis');
|
|
const ellipsisCount = await allEllipsis.count();
|
|
console.log(` 📍 Total ellipsis icons on page: ${ellipsisCount}`);
|
|
|
|
// Skip the first one (which is the global topic list menu)
|
|
// and click the second one (which should be in the topic item)
|
|
if (ellipsisCount > 1) {
|
|
await allEllipsis.nth(1).click();
|
|
console.log(' 📍 Clicked second ellipsis icon');
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now find the rename option in the dropdown menu
|
|
const renameOption = this.page.getByRole('menuitem', { exact: true, name: /^(Rename|重命名)$/ });
|
|
|
|
await expect(renameOption).toBeVisible({ timeout: 5000 });
|
|
console.log(' 📍 Found rename menu item');
|
|
|
|
// Click the rename option
|
|
await renameOption.click();
|
|
console.log(' 📍 Clicked rename menu item');
|
|
|
|
// Wait for the popover/input to appear
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// Check if input appeared
|
|
const inputCount = await this.page.locator('input').count();
|
|
console.log(` 📍 After click: ${inputCount} inputs on page`);
|
|
|
|
console.log(' ✅ 已选择重命名选项');
|
|
});
|
|
|
|
When('用户输入新的对话名称 {string}', async function (this: CustomWorld, newName: string) {
|
|
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
|
|
|
|
// Debug: check what's on the page
|
|
const debugInfo = await this.page.evaluate(() => {
|
|
const allInputs = document.querySelectorAll('input');
|
|
const allPopovers = document.querySelectorAll('[class*="popover"], .ant-popover');
|
|
const focusedElement = document.activeElement;
|
|
return {
|
|
focusedClass: focusedElement?.className,
|
|
focusedTag: focusedElement?.tagName,
|
|
inputCount: allInputs.length,
|
|
inputTags: Array.from(allInputs).map((i) => ({
|
|
className: i.className,
|
|
placeholder: i.placeholder,
|
|
type: i.type,
|
|
visible: i.offsetParent !== null,
|
|
})),
|
|
popoverCount: allPopovers.length,
|
|
};
|
|
});
|
|
console.log(' 📍 Debug info:', JSON.stringify(debugInfo, null, 2));
|
|
|
|
// Wait a short moment for the popover to render
|
|
await this.page.waitForTimeout(300);
|
|
|
|
// The rename UI can render as a dialog/modal in CI, not only as a popover.
|
|
const renameInputSelectors = [
|
|
'[role="dialog"] input[type="text"]',
|
|
'.ant-modal input[type="text"]',
|
|
'[data-testid="editing-popover"] input',
|
|
'.ant-popover-inner input',
|
|
'.ant-popover-content input',
|
|
'.ant-popover input',
|
|
'input[type="text"]:visible',
|
|
];
|
|
|
|
let renameInput = null;
|
|
|
|
// Wait for any rename input to appear
|
|
for (const selector of renameInputSelectors) {
|
|
try {
|
|
const locator = this.page.locator(selector).first();
|
|
await locator.waitFor({ state: 'visible', timeout: 2000 });
|
|
renameInput = locator;
|
|
console.log(` 📍 Found input with selector: ${selector}`);
|
|
break;
|
|
} catch {
|
|
// Try next selector
|
|
}
|
|
}
|
|
|
|
if (!renameInput) {
|
|
// Fallback: find any visible input that's not the search or chat input
|
|
console.log(' 📍 Trying fallback: finding any visible input...');
|
|
const allInputs = this.page.locator('input:visible');
|
|
const count = await allInputs.count();
|
|
console.log(` 📍 Found ${count} visible inputs`);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const input = allInputs.nth(i);
|
|
const placeholder = await input.getAttribute('placeholder').catch(() => '');
|
|
const testId = await input.getAttribute('data-testid').catch(() => '');
|
|
|
|
// Skip search inputs and chat inputs
|
|
if (placeholder?.includes('Search') || placeholder?.includes('搜索')) continue;
|
|
if (testId === 'chat-input') continue;
|
|
|
|
// Prefer inputs rendered inside rename containers.
|
|
const isInRenameContainer = await input.evaluate((el) => {
|
|
return (
|
|
el.closest('[role="dialog"]') !== null ||
|
|
el.closest('.ant-modal') !== null ||
|
|
el.closest('.ant-popover') !== null ||
|
|
el.closest('[class*="popover"]') !== null
|
|
);
|
|
});
|
|
|
|
if (isInRenameContainer || count === 1) {
|
|
renameInput = input;
|
|
console.log(` 📍 Found candidate input at index ${i}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (renameInput) {
|
|
// Clear and fill the input
|
|
await renameInput.click();
|
|
await renameInput.clear();
|
|
await renameInput.fill(newName);
|
|
console.log(` 📍 Filled input with "${newName}"`);
|
|
|
|
const saveButton = this.page
|
|
.locator('[role="dialog"]')
|
|
.getByRole('button', { exact: true, name: /^(Save|保存)$/ })
|
|
.first();
|
|
|
|
try {
|
|
await saveButton.waitFor({ state: 'visible', timeout: 1000 });
|
|
await saveButton.click();
|
|
console.log(' 📍 Clicked save button');
|
|
} catch {
|
|
// Popover-based rename UIs still confirm with Enter.
|
|
await renameInput.press('Enter');
|
|
console.log(' 📍 Confirmed rename with Enter');
|
|
}
|
|
|
|
console.log(` ✅ 已输入新名称 "${newName}"`);
|
|
} else {
|
|
// Last resort: the input should have autoFocus, so keyboard should work
|
|
console.log(' ⚠️ Could not find rename input element, using keyboard fallback...');
|
|
// Select all and replace
|
|
await this.page.keyboard.press('Meta+A');
|
|
await this.page.waitForTimeout(50);
|
|
await this.page.keyboard.type(newName, { delay: 20 });
|
|
await this.page.keyboard.press('Enter');
|
|
console.log(` ✅ 已通过键盘输入新名称 "${newName}"`);
|
|
}
|
|
|
|
// Wait for the rename to be saved
|
|
await this.page.waitForTimeout(1000);
|
|
});
|
|
|
|
When('用户选择删除选项', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 选择删除选项...');
|
|
|
|
// The context menu should be visible with "delete" option
|
|
// Support both English and Chinese
|
|
const deleteOption = this.page.getByRole('menuitem', { exact: true, name: /^(Delete|删除)$/ });
|
|
|
|
await expect(deleteOption).toBeVisible({ timeout: 5000 });
|
|
await deleteOption.click();
|
|
|
|
console.log(' ✅ 已选择删除选项');
|
|
await this.page.waitForTimeout(300);
|
|
});
|
|
|
|
When('用户确认删除', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 确认删除...');
|
|
|
|
const confirmButton = this.page
|
|
.getByRole('dialog')
|
|
.getByRole('button', { name: /^(ok|delete|删除|确认|确定)$/i });
|
|
|
|
await expect(confirmButton).toBeVisible({ timeout: 5000 });
|
|
await confirmButton.click();
|
|
|
|
console.log(' ✅ 已确认删除');
|
|
await this.page.waitForTimeout(500);
|
|
});
|
|
|
|
When('用户在搜索框中输入 {string}', async function (this: CustomWorld, searchText: string) {
|
|
console.log(` 📍 Step: 在搜索框中输入 "${searchText}"...`);
|
|
|
|
// Find the search input in the sidebar
|
|
// Support both English and Chinese placeholders
|
|
const searchInput = this.page.locator(
|
|
'input[placeholder*="Search"], input[placeholder*="搜索"], [data-testid="search-input"]',
|
|
);
|
|
|
|
if ((await searchInput.count()) > 0) {
|
|
await searchInput.first().click();
|
|
await searchInput.first().fill(searchText);
|
|
} else {
|
|
// Fallback: click on search icon to reveal search input
|
|
const searchIcon = this.page.locator('svg.lucide-search').locator('..');
|
|
if ((await searchIcon.count()) > 0) {
|
|
await searchIcon.first().click();
|
|
await this.page.waitForTimeout(300);
|
|
// Now find the input
|
|
const input = this.page.locator('input[type="text"]').last();
|
|
await input.fill(searchText);
|
|
}
|
|
}
|
|
|
|
console.log(` ✅ 已输入搜索内容 "${searchText}"`);
|
|
await this.page.waitForTimeout(500);
|
|
});
|
|
|
|
// ============================================
|
|
// Then Steps
|
|
// ============================================
|
|
|
|
Then('应该创建一个新的空白对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证新对话已创建...');
|
|
|
|
// The chat area should be empty or show welcome message
|
|
// Check that there are no user/assistant messages
|
|
const userMessages = this.page.locator('[data-role="user"]');
|
|
const assistantMessages = this.page.locator('[data-role="assistant"]');
|
|
|
|
const userCount = await userMessages.count();
|
|
const assistantCount = await assistantMessages.count();
|
|
|
|
console.log(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
|
|
|
|
// New conversation should have no messages
|
|
expect(userCount).toBe(0);
|
|
expect(assistantCount).toBe(0);
|
|
|
|
console.log(' ✅ 新对话已创建');
|
|
});
|
|
|
|
Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证页面显示欢迎界面...');
|
|
|
|
// Wait for the page to update
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// New conversation typically shows a welcome/empty state
|
|
// Check for visible chat input (there may be 2 - desktop and mobile, find the visible one)
|
|
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
|
const count = await chatInputs.count();
|
|
|
|
let foundVisible = false;
|
|
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) {
|
|
foundVisible = true;
|
|
console.log(` 📍 Found visible chat-input at index ${i}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Just verify the page is loaded properly by checking URL or any content
|
|
if (!foundVisible) {
|
|
// Fallback: just verify we're still on the chat page
|
|
const currentUrl = this.page.url();
|
|
expect(currentUrl).toContain('/chat');
|
|
console.log(' 📍 Fallback: verified we are on chat page');
|
|
}
|
|
|
|
console.log(' ✅ 欢迎界面已显示');
|
|
});
|
|
|
|
Then('应该切换到该对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证已切换对话...');
|
|
|
|
// The URL or active state should change
|
|
// For now, just verify the page is responsive
|
|
await this.page.waitForTimeout(500);
|
|
|
|
console.log(' ✅ 已切换到该对话');
|
|
});
|
|
|
|
Then('显示该对话的历史消息', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证显示历史消息...');
|
|
|
|
// Wait for the loading to finish - the messages need time to load after switching topics
|
|
console.log(' 📍 等待消息加载...');
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Wait for the message wrapper to appear (ChatItem component uses message-wrapper class)
|
|
const messageSelector = '.message-wrapper';
|
|
try {
|
|
await this.page.waitForSelector(messageSelector, { timeout: 10_000 });
|
|
} catch {
|
|
console.log(' ⚠️ 等待消息选择器超时,尝试备用选择器...');
|
|
}
|
|
|
|
// There should be messages in the chat area
|
|
const messages = this.page.locator(messageSelector);
|
|
const messageCount = await messages.count();
|
|
|
|
console.log(` 📍 找到 ${messageCount} 条消息`);
|
|
|
|
// At least some messages should be visible
|
|
expect(messageCount).toBeGreaterThan(0);
|
|
|
|
console.log(' ✅ 历史消息已显示');
|
|
});
|
|
|
|
Then('对话名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
|
|
console.log(` 📍 Step: 验证对话名称为 "${expectedName}"...`);
|
|
|
|
// Wait for the rename to take effect
|
|
await this.page.waitForTimeout(1000);
|
|
|
|
// Find the topic with the new name by text content
|
|
// Topics are in the sidebar, look for text directly
|
|
// Use .first() since the name might appear in multiple places (sidebar + favorites section)
|
|
const renamedTopic = this.page.getByText(expectedName, { exact: true }).first();
|
|
|
|
await expect(renamedTopic).toBeVisible({ timeout: 5000 });
|
|
|
|
console.log(` ✅ 对话名称已更新为 "${expectedName}"`);
|
|
});
|
|
|
|
Then('该对话应该被删除', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证对话已删除...');
|
|
|
|
// Wait for deletion to take effect
|
|
await this.page.waitForTimeout(500);
|
|
|
|
console.log(' ✅ 对话已删除');
|
|
});
|
|
|
|
Then('对话列表中不再显示该对话', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证对话列表中不再显示该对话...');
|
|
|
|
// Wait for UI to update
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// The deleted topic should not be in the list
|
|
if (this.testContext.deletedTopicTitle) {
|
|
const deletedTopic = this.page.locator(
|
|
`[class*="NavItem"]:has-text("${this.testContext.deletedTopicTitle}")`,
|
|
);
|
|
const count = await deletedTopic.count();
|
|
expect(count).toBe(0);
|
|
console.log(` ✅ 对话 "${this.testContext.deletedTopicTitle}" 已从列表中移除`);
|
|
} else {
|
|
console.log(' ✅ 对话已从列表中移除');
|
|
}
|
|
});
|
|
|
|
Then('应该显示包含 {string} 的对话', async function (this: CustomWorld, searchText: string) {
|
|
console.log(` 📍 Step: 验证搜索结果包含 "${searchText}"...`);
|
|
|
|
// Wait for search results to load (search opens a modal dialog)
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Search results appear in a modal/dialog, not in sidebar
|
|
// Look for the search modal and check for matching results
|
|
const searchModal = this.page.locator('.ant-modal, [role="dialog"]');
|
|
const hasModal = (await searchModal.count()) > 0;
|
|
console.log(` 📍 搜索模态框: ${hasModal}`);
|
|
|
|
// Find matching items in the search results (either in modal or in sidebar if filtered)
|
|
const matchingInModal = searchModal.getByText(searchText);
|
|
const matchingInPage = this.page.getByText(searchText);
|
|
|
|
const modalMatchCount = await matchingInModal.count();
|
|
const pageMatchCount = await matchingInPage.count();
|
|
|
|
console.log(` 📍 模态框中找到 ${modalMatchCount} 个匹配, 页面中找到 ${pageMatchCount} 个匹配`);
|
|
|
|
// At least one match should be found (either in search input or results)
|
|
expect(modalMatchCount + pageMatchCount).toBeGreaterThan(0);
|
|
|
|
console.log(` ✅ 搜索结果显示包含 "${searchText}" 的对话`);
|
|
});
|
|
|
|
Then('不相关的对话应该被过滤', async function (this: CustomWorld) {
|
|
console.log(' 📍 Step: 验证不相关对话已被过滤...');
|
|
|
|
// This would require checking that non-matching topics are hidden
|
|
// For now, just verify the search is active
|
|
await this.page.waitForTimeout(300);
|
|
|
|
console.log(' ✅ 不相关对话已被过滤');
|
|
});
|