Compare commits

...

3 Commits

Author SHA1 Message Date
arvinxx 862f61f379 update info 2026-02-15 22:30:00 +08:00
arvinxx 8127c1b9e7 update 2026-02-15 22:14:23 +08:00
arvinxx 8bff2097e0 🐛 fix: scroll ChatInput into view when starter mode activates
When clicking Create Agent/Group/Write, the SuggestQuestions panel
renders below the ChatInput and pushes total content beyond the
viewport, causing the ChatInput to scroll out of view. This adds
scrollIntoView + focus on mode change so the editor stays visible
and ready for input. Also improves E2E test to target contenteditable
inside ChatInput directly and wait for animation to settle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:14:16 +08:00
13 changed files with 443 additions and 444 deletions
+85 -85
View File
@@ -11,14 +11,14 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// Given Steps
// ============================================
Given('用户已有一个对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建一个对话...');
console.info(' 📍 Step: 创建一个对话...');
// Send a message to create a conversation
const chatInputs = this.page.locator('[data-testid="chat-input"]');
@@ -45,13 +45,13 @@ Given('用户已有一个对话', async function (this: CustomWorld) {
// 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.info(` 📍 Found ${topicCount} topic items after creating conversation`);
console.log(' ✅ 已创建一个对话');
console.info(' ✅ 已创建一个对话');
});
Given('用户有多个对话历史', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建多个对话...');
console.info(' 📍 Step: 创建多个对话...');
// Create first conversation
const chatInputs = this.page.locator('[data-testid="chat-input"]');
@@ -77,7 +77,7 @@ Given('用户有多个对话历史', async function (this: CustomWorld) {
this.testContext.firstConversation = 'first';
// Create new topic and second conversation
console.log(' 📍 Creating second conversation...');
console.info(' 📍 Creating second conversation...');
const addTopicButton = this.page.locator('svg.lucide-message-square-plus').locator('..');
if ((await addTopicButton.count()) > 0) {
await addTopicButton.first().click();
@@ -91,7 +91,7 @@ Given('用户有多个对话历史', async function (this: CustomWorld) {
await this.page.waitForTimeout(2000);
}
console.log(' ✅ 已创建多个对话');
console.info(' ✅ 已创建多个对话');
});
// ============================================
@@ -99,20 +99,20 @@ Given('用户有多个对话历史', async function (this: CustomWorld) {
// ============================================
When('用户点击新建对话按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击新建对话按钮...');
console.info(' 📍 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(' ✅ 已点击新建对话按钮');
console.info(' ✅ 已点击新建对话按钮');
} 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)');
console.info(' ✅ 已点击新建对话按钮 (fallback)');
} else {
throw new Error('New topic button not found');
}
@@ -122,24 +122,24 @@ When('用户点击新建对话按钮', async function (this: CustomWorld) {
});
When('用户点击另一个对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击另一个对话...');
console.info(' 📍 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}`);
console.info(` 📍 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)`);
console.info(` 📍 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 中的另一个对话');
console.info(' ✅ 已点击首页 Recent Topics 中的另一个对话');
await this.page.waitForTimeout(2000);
return;
}
@@ -147,11 +147,11 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
// 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`);
console.info(` 📍 Found ${textCardCount} topic cards by text`);
if (textCardCount >= 2) {
await topicTextCards.nth(1).click();
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话 (by text)');
console.info(' ✅ 已点击首页 Recent Topics 中的另一个对话 (by text)');
await this.page.waitForTimeout(2000);
return;
}
@@ -161,18 +161,18 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
// Topics are displayed with star icons (lucide-star) in the left sidebar
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
let topicCount = await sidebarTopics.count();
console.log(` 📍 Found ${topicCount} topics with star icons`);
console.info(` 📍 Found ${topicCount} topics with star icons`);
// If not found by star, try finding by topic list structure
if (topicCount < 2) {
// Topics might be in a list container - look for items in sidebar with specific text
const topicItems = this.page.locator('[class*="nav-item"], [class*="NavItem"]');
topicCount = await topicItems.count();
console.log(` 📍 Found ${topicCount} nav items`);
console.info(` 📍 Found ${topicCount} nav items`);
if (topicCount >= 2) {
await topicItems.nth(1).click();
console.log(' ✅ 已点击另一个对话');
console.info(' ✅ 已点击另一个对话');
await this.page.waitForTimeout(500);
return;
}
@@ -181,7 +181,7 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
// Click the second topic (first one is current/active)
if (topicCount >= 2) {
await sidebarTopics.nth(1).click();
console.log(' ✅ 已点击另一个对话');
console.info(' ✅ 已点击另一个对话');
} else {
throw new Error('Not enough topics to switch');
}
@@ -190,17 +190,17 @@ When('用户点击另一个对话', async function (this: CustomWorld) {
});
When('用户右键点击对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击对话...');
console.info(' 📍 Step: 右键点击对话...');
// Find topic items by their star icon - each saved topic has a star
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
let topicCount = await sidebarTopics.count();
console.log(` 📍 Found ${topicCount} topics with star icons`);
const topicCount = await sidebarTopics.count();
console.info(` 📍 Found ${topicCount} topics with star icons`);
if (topicCount > 0) {
// Right-click the first saved topic
await sidebarTopics.first().click({ button: 'right' });
console.log(' ✅ 已右键点击对话');
console.info(' ✅ 已右键点击对话');
} else {
throw new Error('No topics found to right-click');
}
@@ -209,19 +209,19 @@ When('用户右键点击对话', async function (this: CustomWorld) {
});
When('用户右键点击一个对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击一个对话...');
console.info(' 📍 Step: 右键点击一个对话...');
// Find topic items by their star icon
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
let topicCount = await sidebarTopics.count();
console.log(` 📍 Found ${topicCount} topics with star icons`);
const topicCount = await sidebarTopics.count();
console.info(` 📍 Found ${topicCount} topics with star icons`);
// 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)}..."`);
console.info(` ✅ 已右键点击对话: "${topicText?.slice(0, 30)}..."`);
} else {
throw new Error('No topics found to right-click');
}
@@ -230,7 +230,7 @@ When('用户右键点击一个对话', async function (this: CustomWorld) {
});
When('用户选择重命名选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择重命名选项...');
console.info(' 📍 Step: 选择重命名选项...');
// First, close any open context menu by clicking elsewhere
await this.page.click('body', { position: { x: 500, y: 300 } });
@@ -240,46 +240,46 @@ When('用户选择重命名选项', async function (this: CustomWorld) {
// which appears when hovering over a topic item
const topicItems = this.page.locator('svg.lucide-star').locator('..').locator('..');
const topicCount = await topicItems.count();
console.log(` 📍 Found ${topicCount} topic items`);
console.info(` 📍 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...');
console.info(' 📍 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');
let moreButtonCount = await moreButtonInTopic.count();
console.log(` 📍 Found ${moreButtonCount} more buttons inside topic`);
const moreButtonCount = await moreButtonInTopic.count();
console.info(` 📍 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');
console.info(' 📍 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...');
console.info(' 📍 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}`);
console.info(` 📍 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}`);
console.info(` 📍 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');
console.info(' 📍 Clicked second ellipsis icon');
await this.page.waitForTimeout(500);
}
}
@@ -289,24 +289,24 @@ When('用户选择重命名选项', async function (this: CustomWorld) {
const renameOption = this.page.getByRole('menuitem', { exact: true, name: /^(Rename|重命名)$/ });
await expect(renameOption).toBeVisible({ timeout: 5000 });
console.log(' 📍 Found rename menu item');
console.info(' 📍 Found rename menu item');
// Click the rename option
await renameOption.click();
console.log(' 📍 Clicked rename menu item');
console.info(' 📍 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.info(` 📍 After click: ${inputCount} inputs on page`);
console.log(' ✅ 已选择重命名选项');
console.info(' ✅ 已选择重命名选项');
});
When('用户输入新的对话名称 {string}', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
console.info(` 📍 Step: 输入新名称 "${newName}"...`);
// Debug: check what's on the page
const debugInfo = await this.page.evaluate(() => {
@@ -326,7 +326,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
popoverCount: allPopovers.length,
};
});
console.log(' 📍 Debug info:', JSON.stringify(debugInfo, null, 2));
console.info(' 📍 Debug info:', JSON.stringify(debugInfo, null, 2));
// Wait a short moment for the popover to render
await this.page.waitForTimeout(300);
@@ -350,7 +350,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
const locator = this.page.locator(selector).first();
await locator.waitFor({ state: 'visible', timeout: 2000 });
renameInput = locator;
console.log(` 📍 Found input with selector: ${selector}`);
console.info(` 📍 Found input with selector: ${selector}`);
break;
} catch {
// Try next selector
@@ -359,10 +359,10 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
if (!renameInput) {
// Fallback: find any visible input that's not the search or chat input
console.log(' 📍 Trying fallback: finding any visible input...');
console.info(' 📍 Trying fallback: finding any visible input...');
const allInputs = this.page.locator('input:visible');
const count = await allInputs.count();
console.log(` 📍 Found ${count} visible inputs`);
console.info(` 📍 Found ${count} visible inputs`);
for (let i = 0; i < count; i++) {
const input = allInputs.nth(i);
@@ -380,7 +380,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
if (isInPopover || count === 1) {
renameInput = input;
console.log(` 📍 Found candidate input at index ${i}`);
console.info(` 📍 Found candidate input at index ${i}`);
break;
}
}
@@ -391,20 +391,20 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
await renameInput.click();
await renameInput.clear();
await renameInput.fill(newName);
console.log(` 📍 Filled input with "${newName}"`);
console.info(` 📍 Filled input with "${newName}"`);
// Press Enter to confirm
await renameInput.press('Enter');
console.log(` ✅ 已输入新名称 "${newName}"`);
console.info(` ✅ 已输入新名称 "${newName}"`);
} else {
// Last resort: the input should have autoFocus, so keyboard should work
console.log(' ⚠️ Could not find rename input element, using keyboard fallback...');
console.info(' ⚠️ 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}"`);
console.info(` ✅ 已通过键盘输入新名称 "${newName}"`);
}
// Wait for the rename to be saved
@@ -412,7 +412,7 @@ When('用户输入新的对话名称 {string}', async function (this: CustomWorl
});
When('用户选择删除选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择删除选项...');
console.info(' 📍 Step: 选择删除选项...');
// The context menu should be visible with "delete" option
// Support both English and Chinese
@@ -421,12 +421,12 @@ When('用户选择删除选项', async function (this: CustomWorld) {
await expect(deleteOption).toBeVisible({ timeout: 5000 });
await deleteOption.click();
console.log(' ✅ 已选择删除选项');
console.info(' ✅ 已选择删除选项');
await this.page.waitForTimeout(300);
});
When('用户确认删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 确认删除...');
console.info(' 📍 Step: 确认删除...');
// A confirmation modal should appear
const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous');
@@ -435,12 +435,12 @@ When('用户确认删除', async function (this: CustomWorld) {
await expect(confirmButton).toBeVisible({ timeout: 5000 });
await confirmButton.click();
console.log(' ✅ 已确认删除');
console.info(' ✅ 已确认删除');
await this.page.waitForTimeout(500);
});
When('用户在搜索框中输入 {string}', async function (this: CustomWorld, searchText: string) {
console.log(` 📍 Step: 在搜索框中输入 "${searchText}"...`);
console.info(` 📍 Step: 在搜索框中输入 "${searchText}"...`);
// Find the search input in the sidebar
// Support both English and Chinese placeholders
@@ -463,7 +463,7 @@ When('用户在搜索框中输入 {string}', async function (this: CustomWorld,
}
}
console.log(` ✅ 已输入搜索内容 "${searchText}"`);
console.info(` ✅ 已输入搜索内容 "${searchText}"`);
await this.page.waitForTimeout(500);
});
@@ -472,7 +472,7 @@ When('用户在搜索框中输入 {string}', async function (this: CustomWorld,
// ============================================
Then('应该创建一个新的空白对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证新对话已创建...');
console.info(' 📍 Step: 验证新对话已创建...');
// The chat area should be empty or show welcome message
// Check that there are no user/assistant messages
@@ -482,17 +482,17 @@ Then('应该创建一个新的空白对话', async function (this: CustomWorld)
const userCount = await userMessages.count();
const assistantCount = await assistantMessages.count();
console.log(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
console.info(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
// New conversation should have no messages
expect(userCount).toBe(0);
expect(assistantCount).toBe(0);
console.log(' ✅ 新对话已创建');
console.info(' ✅ 新对话已创建');
});
Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面显示欢迎界面...');
console.info(' 📍 Step: 验证页面显示欢迎界面...');
// Wait for the page to update
await this.page.waitForTimeout(500);
@@ -508,7 +508,7 @@ Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
const box = await elem.boundingBox();
if (box && box.width > 0 && box.height > 0) {
foundVisible = true;
console.log(` 📍 Found visible chat-input at index ${i}`);
console.info(` 📍 Found visible chat-input at index ${i}`);
break;
}
}
@@ -518,27 +518,27 @@ Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
// 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.info(' 📍 Fallback: verified we are on chat page');
}
console.log(' ✅ 欢迎界面已显示');
console.info(' ✅ 欢迎界面已显示');
});
Then('应该切换到该对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证已切换对话...');
console.info(' 📍 Step: 验证已切换对话...');
// The URL or active state should change
// For now, just verify the page is responsive
await this.page.waitForTimeout(500);
console.log(' ✅ 已切换到该对话');
console.info(' ✅ 已切换到该对话');
});
Then('显示该对话的历史消息', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示历史消息...');
console.info(' 📍 Step: 验证显示历史消息...');
// Wait for the loading to finish - the messages need time to load after switching topics
console.log(' 📍 等待消息加载...');
console.info(' 📍 等待消息加载...');
await this.page.waitForTimeout(2000);
// Wait for the message wrapper to appear (ChatItem component uses message-wrapper class)
@@ -546,23 +546,23 @@ Then('显示该对话的历史消息', async function (this: CustomWorld) {
try {
await this.page.waitForSelector(messageSelector, { timeout: 10_000 });
} catch {
console.log(' ⚠️ 等待消息选择器超时,尝试备用选择器...');
console.info(' ⚠️ 等待消息选择器超时,尝试备用选择器...');
}
// There should be messages in the chat area
const messages = this.page.locator(messageSelector);
const messageCount = await messages.count();
console.log(` 📍 找到 ${messageCount} 条消息`);
console.info(` 📍 找到 ${messageCount} 条消息`);
// At least some messages should be visible
expect(messageCount).toBeGreaterThan(0);
console.log(' ✅ 历史消息已显示');
console.info(' ✅ 历史消息已显示');
});
Then('对话名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证对话名称为 "${expectedName}"...`);
console.info(` 📍 Step: 验证对话名称为 "${expectedName}"...`);
// Wait for the rename to take effect
await this.page.waitForTimeout(1000);
@@ -574,20 +574,20 @@ Then('对话名称应该更新为 {string}', async function (this: CustomWorld,
await expect(renamedTopic).toBeVisible({ timeout: 5000 });
console.log(` ✅ 对话名称已更新为 "${expectedName}"`);
console.info(` ✅ 对话名称已更新为 "${expectedName}"`);
});
Then('该对话应该被删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证对话已删除...');
console.info(' 📍 Step: 验证对话已删除...');
// Wait for deletion to take effect
await this.page.waitForTimeout(500);
console.log(' ✅ 对话已删除');
console.info(' ✅ 对话已删除');
});
Then('对话列表中不再显示该对话', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证对话列表中不再显示该对话...');
console.info(' 📍 Step: 验证对话列表中不再显示该对话...');
// Wait for UI to update
await this.page.waitForTimeout(500);
@@ -599,14 +599,14 @@ Then('对话列表中不再显示该对话', async function (this: CustomWorld)
);
const count = await deletedTopic.count();
expect(count).toBe(0);
console.log(` ✅ 对话 "${this.testContext.deletedTopicTitle}" 已从列表中移除`);
console.info(` ✅ 对话 "${this.testContext.deletedTopicTitle}" 已从列表中移除`);
} else {
console.log(' ✅ 对话已从列表中移除');
console.info(' ✅ 对话已从列表中移除');
}
});
Then('应该显示包含 {string} 的对话', async function (this: CustomWorld, searchText: string) {
console.log(` 📍 Step: 验证搜索结果包含 "${searchText}"...`);
console.info(` 📍 Step: 验证搜索结果包含 "${searchText}"...`);
// Wait for search results to load (search opens a modal dialog)
await this.page.waitForTimeout(2000);
@@ -615,7 +615,7 @@ Then('应该显示包含 {string} 的对话', async function (this: CustomWorld,
// 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}`);
console.info(` 📍 搜索模态框: ${hasModal}`);
// Find matching items in the search results (either in modal or in sidebar if filtered)
const matchingInModal = searchModal.getByText(searchText);
@@ -624,20 +624,20 @@ Then('应该显示包含 {string} 的对话', async function (this: CustomWorld,
const modalMatchCount = await matchingInModal.count();
const pageMatchCount = await matchingInPage.count();
console.log(` 📍 模态框中找到 ${modalMatchCount} 个匹配, 页面中找到 ${pageMatchCount} 个匹配`);
console.info(` 📍 模态框中找到 ${modalMatchCount} 个匹配, 页面中找到 ${pageMatchCount} 个匹配`);
// At least one match should be found (either in search input or results)
expect(modalMatchCount + pageMatchCount).toBeGreaterThan(0);
console.log(` ✅ 搜索结果显示包含 "${searchText}" 的对话`);
console.info(` ✅ 搜索结果显示包含 "${searchText}" 的对话`);
});
Then('不相关的对话应该被过滤', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不相关对话已被过滤...');
console.info(' 📍 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(' ✅ 不相关对话已被过滤');
console.info(' ✅ 不相关对话已被过滤');
});
+23 -23
View File
@@ -7,7 +7,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { llmMockManager, presetResponses } from '../../mocks/llm';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Given Steps
@@ -21,29 +21,29 @@ Given('用户已登录系统', async function (this: CustomWorld) {
});
Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 设置 LLM mock...');
console.info(' 📍 Step: 设置 LLM mock...');
// Setup LLM mock before navigation
llmMockManager.setResponse('hello', presetResponses.greeting);
await llmMockManager.setup(this.page);
console.log(' 📍 Step: 导航到首页...');
console.info(' 📍 Step: 导航到首页...');
// Navigate to home page first
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: WAIT_TIMEOUT });
console.log(' 📍 Step: 查找 Lobe AI...');
console.info(' 📍 Step: 查找 Lobe AI...');
// Find and click on "Lobe AI" agent in the sidebar/home
const lobeAIAgent = this.page.locator('text=Lobe AI').first();
await expect(lobeAIAgent).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(' 📍 Step: 点击 Lobe AI...');
console.info(' 📍 Step: 点击 Lobe AI...');
await lobeAIAgent.click();
console.log(' 📍 Step: 等待聊天界面加载...');
console.info(' 📍 Step: 等待聊天界面加载...');
// Wait for the chat interface to be ready
await this.page.waitForLoadState('networkidle', { timeout: WAIT_TIMEOUT });
console.log(' 📍 Step: 查找输入框...');
console.info(' 📍 Step: 查找输入框...');
// The input is a rich text editor with contenteditable
// There are 2 ChatInput components (desktop & mobile), find the visible one
@@ -53,7 +53,7 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
// Find all chat-input elements and get the visible one
const chatInputs = this.page.locator('[data-testid="chat-input"]');
const count = await chatInputs.count();
console.log(` 📍 Found ${count} chat-input elements`);
console.info(` 📍 Found ${count} chat-input elements`);
// Find the first visible one or just use the first one
let chatInputContainer = chatInputs.first();
@@ -62,19 +62,19 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
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)`);
console.info(` ✓ Using chat-input element ${i} (has bounding box)`);
break;
}
}
// Click the container to focus the editor
await chatInputContainer.click();
console.log(' ✓ Clicked on chat input container');
console.info(' ✓ Clicked on chat input container');
// Wait for any animations to complete
await this.page.waitForTimeout(300);
console.log(' ✅ 已进入 Lobe AI 对话页面');
console.info(' ✅ 已进入 Lobe AI 对话页面');
});
// ============================================
@@ -86,7 +86,7 @@ Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
* This sends a message and waits for the AI response
*/
Given('用户已发送消息 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 发送消息 "${message}" 并等待回复...`);
console.info(` 📍 Step: 发送消息 "${message}" 并等待回复...`);
// Find visible chat input container first
const chatInputs = this.page.locator('[data-testid="chat-input"]');
@@ -118,7 +118,7 @@ Given('用户已发送消息 {string}', async function (this: CustomWorld, messa
// Wait for the assistant response to appear
// Assistant messages are left-aligned .message-wrapper elements that contain "Lobe AI" title
console.log(' 📍 Step: 等待助手回复...');
console.info(' 📍 Step: 等待助手回复...');
// Wait for any new message wrapper to appear (there should be at least 2 - user + assistant)
const messageWrappers = this.page.locator('.message-wrapper');
@@ -126,7 +126,7 @@ Given('用户已发送消息 {string}', async function (this: CustomWorld, messa
.toHaveCount(2, { timeout: 15_000 })
.catch(() => {
// Fallback: just wait for at least one message wrapper
console.log(' 📍 Fallback: checking for any message wrapper');
console.info(' 📍 Fallback: checking for any message wrapper');
});
// Verify the assistant message contains expected content
@@ -136,16 +136,16 @@ Given('用户已发送消息 {string}', async function (this: CustomWorld, messa
await expect(assistantMessage).toBeVisible({ timeout: 5000 });
this.testContext.lastMessage = message;
console.log(` ✅ 消息已发送并收到回复`);
console.info(` ✅ 消息已发送并收到回复`);
});
When('用户发送消息 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 查找输入框...`);
console.info(` 📍 Step: 查找输入框...`);
// Find visible chat input container first
const chatInputs = this.page.locator('[data-testid="chat-input"]');
const count = await chatInputs.count();
console.log(` 📍 Found ${count} chat-input containers`);
console.info(` 📍 Found ${count} chat-input containers`);
let chatInputContainer = chatInputs.first();
for (let i = 0; i < count; i++) {
@@ -153,28 +153,28 @@ When('用户发送消息 {string}', async function (this: CustomWorld, message:
const box = await elem.boundingBox();
if (box && box.width > 0 && box.height > 0) {
chatInputContainer = elem;
console.log(` 📍 Using container ${i}`);
console.info(` 📍 Using container ${i}`);
break;
}
}
// Click the container to ensure focus is on the input area
console.log(` 📍 Step: 点击输入区域...`);
console.info(` 📍 Step: 点击输入区域...`);
await chatInputContainer.click();
await this.page.waitForTimeout(500);
console.log(` 📍 Step: 输入消息 "${message}"...`);
console.info(` 📍 Step: 输入消息 "${message}"...`);
// Just type via keyboard - the input should be focused after clicking
await this.page.keyboard.type(message, { delay: 30 });
await this.page.waitForTimeout(300);
console.log(` 📍 Step: 发送消息 (按 Enter)...`);
console.info(` 📍 Step: 发送消息 (按 Enter)...`);
await this.page.keyboard.press('Enter');
// Wait for the message to be sent and processed
await this.page.waitForTimeout(1000);
console.log(` ✅ 消息已发送`);
console.info(` ✅ 消息已发送`);
this.testContext.lastMessage = message;
});
@@ -207,5 +207,5 @@ Then('回复内容应该可见', async function (this: CustomWorld) {
expect(text).toBeTruthy();
expect(text!.length).toBeGreaterThan(0);
console.log(` ✅ Assistant replied: "${text?.slice(0, 50)}..."`);
console.info(` ✅ Assistant replied: "${text?.slice(0, 50)}..."`);
});
+53 -53
View File
@@ -10,7 +10,7 @@
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// When Steps
@@ -20,7 +20,7 @@ import { CustomWorld } from '../../support/world';
async function findAssistantMessage(page: CustomWorld['page']) {
const messageWrappers = page.locator('.message-wrapper');
const wrapperCount = await messageWrappers.count();
console.log(` 📍 Found ${wrapperCount} message wrappers`);
console.info(` 📍 Found ${wrapperCount} message wrappers`);
// Find the assistant message by looking for the one with "Lobe AI" or "AI" in title
for (let i = wrapperCount - 1; i >= 0; i--) {
@@ -31,7 +31,7 @@ async function findAssistantMessage(page: CustomWorld['page']) {
.catch(() => '');
if (titleText?.includes('Lobe AI') || titleText?.includes('AI')) {
console.log(` 📍 Found assistant message at index ${i}`);
console.info(` 📍 Found assistant message at index ${i}`);
return wrapper;
}
}
@@ -41,7 +41,7 @@ async function findAssistantMessage(page: CustomWorld['page']) {
}
When('用户点击消息的复制按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击复制按钮...');
console.info(' 📍 Step: 点击复制按钮...');
// Find the assistant message wrapper
const assistantMessage = await findAssistantMessage(this.page);
@@ -52,8 +52,8 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
// First try: find copy button directly by its icon (lucide-copy)
const copyButtonByIcon = this.page.locator('svg.lucide-copy').locator('..');
let copyButtonCount = await copyButtonByIcon.count();
console.log(` 📍 Found ${copyButtonCount} buttons with copy icon`);
const copyButtonCount = await copyButtonByIcon.count();
console.info(` 📍 Found ${copyButtonCount} buttons with copy icon`);
if (copyButtonCount > 0) {
// Click the visible copy button
@@ -62,7 +62,7 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
const box = await btn.boundingBox();
if (box && box.width > 0 && box.height > 0) {
await btn.click();
console.log(' ✅ 已点击复制按钮');
console.info(' ✅ 已点击复制按钮');
await this.page.waitForTimeout(500);
return;
}
@@ -70,7 +70,7 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
}
// Fallback: Look for action bar within message and open more menu
console.log(' 📍 Fallback: Looking for copy in more menu...');
console.info(' 📍 Fallback: Looking for copy in more menu...');
const actionBar = assistantMessage.locator('[role="menubar"]');
if ((await actionBar.count()) > 0) {
const moreButton = actionBar.locator('button').last();
@@ -80,7 +80,7 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
const copyMenuItem = this.page.getByRole('menuitem', { name: /复制/ });
if ((await copyMenuItem.count()) > 0) {
await copyMenuItem.click();
console.log(' ✅ 已从菜单中点击复制');
console.info(' ✅ 已从菜单中点击复制');
await this.page.waitForTimeout(500);
return;
}
@@ -94,14 +94,14 @@ When('用户点击消息的复制按钮', async function (this: CustomWorld) {
const copyMenuItem = this.page.getByRole('menuitem', { name: /复制/ });
await copyMenuItem.click();
console.log(' ✅ 已从更多菜单中点击复制');
console.info(' ✅ 已从更多菜单中点击复制');
}
await this.page.waitForTimeout(500);
});
When('用户点击助手消息的编辑按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击编辑按钮...');
console.info(' 📍 Step: 点击编辑按钮...');
// Find the assistant message wrapper
const assistantMessage = await findAssistantMessage(this.page);
@@ -112,8 +112,8 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
// First try: find edit button directly by its icon (lucide-pencil)
const editButtonByIcon = this.page.locator('svg.lucide-pencil').locator('..');
let editButtonCount = await editButtonByIcon.count();
console.log(` 📍 Found ${editButtonCount} buttons with pencil icon`);
const editButtonCount = await editButtonByIcon.count();
console.info(` 📍 Found ${editButtonCount} buttons with pencil icon`);
if (editButtonCount > 0) {
for (let i = 0; i < editButtonCount; i++) {
@@ -121,7 +121,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
const box = await btn.boundingBox();
if (box && box.width > 0 && box.height > 0) {
await btn.click();
console.log(' ✅ 已点击编辑按钮');
console.info(' ✅ 已点击编辑按钮');
await this.page.waitForTimeout(500);
return;
}
@@ -129,7 +129,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
}
// Fallback: Look for edit in more menu
console.log(' 📍 Fallback: Looking for edit in more menu...');
console.info(' 📍 Fallback: Looking for edit in more menu...');
const moreButtonByIcon = this.page.locator('svg.lucide-more-horizontal').locator('..');
if ((await moreButtonByIcon.count()) > 0) {
await moreButtonByIcon.first().click();
@@ -138,7 +138,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
const editMenuItem = this.page.getByRole('menuitem', { name: /编辑/ });
if ((await editMenuItem.count()) > 0) {
await editMenuItem.click();
console.log(' ✅ 已从菜单中点击编辑');
console.info(' ✅ 已从菜单中点击编辑');
}
}
@@ -146,7 +146,7 @@ When('用户点击助手消息的编辑按钮', async function (this: CustomWorl
});
When('用户修改消息内容为 {string}', async function (this: CustomWorld, newContent: string) {
console.log(` 📍 Step: 修改消息内容为 "${newContent}"...`);
console.info(` 📍 Step: 修改消息内容为 "${newContent}"...`);
// Find the editing textarea or input
const editArea = this.page.locator('textarea, [contenteditable="true"]').last();
@@ -160,11 +160,11 @@ When('用户修改消息内容为 {string}', async function (this: CustomWorld,
// Store for later verification
this.testContext.editedContent = newContent;
console.log(` ✅ 已修改消息内容为 "${newContent}"`);
console.info(` ✅ 已修改消息内容为 "${newContent}"`);
});
When('用户保存编辑', async function (this: CustomWorld) {
console.log(' 📍 Step: 保存编辑...');
console.info(' 📍 Step: 保存编辑...');
// Find and click the save/confirm button
const saveButton = this.page.locator('button').filter({
@@ -178,12 +178,12 @@ When('用户保存编辑', async function (this: CustomWorld) {
await this.page.keyboard.press('Enter');
}
console.log(' ✅ 已保存编辑');
console.info(' ✅ 已保存编辑');
await this.page.waitForTimeout(500);
});
When('用户点击消息的更多操作按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击更多操作按钮...');
console.info(' 📍 Step: 点击更多操作按钮...');
// Find the assistant message wrapper
const assistantMessage = await findAssistantMessage(this.page);
@@ -194,15 +194,15 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
// Get the bounding box of the message to help filter buttons
const messageBox = await assistantMessage.boundingBox();
console.log(` 📍 Message bounding box: y=${messageBox?.y}, height=${messageBox?.height}`);
console.info(` 📍 Message bounding box: y=${messageBox?.y}, height=${messageBox?.height}`);
// Look for the "more" button by ellipsis icon (lucide-ellipsis or lucide-more-horizontal)
// The icon might be `...` which is lucide-ellipsis
const ellipsisButtons = this.page
.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal')
.locator('..');
let ellipsisCount = await ellipsisButtons.count();
console.log(` 📍 Found ${ellipsisCount} buttons with ellipsis/more icon`);
const ellipsisCount = await ellipsisButtons.count();
console.info(` 📍 Found ${ellipsisCount} buttons with ellipsis/more icon`);
if (ellipsisCount > 0 && messageBox) {
// Find buttons in the message area (x > 320 to exclude sidebar)
@@ -210,7 +210,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
const btn = ellipsisButtons.nth(i);
const box = await btn.boundingBox();
if (box && box.width > 0 && box.height > 0) {
console.log(` 📍 Ellipsis button ${i}: x=${box.x}, y=${box.y}`);
console.info(` 📍 Ellipsis button ${i}: x=${box.x}, y=${box.y}`);
// Check if button is within the message area
if (
box.x > 320 &&
@@ -218,7 +218,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
box.y <= messageBox.y + messageBox.height + 50
) {
await btn.click();
console.log(` ✅ 已点击更多操作按钮 (ellipsis at x=${box.x}, y=${box.y})`);
console.info(` ✅ 已点击更多操作按钮 (ellipsis at x=${box.x}, y=${box.y})`);
await this.page.waitForTimeout(300);
return;
}
@@ -229,18 +229,18 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
// Second approach: Find the action bar and click its last button
const actionBar = assistantMessage.locator('[role="menubar"]');
const actionBarCount = await actionBar.count();
console.log(` 📍 Found ${actionBarCount} action bars in message`);
console.info(` 📍 Found ${actionBarCount} action bars in message`);
if (actionBarCount > 0) {
// Find all clickable elements (button, span with onClick, etc.)
const clickables = actionBar.locator('button, span[role="button"], [class*="action"]');
const clickableCount = await clickables.count();
console.log(` 📍 Found ${clickableCount} clickable elements in action bar`);
console.info(` 📍 Found ${clickableCount} clickable elements in action bar`);
if (clickableCount > 0) {
// Click the last one (usually "more")
await clickables.last().click();
console.log(' ✅ 已点击更多操作按钮 (last clickable)');
console.info(' ✅ 已点击更多操作按钮 (last clickable)');
await this.page.waitForTimeout(300);
return;
}
@@ -249,7 +249,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
// Third approach: Find buttons by looking for all SVG icons in the message area
const allSvgButtons = this.page.locator('.message-wrapper svg').locator('..');
const svgButtonCount = await allSvgButtons.count();
console.log(` 📍 Found ${svgButtonCount} SVG button parents in message wrappers`);
console.info(` 📍 Found ${svgButtonCount} SVG button parents in message wrappers`);
if (svgButtonCount > 0 && messageBox) {
// Find the rightmost button in the action area (more button is usually last)
@@ -276,7 +276,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
if (rightmostBtn) {
await rightmostBtn.click();
console.log(` ✅ 已点击更多操作按钮 (rightmost at x=${maxX})`);
console.info(` ✅ 已点击更多操作按钮 (rightmost at x=${maxX})`);
await this.page.waitForTimeout(300);
return;
}
@@ -286,7 +286,7 @@ When('用户点击消息的更多操作按钮', async function (this: CustomWorl
});
When('用户选择删除消息选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择删除消息选项...');
console.info(' 📍 Step: 选择删除消息选项...');
// Find and click delete option (exact match to avoid "Delete and Regenerate")
// Support both English and Chinese
@@ -294,48 +294,48 @@ When('用户选择删除消息选项', async function (this: CustomWorld) {
await expect(deleteOption).toBeVisible({ timeout: 5000 });
await deleteOption.click();
console.log(' ✅ 已选择删除消息选项');
console.info(' ✅ 已选择删除消息选项');
await this.page.waitForTimeout(300);
});
When('用户确认删除消息', async function (this: CustomWorld) {
console.log(' 📍 Step: 确认删除消息...');
console.info(' 📍 Step: 确认删除消息...');
// A confirmation popconfirm might appear
const confirmButton = this.page.locator('.ant-popconfirm-buttons button.ant-btn-dangerous');
if ((await confirmButton.count()) > 0) {
await confirmButton.click();
console.log(' ✅ 已确认删除消息');
console.info(' ✅ 已确认删除消息');
} else {
// If no popconfirm, deletion might be immediate
console.log(' ✅ 删除操作已执行(无需确认)');
console.info(' ✅ 删除操作已执行(无需确认)');
}
await this.page.waitForTimeout(500);
});
When('用户选择折叠消息选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择折叠消息选项...');
console.info(' 📍 Step: 选择折叠消息选项...');
// The collapse option is "Collapse Message" or "收起消息" in the menu
const collapseOption = this.page.getByRole('menuitem', { name: /Collapse Message|收起消息/ });
await expect(collapseOption).toBeVisible({ timeout: 5000 });
await collapseOption.click();
console.log(' ✅ 已选择折叠消息选项');
console.info(' ✅ 已选择折叠消息选项');
await this.page.waitForTimeout(500);
});
When('用户选择展开消息选项', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择展开消息选项...');
console.info(' 📍 Step: 选择展开消息选项...');
// The expand option is "Expand Message" or "展开消息" in the menu
const expandOption = this.page.getByRole('menuitem', { name: /Expand Message|展开消息/ });
await expect(expandOption).toBeVisible({ timeout: 5000 });
await expandOption.click();
console.log(' ✅ 已选择展开消息选项');
console.info(' ✅ 已选择展开消息选项');
await this.page.waitForTimeout(500);
});
@@ -344,7 +344,7 @@ When('用户选择展开消息选项', async function (this: CustomWorld) {
// ============================================
Then('消息内容应该被复制到剪贴板', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息已复制到剪贴板...');
console.info(' 📍 Step: 验证消息已复制到剪贴板...');
// Check for success message/toast
const successMessage = this.page.locator('.ant-message-success, [class*="toast"]');
@@ -355,15 +355,15 @@ Then('消息内容应该被复制到剪贴板', async function (this: CustomWorl
// Verify by checking if clipboard has content (or success message appeared)
const successCount = await successMessage.count();
if (successCount > 0) {
console.log(' ✅ 显示复制成功提示');
console.info(' ✅ 显示复制成功提示');
} else {
// Just verify the action completed without error
console.log(' ✅ 复制操作已完成');
console.info(' ✅ 复制操作已完成');
}
});
Then('消息内容应该更新为 {string}', async function (this: CustomWorld, expectedContent: string) {
console.log(` 📍 Step: 验证消息内容为 "${expectedContent}"...`);
console.info(` 📍 Step: 验证消息内容为 "${expectedContent}"...`);
await this.page.waitForTimeout(1000);
@@ -371,11 +371,11 @@ Then('消息内容应该更新为 {string}', async function (this: CustomWorld,
const messageContent = this.page.getByText(expectedContent);
await expect(messageContent).toBeVisible({ timeout: 5000 });
console.log(` ✅ 消息内容已更新为 "${expectedContent}"`);
console.info(` ✅ 消息内容已更新为 "${expectedContent}"`);
});
Then('该消息应该从对话中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息已移除...');
console.info(' 📍 Step: 验证消息已移除...');
await this.page.waitForTimeout(500);
@@ -384,12 +384,12 @@ Then('该消息应该从对话中移除', async function (this: CustomWorld) {
const assistantMessages = this.page.locator('[data-role="assistant"]');
const count = await assistantMessages.count();
console.log(` 📍 剩余助手消息数量: ${count}`);
console.log(' ✅ 消息已移除');
console.info(` 📍 剩余助手消息数量: ${count}`);
console.info(' ✅ 消息已移除');
});
Then('消息内容应该被折叠', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息已折叠...');
console.info(' 📍 Step: 验证消息已折叠...');
await this.page.waitForTimeout(500);
@@ -400,15 +400,15 @@ Then('消息内容应该被折叠', async function (this: CustomWorld) {
const hasCollapsed = (await collapsedIndicator.count()) > 0;
if (hasCollapsed) {
console.log(' ✅ 消息已折叠');
console.info(' ✅ 消息已折叠');
} else {
// Alternative verification: content height should be reduced
console.log(' ✅ 消息折叠操作已执行');
console.info(' ✅ 消息折叠操作已执行');
}
});
Then('消息内容应该完整显示', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证消息完整显示...');
console.info(' 📍 Step: 验证消息完整显示...');
await this.page.waitForTimeout(500);
@@ -416,5 +416,5 @@ Then('消息内容应该完整显示', async function (this: CustomWorld) {
const assistantMessage = await findAssistantMessage(this.page);
await expect(assistantMessage).toBeVisible();
console.log(' ✅ 消息内容完整显示');
console.info(' ✅ 消息内容完整显示');
});
+6 -6
View File
@@ -1,8 +1,8 @@
import { Given, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TEST_USER, createTestSession } from '../../support/seedTestUser';
import { CustomWorld } from '../../support/world';
import { createTestSession, TEST_USER } from '../../support/seedTestUser';
import { type CustomWorld } from '../../support/world';
/**
* Login via UI - fills in the login form and submits
@@ -26,7 +26,7 @@ Given('I am logged in as the test user', async function (this: CustomWorld) {
// Wait for navigation away from signin page
await this.page.waitForURL((url) => !url.pathname.includes('/signin'), { timeout: 30_000 });
console.log('✅ Logged in as test user via UI');
console.info('✅ Logged in as test user via UI');
});
/**
@@ -53,7 +53,7 @@ Given('I am logged in with a session', async function (this: CustomWorld) {
},
]);
console.log('✅ Session cookie set for test user');
console.info('✅ Session cookie set for test user');
});
/**
@@ -87,7 +87,7 @@ Given('I should be logged in', async function (this: CustomWorld) {
await expect(this.page).not.toHaveURL(/\/signin/);
// Optionally check for user menu or other logged-in indicators
console.log('✅ User is logged in');
console.info('✅ User is logged in');
});
/**
@@ -96,5 +96,5 @@ Given('I should be logged in', async function (this: CustomWorld) {
When('I logout', async function (this: CustomWorld) {
// Clear cookies to logout
await this.browserContext.clearCookies();
console.log('✅ User logged out (cookies cleared)');
console.info('✅ User logged out (cookies cleared)');
});
+11 -11
View File
@@ -1,7 +1,7 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// Given Steps (Preconditions)
@@ -23,7 +23,7 @@ When('I click the back button', async function (this: CustomWorld) {
// Store current URL to verify navigation
const currentUrl = this.page.url();
console.log(` 📍 Current URL before back: ${currentUrl}`);
console.info(` 📍 Current URL before back: ${currentUrl}`);
// Try to find a back button - look for arrow icon or back text
// The UI has a back arrow (←) next to the search bar
@@ -34,7 +34,7 @@ When('I click the back button', async function (this: CustomWorld) {
.first();
const backButtonVisible = await backButton.isVisible().catch(() => false);
console.log(` 📍 Back button visible: ${backButtonVisible}`);
console.info(` 📍 Back button visible: ${backButtonVisible}`);
if (backButtonVisible) {
// Click the parent element if it's an SVG icon
@@ -44,10 +44,10 @@ When('I click the back button', async function (this: CustomWorld) {
} else {
await backButton.click();
}
console.log(' 📍 Clicked back button');
console.info(' 📍 Clicked back button');
} else {
// Use browser back as fallback
console.log(' 📍 Using browser goBack()');
console.info(' 📍 Using browser goBack()');
await this.page.goBack();
}
@@ -55,7 +55,7 @@ When('I click the back button', async function (this: CustomWorld) {
await this.page.waitForTimeout(500);
const newUrl = this.page.url();
console.log(` 📍 URL after back: ${newUrl}`);
console.info(` 📍 URL after back: ${newUrl}`);
});
// ============================================
@@ -142,7 +142,7 @@ Then('I should be on the assistant list page', async function (this: CustomWorld
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be assistant list page, but got: ${currentUrl}`).toBeTruthy();
});
@@ -183,7 +183,7 @@ Then('I should see the model description', async function (this: CustomWorld) {
// Pass if any content area is visible - the description might be a placeholder
expect(isVisible || true).toBeTruthy();
console.log(' 📍 Model description area checked');
console.info(' 📍 Model description area checked');
});
Then('I should see the model parameters information', async function (this: CustomWorld) {
@@ -210,7 +210,7 @@ Then('I should be on the model list page', async function (this: CustomWorld) {
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be model list page, but got: ${currentUrl}`).toBeTruthy();
});
@@ -274,7 +274,7 @@ Then('I should be on the provider list page', async function (this: CustomWorld)
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be provider list page, but got: ${currentUrl}`).toBeTruthy();
});
@@ -336,6 +336,6 @@ Then('I should be on the MCP list page', async function (this: CustomWorld) {
currentUrl.endsWith('/community') ||
currentUrl.includes('/community#');
console.log(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
console.info(` 📍 Current URL: ${currentUrl}, isListPage: ${isListPage}`);
expect(isListPage, `Expected URL to be MCP list page, but got: ${currentUrl}`).toBeTruthy();
});
+21 -21
View File
@@ -1,7 +1,7 @@
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// When Steps (Actions)
@@ -35,7 +35,7 @@ When('I click on a category in the category menu', async function (this: CustomW
);
const count = await categoryItems.count();
console.log(` 📍 Found ${count} category items`);
console.info(` 📍 Found ${count} category items`);
if (count === 0) {
// Fallback: try finding by text content that looks like a category
@@ -43,7 +43,7 @@ When('I click on a category in the category menu', async function (this: CustomW
'text=/^(Academic|Career|Design|Programming|General)/',
);
const fallbackCount = await fallbackCategories.count();
console.log(` 📍 Fallback: Found ${fallbackCount} category items by text`);
console.info(` 📍 Fallback: Found ${fallbackCount} category items by text`);
if (fallbackCount > 0) {
await fallbackCategories.first().click();
@@ -75,7 +75,7 @@ When('I click on a category in the category filter', async function (this: Custo
);
const count = await categoryItems.count();
console.log(` 📍 Found ${count} category filter items`);
console.info(` 📍 Found ${count} category filter items`);
if (count === 0) {
// Fallback: try finding by text content that looks like MCP categories
@@ -83,7 +83,7 @@ When('I click on a category in the category filter', async function (this: Custo
'text=/^(Developer Tools|Productivity Tools|Utility Tools|Media Generation|Business Services)/',
);
const fallbackCount = await fallbackCategories.count();
console.log(` 📍 Fallback: Found ${fallbackCount} MCP category items by text`);
console.info(` 📍 Fallback: Found ${fallbackCount} MCP category items by text`);
if (fallbackCount > 0) {
await fallbackCategories.first().click();
@@ -120,11 +120,11 @@ When('I click the next page button', async function (this: CustomWorld) {
await assistantCards.first().waitFor({ state: 'visible', timeout: 30_000 });
const initialCount = await assistantCards.count();
console.log(` 📍 Initial card count: ${initialCount}`);
console.info(` 📍 Initial card count: ${initialCount}`);
// The page uses infinite scroll instead of pagination buttons
// Scroll to bottom to trigger infinite scroll
console.log(' 📍 Page uses infinite scroll, scrolling to bottom');
console.info(' 📍 Page uses infinite scroll, scrolling to bottom');
await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await this.page.waitForTimeout(2000); // Wait for new content to load
@@ -280,7 +280,7 @@ When(
const mcpLinkVisible = await mcpLink.isVisible().catch(() => false);
if (mcpLinkVisible) {
console.log(' 📍 Found direct MCP link');
console.info(' 📍 Found direct MCP link');
await mcpLink.click();
return;
}
@@ -303,7 +303,7 @@ When(
}
// Fallback: click on MCP in the sidebar navigation
console.log(' 📍 Fallback: clicking MCP in sidebar');
console.info(' 📍 Fallback: clicking MCP in sidebar');
const mcpNavItem = this.page
.locator('nav a:has-text("MCP"), [class*="nav"] a:has-text("MCP")')
.first();
@@ -313,7 +313,7 @@ When(
}
// Last resort: navigate directly
console.log(' 📍 Last resort: direct navigation to /community/mcp');
console.info(' 📍 Last resort: direct navigation to /community/mcp');
await this.page.goto('/community/mcp');
},
);
@@ -372,8 +372,8 @@ Then(
Then('the URL should contain the category parameter', async function (this: CustomWorld) {
const currentUrl = this.page.url();
console.log(` 📍 Current URL: ${currentUrl}`);
console.log(` 📍 Selected category: ${this.testContext.selectedCategory}`);
console.info(` 📍 Current URL: ${currentUrl}`);
console.info(` 📍 Selected category: ${this.testContext.selectedCategory}`);
// Check if URL contains a category-related parameter
// The URL format is: /community/agent?category=xxx
@@ -398,11 +398,11 @@ Then('I should see different assistant cards', async function (this: CustomWorld
await expect(assistantItems.first()).toBeVisible({ timeout: 30_000 });
const currentCount = await assistantItems.count();
console.log(` 📍 Current card count: ${currentCount}`);
console.info(` 📍 Current card count: ${currentCount}`);
// If we used infinite scroll, check that we have cards (might be same or more)
if (this.testContext.usedInfiniteScroll) {
console.log(
console.info(
` 📍 Used infinite scroll, initial count was: ${this.testContext.initialCardCount}`,
);
expect(currentCount).toBeGreaterThan(0);
@@ -416,7 +416,7 @@ Then('the URL should contain the page parameter', async function (this: CustomWo
// If we used infinite scroll, URL won't have page parameter - that's expected
if (this.testContext.usedInfiniteScroll) {
console.log(' 📍 Used infinite scroll, page parameter not expected');
console.info(' 📍 Used infinite scroll, page parameter not expected');
// Just verify we're still on the assistant page
expect(currentUrl.includes('/community/agent')).toBeTruthy();
return;
@@ -488,11 +488,11 @@ Then('I should see the model detail content', async function (this: CustomWorld)
'text=/Overview|Model Parameters|Related Recommendations|Configuration Guide/',
);
console.log(' 📍 Waiting for model detail content to load...');
console.info(' 📍 Waiting for model detail content to load...');
await expect(modelTabs.first()).toBeVisible({ timeout: 30_000 });
const tabCount = await modelTabs.count();
console.log(` 📍 Found ${tabCount} model detail tabs`);
console.info(` 📍 Found ${tabCount} model detail tabs`);
expect(tabCount).toBeGreaterThan(0);
});
@@ -519,11 +519,11 @@ Then('I should see the provider detail content', async function (this: CustomWor
// Wait for the provider title to appear
const providerTitle = this.page.locator('h1, h2, [class*="title"]').first();
console.log(' 📍 Waiting for provider detail content to load...');
console.info(' 📍 Waiting for provider detail content to load...');
await expect(providerTitle).toBeVisible({ timeout: 30_000 });
const titleText = await providerTitle.textContent();
console.log(` 📍 Provider title: ${titleText}`);
console.info(` 📍 Provider title: ${titleText}`);
expect(titleText?.trim().length).toBeGreaterThan(0);
});
@@ -571,13 +571,13 @@ Then('I should be navigated to {string}', async function (this: CustomWorld, exp
await this.page.waitForTimeout(500); // Extra wait for client-side routing
const currentUrl = this.page.url();
console.log(` 📍 Expected path: ${expectedPath}, Current URL: ${currentUrl}`);
console.info(` 📍 Expected path: ${expectedPath}, Current URL: ${currentUrl}`);
// Verify that URL contains the expected path
const urlMatches = currentUrl.includes(expectedPath);
if (!urlMatches) {
console.log(` ⚠️ URL mismatch, but page might still be correct`);
console.info(` ⚠️ URL mismatch, but page might still be correct`);
}
expect(
+45 -45
View File
@@ -10,7 +10,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TEST_USER } from '../../support/seedTestUser';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Helper Functions
@@ -88,7 +88,7 @@ async function inputNewName(
}
await this.page.waitForTimeout(1000);
console.log(` ✅ 已输入新名称 "${newName}"`);
console.info(` ✅ 已输入新名称 "${newName}"`);
}
/**
@@ -115,7 +115,7 @@ async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
[agentId, slug, title, TEST_USER.id, now],
);
console.log(` 📍 Created test agent in DB: ${agentId}`);
console.info(` 📍 Created test agent in DB: ${agentId}`);
return agentId;
} finally {
await client.end();
@@ -127,16 +127,16 @@ async function createTestAgent(title: string = 'Test Agent'): Promise<string> {
// ============================================
Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld) {
console.log(' 📍 Step: 在数据库中创建测试 Agent...');
console.info(' 📍 Step: 在数据库中创建测试 Agent...');
const agentId = await createTestAgent('E2E Test Agent');
this.testContext.createdAgentId = agentId;
console.log(' 📍 Step: 导航到 Home 页面...');
console.info(' 📍 Step: 导航到 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 查找新创建的 Agent...');
console.info(' 📍 Step: 查找新创建的 Agent...');
// Look for the newly created agent in the sidebar by its specific ID
const agentItem = this.page.locator(`a[href="/agent/${agentId}"]`).first();
await expect(agentItem).toBeVisible({ timeout: WAIT_TIMEOUT });
@@ -147,18 +147,18 @@ Given('用户在 Home 页面有一个 Agent', async function (this: CustomWorld)
this.testContext.targetItemSelector = `a[href="/agent/${agentId}"]`;
this.testContext.targetType = 'agent';
console.log(` ✅ 找到 Agent: ${agentLabel}, id: ${agentId}`);
console.info(` ✅ 找到 Agent: ${agentLabel}, id: ${agentId}`);
});
Given('该 Agent 未被置顶', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 检查 Agent 未被置顶...');
console.info(' 📍 Step: 检查 Agent 未被置顶...');
// Check if the agent has a pin icon - if so, unpin it first
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
// Pin icon uses lucide-react which adds class "lucide lucide-pin"
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
if ((await pinIcon.count()) > 0) {
console.log(' 📍 Agent 已置顶,开始取消置顶操作...');
console.info(' 📍 Agent 已置顶,开始取消置顶操作...');
// Unpin it first
await targetItem.hover();
await this.page.waitForTimeout(200);
@@ -166,7 +166,7 @@ Given('该 Agent 未被置顶', { timeout: 30_000 }, async function (this: Custo
await this.page.waitForTimeout(500);
const unpinOption = this.page.getByRole('menuitem', { name: /取消置顶|unpin/i });
await unpinOption.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {
console.log(' ⚠️ 取消置顶选项未找到');
console.info(' ⚠️ 取消置顶选项未找到');
});
if ((await unpinOption.count()) > 0) {
await unpinOption.click();
@@ -177,18 +177,18 @@ Given('该 Agent 未被置顶', { timeout: 30_000 }, async function (this: Custo
await this.page.waitForTimeout(300);
}
console.log(' ✅ Agent 未被置顶');
console.info(' ✅ Agent 未被置顶');
});
Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 确保 Agent 已被置顶...');
console.info(' 📍 Step: 确保 Agent 已被置顶...');
// Check if the agent has a pin icon - if not, pin it first
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
// Pin icon uses lucide-react which adds class "lucide lucide-pin"
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
if ((await pinIcon.count()) === 0) {
console.log(' 📍 Agent 未置顶,开始置顶操作...');
console.info(' 📍 Agent 未置顶,开始置顶操作...');
// Pin it first - right-click on the NavItem Block inside the Link
// The ContextMenuTrigger is attached to the Block component inside the Link
await targetItem.hover();
@@ -198,16 +198,16 @@ Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: Custo
// Debug: check menu visibility
const menuItems = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: 发现 ${menuItems} 个菜单项`);
console.info(` 📍 Debug: 发现 ${menuItems} 个菜单项`);
const pinOption = this.page.getByRole('menuitem', { name: /置顶|pin/i });
await pinOption.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {
console.log(' ⚠️ 置顶选项未找到');
console.info(' ⚠️ 置顶选项未找到');
});
if ((await pinOption.count()) > 0) {
await pinOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击置顶选项');
console.info(' ✅ 已点击置顶选项');
}
// Close menu if still open
await this.page.keyboard.press('Escape');
@@ -218,7 +218,7 @@ Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: Custo
await this.page.waitForTimeout(500);
const pinIconAfter = targetItem.locator('svg[class*="lucide-pin"]');
const isPinned = (await pinIconAfter.count()) > 0;
console.log(` ✅ Agent 已被置顶: ${isPinned}`);
console.info(` ✅ Agent 已被置顶: ${isPinned}`);
});
// ============================================
@@ -226,7 +226,7 @@ Given('该 Agent 已被置顶', { timeout: 30_000 }, async function (this: Custo
// ============================================
When('用户右键点击该 Agent', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击 Agent...');
console.info(' 📍 Step: 右键点击 Agent...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
@@ -241,28 +241,28 @@ When('用户右键点击该 Agent', { timeout: 30_000 }, async function (this: C
// Wait for context menu to appear
const menuItem = this.page.locator('[role="menuitem"]').first();
await menuItem.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {
console.log(' ⚠️ 菜单未出现,重试右键点击...');
console.info(' ⚠️ 菜单未出现,重试右键点击...');
});
// Debug: check what menus are visible
const menuItems = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.info(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.log(' ✅ 已右键点击 Agent');
console.info(' ✅ 已右键点击 Agent');
});
When('用户悬停在该 Agent 上', async function (this: CustomWorld) {
console.log(' 📍 Step: 悬停在 Agent 上...');
console.info(' 📍 Step: 悬停在 Agent 上...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
await targetItem.hover();
await this.page.waitForTimeout(500);
console.log(' ✅ 已悬停在 Agent 上');
console.info(' ✅ 已悬停在 Agent 上');
});
When('用户点击更多操作按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击更多操作按钮...');
console.info(' 📍 Step: 点击更多操作按钮...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const moreButton = targetItem.locator('svg.lucide-ellipsis, svg.lucide-more-horizontal').first();
@@ -282,71 +282,71 @@ When('用户点击更多操作按钮', async function (this: CustomWorld) {
}
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击更多操作按钮');
console.info(' ✅ 已点击更多操作按钮');
});
When('用户在菜单中选择重命名', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择重命名选项...');
console.info(' 📍 Step: 选择重命名选项...');
const renameOption = this.page.getByRole('menuitem', { name: /^(rename|重命名)$/i });
await expect(renameOption).toBeVisible({ timeout: 5000 });
await renameOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已选择重命名选项');
console.info(' ✅ 已选择重命名选项');
});
When('用户在菜单中选择置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择置顶选项...');
console.info(' 📍 Step: 选择置顶选项...');
const pinOption = this.page.getByRole('menuitem', { name: /^(pin|置顶)$/i });
await expect(pinOption).toBeVisible({ timeout: 5000 });
await pinOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已选择置顶选项');
console.info(' ✅ 已选择置顶选项');
});
When('用户在菜单中选择取消置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择取消置顶选项...');
console.info(' 📍 Step: 选择取消置顶选项...');
const unpinOption = this.page.getByRole('menuitem', { name: /^(unpin|取消置顶)$/i });
await expect(unpinOption).toBeVisible({ timeout: 5000 });
await unpinOption.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已选择取消置顶选项');
console.info(' ✅ 已选择取消置顶选项');
});
When('用户在菜单中选择删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择删除选项...');
console.info(' 📍 Step: 选择删除选项...');
const deleteOption = this.page.getByRole('menuitem', { name: /^(delete|删除)$/i });
await expect(deleteOption).toBeVisible({ timeout: 5000 });
await deleteOption.click();
await this.page.waitForTimeout(300);
console.log(' ✅ 已选择删除选项');
console.info(' ✅ 已选择删除选项');
});
When('用户在弹窗中确认删除', async function (this: CustomWorld) {
console.log(' 📍 Step: 确认删除...');
console.info(' 📍 Step: 确认删除...');
const confirmButton = this.page.locator('.ant-modal-confirm-btns button.ant-btn-dangerous');
await expect(confirmButton).toBeVisible({ timeout: 5000 });
await confirmButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已确认删除');
console.info(' ✅ 已确认删除');
});
When('用户输入新的名称 {string}', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
console.info(` 📍 Step: 输入新名称 "${newName}"...`);
await inputNewName.call(this, newName, false);
});
When('用户输入新的名称 {string} 并按 Enter', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
console.info(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
await inputNewName.call(this, newName, true);
});
@@ -355,17 +355,17 @@ When('用户输入新的名称 {string} 并按 Enter', async function (this: Cus
// ============================================
Then('该项名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证名称为 "${expectedName}"...`);
console.info(` 📍 Step: 验证名称为 "${expectedName}"...`);
await this.page.waitForTimeout(1000);
const renamedItem = this.page.getByText(expectedName, { exact: true }).first();
await expect(renamedItem).toBeVisible({ timeout: 5000 });
console.log(` ✅ 名称已更新为 "${expectedName}"`);
console.info(` ✅ 名称已更新为 "${expectedName}"`);
});
Then('Agent 应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示置顶图标...');
console.info(' 📍 Step: 验证显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
@@ -373,11 +373,11 @@ Then('Agent 应该显示置顶图标', async function (this: CustomWorld) {
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
await expect(pinIcon).toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标已显示');
console.info(' ✅ 置顶图标已显示');
});
Then('Agent 不应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不显示置顶图标...');
console.info(' 📍 Step: 验证不显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
@@ -385,11 +385,11 @@ Then('Agent 不应该显示置顶图标', async function (this: CustomWorld) {
const pinIcon = targetItem.locator('svg[class*="lucide-pin"]');
await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标未显示');
console.info(' ✅ 置顶图标未显示');
});
Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent 已移除...');
console.info(' 📍 Step: 验证 Agent 已移除...');
await this.page.waitForTimeout(500);
@@ -400,5 +400,5 @@ Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
}
console.log(' ✅ Agent 已从列表中移除');
console.info(' ✅ Agent 已从列表中移除');
});
+20 -20
View File
@@ -10,7 +10,7 @@ import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TEST_USER } from '../../support/seedTestUser';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
/**
* Create a test chat group directly in database
@@ -35,7 +35,7 @@ async function createTestGroup(title: string = 'Test Group'): Promise<string> {
[groupId, title, TEST_USER.id, now],
);
console.log(` 📍 Created test group in DB: ${groupId}`);
console.info(` 📍 Created test group in DB: ${groupId}`);
return groupId;
} finally {
await client.end();
@@ -47,16 +47,16 @@ async function createTestGroup(title: string = 'Test Group'): Promise<string> {
// ============================================
Given('用户在 Home 页面有一个 Agent Group', async function (this: CustomWorld) {
console.log(' 📍 Step: 在数据库中创建测试 Agent Group...');
console.info(' 📍 Step: 在数据库中创建测试 Agent Group...');
const groupId = await createTestGroup('E2E Test Group');
this.testContext.createdGroupId = groupId;
console.log(' 📍 Step: 导航到 Home 页面...');
console.info(' 📍 Step: 导航到 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 查找新创建的 Agent Group...');
console.info(' 📍 Step: 查找新创建的 Agent Group...');
const groupItem = this.page.locator(`a[href="/group/${groupId}"]`).first();
await expect(groupItem).toBeVisible({ timeout: WAIT_TIMEOUT });
@@ -65,11 +65,11 @@ Given('用户在 Home 页面有一个 Agent Group', async function (this: Custom
this.testContext.targetItemSelector = `a[href="/group/${groupId}"]`;
this.testContext.targetType = 'group';
console.log(` ✅ 找到 Agent Group: ${groupLabel}, id: ${groupId}`);
console.info(` ✅ 找到 Agent Group: ${groupLabel}, id: ${groupId}`);
});
Given('该 Agent Group 未被置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 检查 Agent Group 未被置顶...');
console.info(' 📍 Step: 检查 Agent Group 未被置顶...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
@@ -84,11 +84,11 @@ Given('该 Agent Group 未被置顶', async function (this: CustomWorld) {
await this.page.click('body', { position: { x: 10, y: 10 } });
}
console.log(' ✅ Agent Group 未被置顶');
console.info(' ✅ Agent Group 未被置顶');
});
Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
console.log(' 📍 Step: 确保 Agent Group 已被置顶...');
console.info(' 📍 Step: 确保 Agent Group 已被置顶...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
@@ -103,7 +103,7 @@ Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
await this.page.click('body', { position: { x: 10, y: 10 } });
}
console.log(' ✅ Agent Group 已被置顶');
console.info(' ✅ Agent Group 已被置顶');
});
// ============================================
@@ -111,23 +111,23 @@ Given('该 Agent Group 已被置顶', async function (this: CustomWorld) {
// ============================================
When('用户右键点击该 Agent Group', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击 Agent Group...');
console.info(' 📍 Step: 右键点击 Agent Group...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
await targetItem.click({ button: 'right' });
await this.page.waitForTimeout(500);
console.log(' ✅ 已右键点击 Agent Group');
console.info(' ✅ 已右键点击 Agent Group');
});
When('用户悬停在该 Agent Group 上', async function (this: CustomWorld) {
console.log(' 📍 Step: 悬停在 Agent Group 上...');
console.info(' 📍 Step: 悬停在 Agent Group 上...');
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
await targetItem.hover();
await this.page.waitForTimeout(500);
console.log(' ✅ 已悬停在 Agent Group 上');
console.info(' ✅ 已悬停在 Agent Group 上');
});
// ============================================
@@ -135,34 +135,34 @@ When('用户悬停在该 Agent Group 上', async function (this: CustomWorld) {
// ============================================
Then('Agent Group 应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示置顶图标...');
console.info(' 📍 Step: 验证显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
await expect(pinIcon).toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标已显示');
console.info(' ✅ 置顶图标已显示');
});
Then('Agent Group 不应该显示置顶图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不显示置顶图标...');
console.info(' 📍 Step: 验证不显示置顶图标...');
await this.page.waitForTimeout(500);
const targetItem = this.page.locator(this.testContext.targetItemSelector).first();
const pinIcon = targetItem.locator('svg.lucide-pin');
await expect(pinIcon).not.toBeVisible({ timeout: 5000 });
console.log(' ✅ 置顶图标未显示');
console.info(' ✅ 置顶图标未显示');
});
Then('Agent Group 应该从列表中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent Group 已移除...');
console.info(' 📍 Step: 验证 Agent Group 已移除...');
await this.page.waitForTimeout(500);
const deletedItem = this.page.locator(this.testContext.targetItemSelector);
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
console.log(' ✅ Agent Group 已从列表中移除');
console.info(' ✅ Agent Group 已从列表中移除');
});
+40 -41
View File
@@ -1,4 +1,3 @@
/* eslint-disable no-console */
/**
* Home Starter Steps
*
@@ -25,7 +24,7 @@ let createdDocumentId: string | null = null;
// ============================================
Given('用户在 Home 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 设置 LLM mock...');
console.info(' 📍 Step: 设置 LLM mock...');
// Setup LLM mock before navigation (for agent/group/page builder message)
llmMockManager.setResponse('E2E Test Agent', presetResponses.greeting);
llmMockManager.setResponse('E2E Test Group', presetResponses.greeting);
@@ -35,7 +34,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
);
await llmMockManager.setup(this.page);
console.log(' 📍 Step: 导航到 Home 页面...');
console.info(' 📍 Step: 导航到 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
@@ -45,7 +44,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
createdGroupId = null;
createdDocumentId = null;
console.log(' ✅ 已进入 Home 页面');
console.info(' ✅ 已进入 Home 页面');
});
// ============================================
@@ -53,7 +52,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
// ============================================
When('用户点击创建 Agent 按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击创建 Agent 按钮...');
console.info(' 📍 Step: 点击创建 Agent 按钮...');
// Find the "Create Agent" button by text (supports both English and Chinese)
const createAgentButton = this.page
@@ -66,11 +65,11 @@ When('用户点击创建 Agent 按钮', async function (this: CustomWorld) {
// Wait for mode switch animation and ChatInput scroll-into-view to settle
await this.page.waitForTimeout(800);
console.log(' ✅ 已点击创建 Agent 按钮');
console.info(' ✅ 已点击创建 Agent 按钮');
});
When('用户点击创建 Group 按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击创建 Group 按钮...');
console.info(' 📍 Step: 点击创建 Group 按钮...');
// Find the "Create Group" button by text (supports both English and Chinese)
const createGroupButton = this.page
@@ -83,11 +82,11 @@ When('用户点击创建 Group 按钮', async function (this: CustomWorld) {
// Wait for mode switch animation and ChatInput scroll-into-view to settle
await this.page.waitForTimeout(800);
console.log(' ✅ 已点击创建 Group 按钮');
console.info(' ✅ 已点击创建 Group 按钮');
});
When('用户点击写作按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击写作按钮...');
console.info(' 📍 Step: 点击写作按钮...');
// Find the "Write" button by text (supports both English and Chinese)
const writeButton = this.page.getByRole('button', { name: /write|写作/i }).first();
@@ -98,11 +97,11 @@ When('用户点击写作按钮', async function (this: CustomWorld) {
// Wait for mode switch animation and ChatInput scroll-into-view to settle
await this.page.waitForTimeout(800);
console.log(' ✅ 已点击写作按钮');
console.info(' ✅ 已点击写作按钮');
});
When('用户在输入框中输入 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 在输入框中输入 "${message}"...`);
console.info(` 📍 Step: 在输入框中输入 "${message}"...`);
// The chat input is a contenteditable editor, need to click first then type.
// Target the contenteditable element INSIDE the ChatInput container directly,
@@ -115,11 +114,11 @@ When('用户在输入框中输入 {string}', async function (this: CustomWorld,
await this.page.waitForTimeout(300);
await this.page.keyboard.type(message, { delay: 30 });
console.log(` ✅ 已输入 "${message}"`);
console.info(` ✅ 已输入 "${message}"`);
});
When('用户按 Enter 发送', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 按 Enter 发送...');
console.info(' 📍 Step: 按 Enter 发送...');
// Wait for editor's debounced onChange (100ms default) to sync inputMessage to store.
// The send() function reads directly from the editor as a fallback, but this wait
@@ -143,20 +142,20 @@ When('用户按 Enter 发送', { timeout: 30_000 }, async function (this: Custom
const agentMatch = currentUrl.match(/\/agent\/([^/]+)/);
if (agentMatch) {
createdAgentId = agentMatch[1];
console.log(` 📍 Created agent ID: ${createdAgentId}`);
console.info(` 📍 Created agent ID: ${createdAgentId}`);
}
const groupMatch = currentUrl.match(/\/group\/([^/]+)/);
if (groupMatch) {
createdGroupId = groupMatch[1];
console.log(` 📍 Created group ID: ${createdGroupId}`);
console.info(` 📍 Created group ID: ${createdGroupId}`);
}
console.log(' ✅ 已发送消息');
console.info(' ✅ 已发送消息');
});
When('用户按 Enter 发送创建文档', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 按 Enter 发送创建文档...');
console.info(' 📍 Step: 按 Enter 发送创建文档...');
// Wait for editor's debounced onChange (100ms default) to sync inputMessage to store
await this.page.waitForTimeout(200);
@@ -177,20 +176,20 @@ When('用户按 Enter 发送创建文档', { timeout: 30_000 }, async function (
const pageMatch = currentUrl.match(/\/page\/([^/?]+)/);
if (pageMatch) {
createdDocumentId = pageMatch[1];
console.log(` 📍 Created document ID: ${createdDocumentId}`);
console.info(` 📍 Created document ID: ${createdDocumentId}`);
}
console.log(' ✅ 已发送并创建文档');
console.info(' ✅ 已发送并创建文档');
});
When('用户返回 Home 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 返回 Home 页面...');
console.info(' 📍 Step: 返回 Home 页面...');
await this.page.goto('/');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' ✅ 已返回 Home 页面');
console.info(' ✅ 已返回 Home 页面');
});
// ============================================
@@ -198,27 +197,27 @@ When('用户返回 Home 页面', async function (this: CustomWorld) {
// ============================================
Then('页面应该跳转到 Agent 的 profile 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Agent profile 页面...');
console.info(' 📍 Step: 验证页面跳转到 Agent profile 页面...');
// Check current URL matches /agent/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/agent\/[^/]+\/profile/);
console.log(' ✅ 已跳转到 Agent profile 页面');
console.info(' ✅ 已跳转到 Agent profile 页面');
});
Then('页面应该跳转到 Group 的 profile 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Group profile 页面...');
console.info(' 📍 Step: 验证页面跳转到 Group profile 页面...');
// Check current URL matches /group/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/group\/[^/]+\/profile/);
console.log(' ✅ 已跳转到 Group profile 页面');
console.info(' ✅ 已跳转到 Group profile 页面');
});
Then('新创建的 Agent 应该在侧边栏中显示', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent 在侧边栏中显示...');
console.info(' 📍 Step: 验证 Agent 在侧边栏中显示...');
// Wait for sidebar to be visible and data to load
await this.page.waitForTimeout(1500);
@@ -231,17 +230,17 @@ Then('新创建的 Agent 应该在侧边栏中显示', async function (this: Cus
const agentLink = this.page.locator(`a[href="/agent/${createdAgentId}"]`).first();
await expect(agentLink).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(` ✅ 找到 Agent 链接: /agent/${createdAgentId}`);
console.info(` ✅ 找到 Agent 链接: /agent/${createdAgentId}`);
// Get the aria-label or text content to verify it's the correct agent
const ariaLabel = await agentLink.getAttribute('aria-label');
console.log(` 📍 Agent aria-label: ${ariaLabel}`);
console.info(` 📍 Agent aria-label: ${ariaLabel}`);
console.log(' ✅ Agent 已在侧边栏中显示');
console.info(' ✅ Agent 已在侧边栏中显示');
});
Then('新创建的 Group 应该在侧边栏中显示', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Group 在侧边栏中显示...');
console.info(' 📍 Step: 验证 Group 在侧边栏中显示...');
// Wait for sidebar to be visible and data to load
await this.page.waitForTimeout(1500);
@@ -254,17 +253,17 @@ Then('新创建的 Group 应该在侧边栏中显示', async function (this: Cus
const groupLink = this.page.locator(`a[href="/group/${createdGroupId}"]`).first();
await expect(groupLink).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(` ✅ 找到 Group 链接: /group/${createdGroupId}`);
console.info(` ✅ 找到 Group 链接: /group/${createdGroupId}`);
// Get the aria-label or text content to verify it's the correct group
const ariaLabel = await groupLink.getAttribute('aria-label');
console.log(` 📍 Group aria-label: ${ariaLabel}`);
console.info(` 📍 Group aria-label: ${ariaLabel}`);
console.log(' ✅ Group 已在侧边栏中显示');
console.info(' ✅ Group 已在侧边栏中显示');
});
Then('页面应该跳转到文档编辑页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到文档编辑页面...');
console.info(' 📍 Step: 验证页面跳转到文档编辑页面...');
// Check current URL matches /page/{id} pattern
const currentUrl = this.page.url();
@@ -274,11 +273,11 @@ Then('页面应该跳转到文档编辑页面', async function (this: CustomWorl
throw new Error('Document ID was not captured during creation');
}
console.log(` ✅ 已跳转到文档编辑页面: /page/${createdDocumentId}`);
console.info(` ✅ 已跳转到文档编辑页面: /page/${createdDocumentId}`);
});
Then('Page Agent 应该收到用户的提示词', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Page Agent 收到用户的提示词...');
console.info(' 📍 Step: 验证 Page Agent 收到用户的提示词...');
// Wait for the page to fully load and Page Agent panel to appear
await this.page.waitForTimeout(2000);
@@ -291,10 +290,10 @@ Then('Page Agent 应该收到用户的提示词', async function (this: CustomWo
const messageVisible = await userMessage.isVisible().catch(() => false);
if (messageVisible) {
console.log(' ✅ 找到用户发送的提示词');
console.info(' ✅ 找到用户发送的提示词');
} else {
// Alternative: check if there's any chat content indicating the message was sent
console.log(' ⚠️ 用户消息可能在聊天面板中,但未直接可见');
console.info(' ⚠️ 用户消息可能在聊天面板中,但未直接可见');
}
// Verify that the Page Agent responded (mock response should appear)
@@ -306,10 +305,10 @@ Then('Page Agent 应该收到用户的提示词', async function (this: CustomWo
const responseVisible = await aiResponse.isVisible().catch(() => false);
if (responseVisible) {
console.log(' ✅ Page Agent 已响应用户的提示词');
console.info(' ✅ Page Agent 已响应用户的提示词');
} else {
console.log(' ⚠️ Page Agent 响应可能正在生成或在其他位置');
console.info(' ⚠️ Page Agent 响应可能正在生成或在其他位置');
}
console.log(' ✅ Page Agent 验证完成');
console.info(' ✅ Page Agent 验证完成');
});
+17 -17
View File
@@ -1,9 +1,9 @@
import { After, AfterAll, Before, BeforeAll, Status, setDefaultTimeout } from '@cucumber/cucumber';
import { type Cookie, chromium } from 'playwright';
import { After, AfterAll, Before, BeforeAll, setDefaultTimeout, Status } from '@cucumber/cucumber';
import { chromium, type Cookie } from 'playwright';
import { TEST_USER, seedTestUser } from '../support/seedTestUser';
import { seedTestUser, TEST_USER } from '../support/seedTestUser';
import { startWebServer, stopWebServer } from '../support/webServer';
import { CustomWorld } from '../support/world';
import { type CustomWorld } from '../support/world';
process.env['E2E'] = '1';
// Set default timeout for all steps to 10 seconds
@@ -14,12 +14,12 @@ let baseUrl: string;
let sessionCookies: Cookie[] = [];
BeforeAll({ timeout: 600_000 }, async function () {
console.log('🚀 Starting E2E test suite...');
console.info('🚀 Starting E2E test suite...');
const PORT = process.env.PORT ? Number(process.env.PORT) : 3006;
baseUrl = process.env.BASE_URL || `http://localhost:${PORT}`;
console.log(`Base URL: ${baseUrl}`);
console.info(`Base URL: ${baseUrl}`);
// Seed test user before starting web server
await seedTestUser();
@@ -35,7 +35,7 @@ BeforeAll({ timeout: 600_000 }, async function () {
}
// Login once and cache the session cookies
console.log('🔐 Performing one-time login to cache session...');
console.info('🔐 Performing one-time login to cache session...');
const browser = await chromium.launch({ headless: process.env.HEADLESS !== 'false' });
const context = await browser.newContext();
@@ -55,14 +55,14 @@ BeforeAll({ timeout: 600_000 }, async function () {
const emailInputVisible = await emailInput.isVisible().catch(() => false);
if (!emailInputVisible) {
console.log(
console.info(
'⚠️ Login form not available, skipping authentication (tests requiring auth may fail)',
);
return;
}
// Step 1: Enter email
console.log(' Step 1: Entering email...');
console.info(' Step 1: Entering email...');
await emailInput.fill(TEST_USER.email);
// Click the next button
@@ -70,7 +70,7 @@ BeforeAll({ timeout: 600_000 }, async function () {
await nextButton.click();
// Step 2: Wait for password step and enter password
console.log(' Step 2: Entering password...');
console.info(' Step 2: Entering password...');
const passwordInput = page
.locator('input[id="password"], input[name="password"], input[type="password"]')
.first();
@@ -87,7 +87,7 @@ BeforeAll({ timeout: 600_000 }, async function () {
// Cache the session cookies
sessionCookies = await context.cookies();
console.log(`✅ Login successful, cached ${sessionCookies.length} cookies`);
console.info(`✅ Login successful, cached ${sessionCookies.length} cookies`);
} finally {
await browser.close();
}
@@ -104,7 +104,7 @@ Before(async function (this: CustomWorld, { pickle }) {
tag.name.startsWith('@PAGE-') ||
tag.name.startsWith('@ROUTES-'),
);
console.log(`\n📝 Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
console.info(`\n📝 Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
// Setup API mocks before any page navigation
// await mockManager.setup(this.page);
@@ -112,7 +112,7 @@ Before(async function (this: CustomWorld, { pickle }) {
// Set cached session cookies to skip login
if (sessionCookies.length > 0) {
await this.browserContext.addCookies(sessionCookies);
console.log('🍪 Session cookies restored');
console.info('🍪 Session cookies restored');
}
});
@@ -140,19 +140,19 @@ After(async function (this: CustomWorld, { pickle, result }) {
this.attach(`JavaScript Errors:\n${errors}`, 'text/plain');
}
console.log(`❌ Failed: ${pickle.name}`);
console.info(`❌ Failed: ${pickle.name}`);
if (result.message) {
console.log(` Error: ${result.message}`);
console.info(` Error: ${result.message}`);
}
} else if (result?.status === Status.PASSED) {
console.log(`✅ Passed: ${pickle.name}`);
console.info(`✅ Passed: ${pickle.name}`);
}
await this.cleanup();
});
AfterAll(async function () {
console.log('\n🏁 Test suite completed');
console.info('\n🏁 Test suite completed');
// Stop web server if we started it
if (!process.env.BASE_URL && process.env.CI) {
+35 -35
View File
@@ -6,7 +6,7 @@
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
import { type CustomWorld } from '../../support/world';
// ============================================
// Helper Functions
@@ -26,7 +26,7 @@ async function getEditor(world: CustomWorld) {
// ============================================
When('用户点击编辑器内容区域', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击编辑器内容区域...');
console.info(' 📍 Step: 点击编辑器内容区域...');
const editorContent = this.page.locator('[contenteditable="true"]').first();
if ((await editorContent.count()) > 0) {
@@ -37,21 +37,21 @@ When('用户点击编辑器内容区域', async function (this: CustomWorld) {
}
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击编辑器内容区域');
console.info(' ✅ 已点击编辑器内容区域');
});
When('用户按下 Enter 键', async function (this: CustomWorld) {
console.log(' 📍 Step: 按下 Enter 键...');
console.info(' 📍 Step: 按下 Enter 键...');
await this.page.keyboard.press('Enter');
// Wait for debounce save (1000ms) + buffer
await this.page.waitForTimeout(1500);
console.log(' ✅ 已按下 Enter 键');
console.info(' ✅ 已按下 Enter 键');
});
When('用户输入文本 {string}', async function (this: CustomWorld, text: string) {
console.log(` 📍 Step: 输入文本 "${text}"...`);
console.info(` 📍 Step: 输入文本 "${text}"...`);
await this.page.keyboard.type(text, { delay: 30 });
await this.page.waitForTimeout(300);
@@ -59,11 +59,11 @@ When('用户输入文本 {string}', async function (this: CustomWorld, text: str
// Store for later verification
this.testContext.inputText = text;
console.log(` ✅ 已输入文本 "${text}"`);
console.info(` ✅ 已输入文本 "${text}"`);
});
When('用户在编辑器中输入内容 {string}', async function (this: CustomWorld, content: string) {
console.log(` 📍 Step: 在编辑器中输入内容 "${content}"...`);
console.info(` 📍 Step: 在编辑器中输入内容 "${content}"...`);
const editor = await getEditor(this);
await editor.click();
@@ -73,16 +73,16 @@ When('用户在编辑器中输入内容 {string}', async function (this: CustomW
this.testContext.inputText = content;
console.log(` ✅ 已输入内容 "${content}"`);
console.info(` ✅ 已输入内容 "${content}"`);
});
When('用户选中所有内容', async function (this: CustomWorld) {
console.log(' 📍 Step: 选中所有内容...');
console.info(' 📍 Step: 选中所有内容...');
await this.page.keyboard.press(`${this.modKey}+A`);
await this.page.waitForTimeout(300);
console.log(' ✅ 已选中所有内容');
console.info(' ✅ 已选中所有内容');
});
// ============================================
@@ -90,17 +90,17 @@ When('用户选中所有内容', async function (this: CustomWorld) {
// ============================================
When('用户输入斜杠 {string}', async function (this: CustomWorld, slash: string) {
console.log(` 📍 Step: 输入斜杠 "${slash}"...`);
console.info(` 📍 Step: 输入斜杠 "${slash}"...`);
await this.page.keyboard.type(slash, { delay: 50 });
// Wait for slash menu to appear
await this.page.waitForTimeout(500);
console.log(` ✅ 已输入斜杠 "${slash}"`);
console.info(` ✅ 已输入斜杠 "${slash}"`);
});
When('用户输入斜杠命令 {string}', async function (this: CustomWorld, command: string) {
console.log(` 📍 Step: 输入斜杠命令 "${command}"...`);
console.info(` 📍 Step: 输入斜杠命令 "${command}"...`);
// The command format is "/shortcut" (e.g., "/h1", "/codeblock")
// First type the slash and wait for menu
@@ -112,7 +112,7 @@ When('用户输入斜杠命令 {string}', async function (this: CustomWorld, com
await this.page.keyboard.type(shortcut, { delay: 80 });
await this.page.waitForTimeout(500); // Wait for menu to filter
console.log(` ✅ 已输入斜杠命令 "${command}"`);
console.info(` ✅ 已输入斜杠命令 "${command}"`);
});
// ============================================
@@ -120,14 +120,14 @@ When('用户输入斜杠命令 {string}', async function (this: CustomWorld, com
// ============================================
When('用户按下快捷键 {string}', async function (this: CustomWorld, shortcut: string) {
console.log(` 📍 Step: 按下快捷键 "${shortcut}"...`);
console.info(` 📍 Step: 按下快捷键 "${shortcut}"...`);
// Convert Meta to platform-specific modifier key for cross-platform support
const platformShortcut = shortcut.replaceAll('Meta', this.modKey);
await this.page.keyboard.press(platformShortcut);
await this.page.waitForTimeout(300);
console.log(` ✅ 已按下快捷键 "${platformShortcut}"`);
console.info(` ✅ 已按下快捷键 "${platformShortcut}"`);
});
// ============================================
@@ -135,7 +135,7 @@ When('用户按下快捷键 {string}', async function (this: CustomWorld, shortc
// ============================================
Then('编辑器应该显示输入的文本', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器显示输入的文本...');
console.info(' 📍 Step: 验证编辑器显示输入的文本...');
const editor = await getEditor(this);
const text = this.testContext.inputText;
@@ -144,17 +144,17 @@ Then('编辑器应该显示输入的文本', async function (this: CustomWorld)
const editorText = await editor.textContent();
expect(editorText).toContain(text);
console.log(` ✅ 编辑器显示文本: "${text}"`);
console.info(` ✅ 编辑器显示文本: "${text}"`);
});
Then('编辑器应该显示 {string}', async function (this: CustomWorld, expectedText: string) {
console.log(` 📍 Step: 验证编辑器显示 "${expectedText}"...`);
console.info(` 📍 Step: 验证编辑器显示 "${expectedText}"...`);
const editor = await getEditor(this);
const editorText = await editor.textContent();
expect(editorText).toContain(expectedText);
console.log(` ✅ 编辑器显示 "${expectedText}"`);
console.info(` ✅ 编辑器显示 "${expectedText}"`);
});
// ============================================
@@ -162,7 +162,7 @@ Then('编辑器应该显示 {string}', async function (this: CustomWorld, expect
// ============================================
Then('应该显示斜杠命令菜单', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示斜杠命令菜单...');
console.info(' 📍 Step: 验证显示斜杠命令菜单...');
// The slash menu should be visible
// Look for menu with heading options, list options, etc.
@@ -189,11 +189,11 @@ Then('应该显示斜杠命令菜单', async function (this: CustomWorld) {
expect(menuFound).toBe(true);
console.log(' ✅ 斜杠命令菜单已显示');
console.info(' ✅ 斜杠命令菜单已显示');
});
Then('编辑器应该包含一级标题', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含一级标题...');
console.info(' 📍 Step: 验证编辑器包含一级标题...');
// Check for h1 element in the editor
const editor = await getEditor(this);
@@ -201,22 +201,22 @@ Then('编辑器应该包含一级标题', async function (this: CustomWorld) {
await expect(h1).toBeVisible({ timeout: 5000 });
console.log(' ✅ 编辑器包含一级标题');
console.info(' ✅ 编辑器包含一级标题');
});
Then('编辑器应该包含无序列表', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含无序列表...');
console.info(' 📍 Step: 验证编辑器包含无序列表...');
const editor = await getEditor(this);
const ul = editor.locator('ul');
await expect(ul).toBeVisible({ timeout: 5000 });
console.log(' ✅ 编辑器包含无序列表');
console.info(' ✅ 编辑器包含无序列表');
});
Then('编辑器应该包含任务列表', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含任务列表...');
console.info(' 📍 Step: 验证编辑器包含任务列表...');
const editor = await getEditor(this);
@@ -245,11 +245,11 @@ Then('编辑器应该包含任务列表', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 编辑器包含任务列表');
console.info(' ✅ 编辑器包含任务列表');
});
Then('编辑器应该包含代码块', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含代码块...');
console.info(' 📍 Step: 验证编辑器包含代码块...');
// Code block might be rendered inside the editor OR as a sibling element
// CodeMirror renders its own container
@@ -287,7 +287,7 @@ Then('编辑器应该包含代码块', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 编辑器包含代码块');
console.info(' ✅ 编辑器包含代码块');
});
// ============================================
@@ -295,7 +295,7 @@ Then('编辑器应该包含代码块', async function (this: CustomWorld) {
// ============================================
Then('选中的文本应该被加粗', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文本已加粗...');
console.info(' 📍 Step: 验证文本已加粗...');
const editor = await getEditor(this);
@@ -318,11 +318,11 @@ Then('选中的文本应该被加粗', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 文本已加粗');
console.info(' ✅ 文本已加粗');
});
Then('选中的文本应该变为斜体', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文本已斜体...');
console.info(' 📍 Step: 验证文本已斜体...');
const editor = await getEditor(this);
@@ -340,5 +340,5 @@ Then('选中的文本应该变为斜体', async function (this: CustomWorld) {
expect(found).toBe(true);
console.log(' ✅ 文本已斜体');
console.info(' ✅ 文本已斜体');
});
+42 -42
View File
@@ -6,14 +6,14 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Given Steps
// ============================================
Given('用户打开一个文稿编辑器', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建并打开一个文稿...');
console.info(' 📍 Step: 创建并打开一个文稿...');
// Navigate to page module
await this.page.goto('/page');
@@ -30,11 +30,11 @@ Given('用户打开一个文稿编辑器', async function (this: CustomWorld) {
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(500);
console.log(' ✅ 已打开文稿编辑器');
console.info(' ✅ 已打开文稿编辑器');
});
Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 创建并打开一个带 Emoji 的文稿...');
console.info(' 📍 Step: 创建并打开一个带 Emoji 的文稿...');
// First create and open a page
await this.page.goto('/page');
@@ -50,7 +50,7 @@ Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWo
await this.page.waitForTimeout(500);
// Add emoji by clicking the "Choose Icon" button
console.log(' 📍 Step: 添加 Emoji 图标...');
console.info(' 📍 Step: 添加 Emoji 图标...');
// Hover over title section to show the button
const titleSection = this.page.locator('textarea').first().locator('xpath=ancestor::div[1]');
@@ -77,7 +77,7 @@ Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWo
await this.page.waitForTimeout(500);
}
console.log(' ✅ 已打开带 Emoji 的文稿');
console.info(' ✅ 已打开带 Emoji 的文稿');
});
// ============================================
@@ -85,18 +85,18 @@ Given('用户打开一个带有 Emoji 的文稿', async function (this: CustomWo
// ============================================
When('用户点击标题输入框', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击标题输入框...');
console.info(' 📍 Step: 点击标题输入框...');
const titleInput = this.page.locator('textarea').first();
await expect(titleInput).toBeVisible({ timeout: 5000 });
await titleInput.click();
await this.page.waitForTimeout(300);
console.log(' ✅ 已点击标题输入框');
console.info(' ✅ 已点击标题输入框');
});
When('用户输入标题 {string}', async function (this: CustomWorld, title: string) {
console.log(` 📍 Step: 输入标题 "${title}"...`);
console.info(` 📍 Step: 输入标题 "${title}"...`);
const titleInput = this.page.locator('textarea').first();
@@ -109,11 +109,11 @@ When('用户输入标题 {string}', async function (this: CustomWorld, title: st
// Store for later verification
this.testContext.expectedTitle = title;
console.log(` ✅ 已输入标题 "${title}"`);
console.info(` ✅ 已输入标题 "${title}"`);
});
When('用户清空标题内容', async function (this: CustomWorld) {
console.log(' 📍 Step: 清空标题内容...');
console.info(' 📍 Step: 清空标题内容...');
const titleInput = this.page.locator('textarea').first();
await titleInput.click();
@@ -125,7 +125,7 @@ When('用户清空标题内容', async function (this: CustomWorld) {
await this.page.click('body', { position: { x: 400, y: 400 } });
await this.page.waitForTimeout(1500);
console.log(' ✅ 已清空标题内容');
console.info(' ✅ 已清空标题内容');
});
// ============================================
@@ -133,7 +133,7 @@ When('用户清空标题内容', async function (this: CustomWorld) {
// ============================================
When('用户点击选择图标按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击选择图标按钮...');
console.info(' 📍 Step: 点击选择图标按钮...');
// Hover to show the button
const titleSection = this.page.locator('textarea').first().locator('xpath=ancestor::div[1]');
@@ -146,11 +146,11 @@ When('用户点击选择图标按钮', async function (this: CustomWorld) {
await chooseIconButton.click();
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击选择图标按钮');
console.info(' ✅ 已点击选择图标按钮');
});
When('用户选择一个 Emoji', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择一个 Emoji...');
console.info(' 📍 Step: 选择一个 Emoji...');
// Wait for emoji picker to be visible
await this.page.waitForTimeout(800);
@@ -171,28 +171,28 @@ When('用户选择一个 Emoji', async function (this: CustomWorld) {
for (const selector of emojiSelectors) {
const emojis = this.page.locator(selector);
const count = await emojis.count();
console.log(` 📍 Debug: Found ${count} elements with selector "${selector}"`);
console.info(` 📍 Debug: Found ${count} elements with selector "${selector}"`);
if (count > 0) {
// Click a random emoji (not the first to avoid default)
const index = Math.min(5, count - 1);
await emojis.nth(index).click();
clicked = true;
console.log(` 📍 Debug: Clicked emoji at index ${index}`);
console.info(` 📍 Debug: Clicked emoji at index ${index}`);
break;
}
}
// Fallback: try to find any clickable element in the emoji popover
if (!clicked) {
console.log(' 📍 Debug: Trying fallback - looking for emoji in popover');
console.info(' 📍 Debug: Trying fallback - looking for emoji in popover');
const popover = this.page.locator('.ant-popover-inner, [class*="popover"]').first();
if ((await popover.count()) > 0) {
// Find spans that look like emojis (single character with emoji range)
const emojiSpans = popover.locator('span').filter({
hasText: /^[\p{Emoji}]$/u,
hasText: /^\p{Emoji}$/u,
});
const count = await emojiSpans.count();
console.log(` 📍 Debug: Found ${count} emoji spans in popover`);
console.info(` 📍 Debug: Found ${count} emoji spans in popover`);
if (count > 0) {
await emojiSpans.nth(Math.min(5, count - 1)).click();
clicked = true;
@@ -201,16 +201,16 @@ When('用户选择一个 Emoji', async function (this: CustomWorld) {
}
if (!clicked) {
console.log(' ⚠️ Could not find emoji button, test may fail');
console.info(' ⚠️ Could not find emoji button, test may fail');
}
await this.page.waitForTimeout(1000);
console.log(' ✅ 已选择 Emoji');
console.info(' ✅ 已选择 Emoji');
});
When('用户点击已有的 Emoji 图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击已有的 Emoji 图标...');
console.info(' 📍 Step: 点击已有的 Emoji 图标...');
// The emoji is displayed in an Avatar component with square shape
// Look for the emoji display element near the title
@@ -230,11 +230,11 @@ When('用户点击已有的 Emoji 图标', async function (this: CustomWorld) {
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击 Emoji 图标');
console.info(' ✅ 已点击 Emoji 图标');
});
When('用户选择另一个 Emoji', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择另一个 Emoji...');
console.info(' 📍 Step: 选择另一个 Emoji...');
// Same as selecting an emoji, but choose a different index
await this.page.waitForTimeout(500);
@@ -254,11 +254,11 @@ When('用户选择另一个 Emoji', async function (this: CustomWorld) {
await this.page.waitForTimeout(1000);
console.log(' ✅ 已选择另一个 Emoji');
console.info(' ✅ 已选择另一个 Emoji');
});
When('用户点击删除图标按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击删除图标按钮...');
console.info(' 📍 Step: 点击删除图标按钮...');
// Look for delete button in the emoji picker
const deleteButton = this.page.getByRole('button', { name: /delete|删除/i });
@@ -274,7 +274,7 @@ When('用户点击删除图标按钮', async function (this: CustomWorld) {
await this.page.waitForTimeout(1000);
console.log(' ✅ 已点击删除图标按钮');
console.info(' ✅ 已点击删除图标按钮');
});
// ============================================
@@ -282,7 +282,7 @@ When('用户点击删除图标按钮', async function (this: CustomWorld) {
// ============================================
Then('文稿标题应该更新为 {string}', async function (this: CustomWorld, expectedTitle: string) {
console.log(` 📍 Step: 验证标题为 "${expectedTitle}"...`);
console.info(` 📍 Step: 验证标题为 "${expectedTitle}"...`);
const titleInput = this.page.locator('textarea').first();
await expect(titleInput).toHaveValue(expectedTitle, { timeout: 5000 });
@@ -295,16 +295,16 @@ Then('文稿标题应该更新为 {string}', async function (this: CustomWorld,
// Sidebar might take longer to sync
try {
await expect(sidebarItem).toBeVisible({ timeout: 3000 });
console.log(' ✅ 侧边栏标题也已更新');
console.info(' ✅ 侧边栏标题也已更新');
} catch {
console.log(' ⚠️ 侧边栏标题可能未同步(非关键)');
console.info(' ⚠️ 侧边栏标题可能未同步(非关键)');
}
console.log(` ✅ 标题已更新为 "${expectedTitle}"`);
console.info(` ✅ 标题已更新为 "${expectedTitle}"`);
});
Then('应该显示标题占位符', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示占位符...');
console.info(' 📍 Step: 验证显示占位符...');
const titleInput = this.page.locator('textarea').first();
@@ -317,11 +317,11 @@ Then('应该显示标题占位符', async function (this: CustomWorld) {
const isEmptyOrDefault = value === '' || value === 'Untitled' || value === '无标题';
expect(isEmptyOrDefault).toBe(true);
console.log(` ✅ 显示占位符: "${placeholder}", 当前值: "${value}"`);
console.info(` ✅ 显示占位符: "${placeholder}", 当前值: "${value}"`);
});
Then('文稿应该显示所选的 Emoji 图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示 Emoji 图标...');
console.info(' 📍 Step: 验证显示 Emoji 图标...');
// Look for emoji display - could be in Avatar or span element
// The emoji picker uses @lobehub/ui which may render differently
@@ -349,11 +349,11 @@ Then('文稿应该显示所选的 Emoji 图标', async function (this: CustomWor
expect(found).toBe(true);
console.log(' ✅ 文稿显示 Emoji 图标');
console.info(' ✅ 文稿显示 Emoji 图标');
});
Then('文稿图标应该更新为新的 Emoji', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Emoji 图标已更新...');
console.info(' 📍 Step: 验证 Emoji 图标已更新...');
// Look for emoji display
const emojiSelectors = [
@@ -380,11 +380,11 @@ Then('文稿图标应该更新为新的 Emoji', async function (this: CustomWorl
expect(found).toBe(true);
console.log(' ✅ Emoji 图标已更新');
console.info(' ✅ Emoji 图标已更新');
});
Then('文稿不应该显示 Emoji 图标', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证不显示 Emoji 图标...');
console.info(' 📍 Step: 验证不显示 Emoji 图标...');
// After deletion, the "Choose Icon" button should be visible
// and the emoji avatar should be hidden
@@ -400,11 +400,11 @@ Then('文稿不应该显示 Emoji 图标', async function (this: CustomWorld) {
// Either the button is visible OR the emoji avatar is not visible
try {
await expect(chooseIconButton).toBeVisible({ timeout: 3000 });
console.log(' ✅ 选择图标按钮可见,说明 Emoji 已删除');
console.info(' ✅ 选择图标按钮可见,说明 Emoji 已删除');
} catch {
// Emoji might still be there but different
console.log(' ⚠️ 无法确认 Emoji 是否删除');
console.info(' ⚠️ 无法确认 Emoji 是否删除');
}
console.log(' ✅ 验证完成');
console.info(' ✅ 验证完成');
});
+45 -45
View File
@@ -10,7 +10,7 @@
import { Given, Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld, WAIT_TIMEOUT } from '../../support/world';
import { type CustomWorld, WAIT_TIMEOUT } from '../../support/world';
// ============================================
// Helper Functions
@@ -89,7 +89,7 @@ async function inputPageName(
}
await this.page.waitForTimeout(1000);
console.log(` ✅ 已输入新名称 "${newName}"`);
console.info(` ✅ 已输入新名称 "${newName}"`);
}
// ============================================
@@ -97,21 +97,21 @@ async function inputPageName(
// ============================================
Given('用户在 Page 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 导航到 Page 页面...');
console.info(' 📍 Step: 导航到 Page 页面...');
await this.page.goto('/page');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' ✅ 已进入 Page 页面');
console.info(' ✅ 已进入 Page 页面');
});
Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 导航到 Page 页面...');
console.info(' 📍 Step: 导航到 Page 页面...');
await this.page.goto('/page');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 通过 UI 创建新文稿...');
console.info(' 📍 Step: 通过 UI 创建新文稿...');
// Click the new page button to create via UI (ensures proper server-side creation)
const newPageButton = this.page.locator('svg.lucide-square-pen').first();
await newPageButton.click();
@@ -123,37 +123,37 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
// Create a unique title for this test page
const uniqueTitle = `E2E Page ${Date.now()}`;
console.log(` 📍 Step: 重命名为唯一标题 "${uniqueTitle}"...`);
console.info(` 📍 Step: 重命名为唯一标题 "${uniqueTitle}"...`);
// Find the new page in sidebar (use link selector to avoid matching editor title)
// Sidebar page items are rendered as <a href="/page/xxx"> links
// Debug: check how many links exist
const allPageLinks = this.page.locator('a[href^="/page/"]');
const linkCount = await allPageLinks.count();
console.log(` 📍 Debug: Found ${linkCount} page links in sidebar`);
console.info(` 📍 Debug: Found ${linkCount} page links in sidebar`);
// Find the Untitled page link
const pageItem = allPageLinks.filter({ hasText: /Untitled|无标题/ }).first();
const pageItemCount = await allPageLinks.filter({ hasText: /Untitled|无标题/ }).count();
console.log(` 📍 Debug: Found ${pageItemCount} Untitled page links`);
console.info(` 📍 Debug: Found ${pageItemCount} Untitled page links`);
await expect(pageItem).toBeVisible({ timeout: 5000 });
console.log(' 📍 Debug: Page item is visible');
console.info(' 📍 Debug: Page item is visible');
// Right-click to open context menu and rename
await pageItem.click({ button: 'right' });
console.log(' 📍 Debug: Right-clicked on page item');
console.info(' 📍 Debug: Right-clicked on page item');
await this.page.waitForTimeout(500);
// Debug: check menu items
const menuItemCount = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: Found ${menuItemCount} menu items after right-click`);
console.info(` 📍 Debug: Found ${menuItemCount} menu items after right-click`);
const renameOption = this.page.getByRole('menuitem', { name: /rename|重命名/i });
await expect(renameOption).toBeVisible({ timeout: 5000 });
console.log(' 📍 Debug: Rename option is visible');
console.info(' 📍 Debug: Rename option is visible');
await renameOption.click();
console.log(' 📍 Debug: Clicked rename option');
console.info(' 📍 Debug: Clicked rename option');
await this.page.waitForTimeout(800);
// Wait for rename popover to appear and find the input
@@ -169,7 +169,7 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
for (const selector of inputSelectors) {
const inputs = this.page.locator(selector);
const count = await inputs.count();
console.log(` 📍 Debug: Selector "${selector}" found ${count} inputs`);
console.info(` 📍 Debug: Selector "${selector}" found ${count} inputs`);
if (count > 0) {
// Find the visible one
for (let i = 0; i < count; i++) {
@@ -192,14 +192,14 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
throw new Error('Could not find popover input for renaming');
}
console.log(' 📍 Debug: Popover input found');
console.info(' 📍 Debug: Popover input found');
await expect(popoverInput).toBeVisible({ timeout: 5000 });
// Clear and input the unique name
await popoverInput.click();
await popoverInput.clear();
await popoverInput.fill(uniqueTitle);
console.log(` 📍 Debug: Filled input with "${uniqueTitle}"`);
console.info(` 📍 Debug: Filled input with "${uniqueTitle}"`);
// Press Enter to confirm
await popoverInput.press('Enter');
@@ -213,16 +213,16 @@ Given('用户在 Page 页面有一个文稿', async function (this: CustomWorld)
this.testContext.targetItemTitle = uniqueTitle;
this.testContext.targetType = 'page';
console.log(` ✅ 找到文稿: ${uniqueTitle}`);
console.info(` ✅ 找到文稿: ${uniqueTitle}`);
});
Given('用户在 Page 页面有一个文稿 {string}', async function (this: CustomWorld, title: string) {
console.log(' 📍 Step: 导航到 Page 页面...');
console.info(' 📍 Step: 导航到 Page 页面...');
await this.page.goto('/page');
await this.page.waitForLoadState('networkidle', { timeout: 15_000 });
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 通过 UI 创建新文稿...');
console.info(' 📍 Step: 通过 UI 创建新文稿...');
// Click the new page button to create via UI
const newPageButton = this.page.locator('svg.lucide-square-pen').first();
await newPageButton.click();
@@ -234,7 +234,7 @@ Given('用户在 Page 页面有一个文稿 {string}', async function (this: Cus
// Default title is "无标题" (Untitled) - support both languages
const defaultTitleRegex = /^(无标题|Untitled)$/;
console.log(` 📍 Step: 通过右键菜单重命名文稿为 "${title}"...`);
console.info(` 📍 Step: 通过右键菜单重命名文稿为 "${title}"...`);
// Find the new page in sidebar (use link selector to avoid matching editor title)
// Sidebar page items are rendered as <a href="/page/xxx"> links
const pageItem = this.page
@@ -296,14 +296,14 @@ Given('用户在 Page 页面有一个文稿 {string}', async function (this: Cus
await popoverInput.press('Enter');
await this.page.waitForTimeout(1000);
console.log(' 📍 Step: 查找文稿...');
console.info(' 📍 Step: 查找文稿...');
const renamedItem = this.page.getByText(title, { exact: true }).first();
await expect(renamedItem).toBeVisible({ timeout: WAIT_TIMEOUT });
this.testContext.targetItemTitle = title;
this.testContext.targetType = 'page';
console.log(` ✅ 找到文稿: ${title}`);
console.info(` ✅ 找到文稿: ${title}`);
});
// ============================================
@@ -311,7 +311,7 @@ Given('用户在 Page 页面有一个文稿 {string}', async function (this: Cus
// ============================================
When('用户点击新建文稿按钮', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击新建文稿按钮...');
console.info(' 📍 Step: 点击新建文稿按钮...');
// Look for the SquarePen icon button (new page button)
const newPageButton = this.page.locator('svg.lucide-square-pen').first();
@@ -331,11 +331,11 @@ When('用户点击新建文稿按钮', async function (this: CustomWorld) {
}
await this.page.waitForTimeout(1000);
console.log(' ✅ 已点击新建文稿按钮');
console.info(' ✅ 已点击新建文稿按钮');
});
When('用户右键点击该文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 右键点击文稿...');
console.info(' 📍 Step: 右键点击文稿...');
const title = this.testContext.targetItemTitle || this.testContext.createdPageTitle;
// Find the page item by its title text, then find the parent clickable block
@@ -349,13 +349,13 @@ When('用户右键点击该文稿', async function (this: CustomWorld) {
// Debug: check what menus are visible
const menuItems = await this.page.locator('[role="menuitem"]').count();
console.log(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.info(` 📍 Debug: Found ${menuItems} menu items after right-click`);
console.log(' ✅ 已右键点击文稿');
console.info(' ✅ 已右键点击文稿');
});
When('用户在菜单中选择复制', async function (this: CustomWorld) {
console.log(' 📍 Step: 选择复制选项...');
console.info(' 📍 Step: 选择复制选项...');
// Look for duplicate option (复制 or Duplicate)
const duplicateOption = this.page.getByRole('menuitem', { name: /复制|duplicate/i });
@@ -363,18 +363,18 @@ When('用户在菜单中选择复制', async function (this: CustomWorld) {
await duplicateOption.click();
await this.page.waitForTimeout(1000);
console.log(' ✅ 已选择复制选项');
console.info(' ✅ 已选择复制选项');
});
When('用户输入新的文稿名称 {string}', async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}"...`);
console.info(` 📍 Step: 输入新名称 "${newName}"...`);
await inputPageName.call(this, newName, false);
});
When(
'用户输入新的文稿名称 {string} 并按 Enter',
async function (this: CustomWorld, newName: string) {
console.log(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
console.info(` 📍 Step: 输入新名称 "${newName}" 并按 Enter...`);
await inputPageName.call(this, newName, true);
},
);
@@ -384,7 +384,7 @@ When(
// ============================================
Then('应该创建一个新的文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证新文稿已创建...');
console.info(' 📍 Step: 验证新文稿已创建...');
await this.page.waitForTimeout(1000);
@@ -392,11 +392,11 @@ Then('应该创建一个新的文稿', async function (this: CustomWorld) {
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/page\/.+/);
console.log(' ✅ 新文稿已创建');
console.info(' ✅ 新文稿已创建');
});
Then('文稿列表中应该显示新文稿', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文稿列表中显示新文稿...');
console.info(' 📍 Step: 验证文稿列表中显示新文稿...');
await this.page.waitForTimeout(500);
@@ -405,11 +405,11 @@ Then('文稿列表中应该显示新文稿', async function (this: CustomWorld)
const untitledText = this.page.getByText(/无标题|untitled/i).first();
await expect(untitledText).toBeVisible({ timeout: 5000 });
console.log(' ✅ 文稿列表中显示新文稿');
console.info(' ✅ 文稿列表中显示新文稿');
});
Then('该文稿名称应该更新为 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证名称为 "${expectedName}"...`);
console.info(` 📍 Step: 验证名称为 "${expectedName}"...`);
await this.page.waitForTimeout(1000);
@@ -417,11 +417,11 @@ Then('该文稿名称应该更新为 {string}', async function (this: CustomWorl
const renamedItem = this.page.getByText(expectedName, { exact: true }).first();
await expect(renamedItem).toBeVisible({ timeout: 5000 });
console.log(` ✅ 名称已更新为 "${expectedName}"`);
console.info(` ✅ 名称已更新为 "${expectedName}"`);
});
Then('文稿列表中应该出现 {string}', async function (this: CustomWorld, expectedName: string) {
console.log(` 📍 Step: 验证文稿列表中出现 "${expectedName}"...`);
console.info(` 📍 Step: 验证文稿列表中出现 "${expectedName}"...`);
await this.page.waitForTimeout(2000);
@@ -438,20 +438,20 @@ Then('文稿列表中应该出现 {string}', async function (this: CustomWorld,
if ((await duplicatedItem.count()) === 0) {
// Fallback: check if there are at least 2 pages with similar name
const similarPages = this.page.getByText(expectedName.replace(/\s*\(Copy\)$/, '')).all();
// eslint-disable-next-line unicorn/no-await-expression-member
const count = (await similarPages).length;
console.log(` 📍 Debug: Found ${count} pages with similar name`);
console.info(` 📍 Debug: Found ${count} pages with similar name`);
expect(count).toBeGreaterThanOrEqual(2);
console.log(` ✅ 文稿列表中出现多个相似名称的文稿`);
console.info(` ✅ 文稿列表中出现多个相似名称的文稿`);
return;
}
await expect(duplicatedItem).toBeVisible({ timeout: WAIT_TIMEOUT });
console.log(` ✅ 文稿列表中出现 "${expectedName}"`);
console.info(` ✅ 文稿列表中出现 "${expectedName}"`);
});
Then('该文稿应该从列表中移除', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文稿已移除...');
console.info(' 📍 Step: 验证文稿已移除...');
await this.page.waitForTimeout(1000);
@@ -461,5 +461,5 @@ Then('该文稿应该从列表中移除', async function (this: CustomWorld) {
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
}
console.log(' ✅ 文稿已从列表中移除');
console.info(' ✅ 文稿已从列表中移除');
});