mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 11:40:07 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 862f61f379 | |||
| 8127c1b9e7 | |||
| 8bff2097e0 |
@@ -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(' ✅ 不相关对话已被过滤');
|
||||
});
|
||||
|
||||
@@ -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)}..."`);
|
||||
});
|
||||
|
||||
@@ -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(' ✅ 消息内容完整显示');
|
||||
});
|
||||
|
||||
@@ -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)');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 已从列表中移除');
|
||||
});
|
||||
|
||||
@@ -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 已从列表中移除');
|
||||
});
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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(' ✅ 文本已斜体');
|
||||
});
|
||||
|
||||
@@ -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(' ✅ 验证完成');
|
||||
});
|
||||
|
||||
@@ -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(' ✅ 文稿已从列表中移除');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user