Files
lobehub/e2e/src/steps/page/editor-content.steps.ts
Arvin Xu df1710bbed test: add page e2e testing (#11423)
* add page e2e

* move

* add more e2e for page

* update

* fix keyboard

* update
2026-01-12 15:41:02 +08:00

345 lines
10 KiB
TypeScript

/**
* Page Editor Content Steps
*
* Step definitions for Page editor rich text editing E2E tests
*/
import { Then, When } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
// ============================================
// Helper Functions
// ============================================
/**
* Get the contenteditable editor element
*/
async function getEditor(world: CustomWorld) {
const editor = world.page.locator('[contenteditable="true"]').first();
await expect(editor).toBeVisible({ timeout: 5000 });
return editor;
}
// ============================================
// When Steps - Basic Text
// ============================================
When('用户点击编辑器内容区域', async function (this: CustomWorld) {
console.log(' 📍 Step: 点击编辑器内容区域...');
const editorContent = this.page.locator('[contenteditable="true"]').first();
if ((await editorContent.count()) > 0) {
await editorContent.click();
} else {
// Fallback: click somewhere else
await this.page.click('body', { position: { x: 400, y: 400 } });
}
await this.page.waitForTimeout(500);
console.log(' ✅ 已点击编辑器内容区域');
});
When('用户按下 Enter 键', async function (this: CustomWorld) {
console.log(' 📍 Step: 按下 Enter 键...');
await this.page.keyboard.press('Enter');
// Wait for debounce save (1000ms) + buffer
await this.page.waitForTimeout(1500);
console.log(' ✅ 已按下 Enter 键');
});
When('用户输入文本 {string}', async function (this: CustomWorld, text: string) {
console.log(` 📍 Step: 输入文本 "${text}"...`);
await this.page.keyboard.type(text, { delay: 30 });
await this.page.waitForTimeout(300);
// Store for later verification
this.testContext.inputText = text;
console.log(` ✅ 已输入文本 "${text}"`);
});
When('用户在编辑器中输入内容 {string}', async function (this: CustomWorld, content: string) {
console.log(` 📍 Step: 在编辑器中输入内容 "${content}"...`);
const editor = await getEditor(this);
await editor.click();
await this.page.waitForTimeout(300);
await this.page.keyboard.type(content, { delay: 30 });
await this.page.waitForTimeout(300);
this.testContext.inputText = content;
console.log(` ✅ 已输入内容 "${content}"`);
});
When('用户选中所有内容', async function (this: CustomWorld) {
console.log(' 📍 Step: 选中所有内容...');
await this.page.keyboard.press(`${this.modKey}+A`);
await this.page.waitForTimeout(300);
console.log(' ✅ 已选中所有内容');
});
// ============================================
// When Steps - Slash Commands
// ============================================
When('用户输入斜杠 {string}', async function (this: CustomWorld, slash: string) {
console.log(` 📍 Step: 输入斜杠 "${slash}"...`);
await this.page.keyboard.type(slash, { delay: 50 });
// Wait for slash menu to appear
await this.page.waitForTimeout(500);
console.log(` ✅ 已输入斜杠 "${slash}"`);
});
When('用户输入斜杠命令 {string}', async function (this: CustomWorld, command: string) {
console.log(` 📍 Step: 输入斜杠命令 "${command}"...`);
// The command format is "/shortcut" (e.g., "/h1", "/codeblock")
// First type the slash and wait for menu
await this.page.keyboard.type('/', { delay: 100 });
await this.page.waitForTimeout(800); // Wait for slash menu to appear
// Then type the rest of the command (without the leading /)
const shortcut = command.startsWith('/') ? command.slice(1) : command;
await this.page.keyboard.type(shortcut, { delay: 80 });
await this.page.waitForTimeout(500); // Wait for menu to filter
console.log(` ✅ 已输入斜杠命令 "${command}"`);
});
// ============================================
// When Steps - Formatting
// ============================================
When('用户按下快捷键 {string}', async function (this: CustomWorld, shortcut: string) {
console.log(` 📍 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}"`);
});
// ============================================
// Then Steps - Basic Text
// ============================================
Then('编辑器应该显示输入的文本', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器显示输入的文本...');
const editor = await getEditor(this);
const text = this.testContext.inputText;
// Check if the text is visible in the editor
const editorText = await editor.textContent();
expect(editorText).toContain(text);
console.log(` ✅ 编辑器显示文本: "${text}"`);
});
Then('编辑器应该显示 {string}', async function (this: CustomWorld, expectedText: string) {
console.log(` 📍 Step: 验证编辑器显示 "${expectedText}"...`);
const editor = await getEditor(this);
const editorText = await editor.textContent();
expect(editorText).toContain(expectedText);
console.log(` ✅ 编辑器显示 "${expectedText}"`);
});
// ============================================
// Then Steps - Slash Commands
// ============================================
Then('应该显示斜杠命令菜单', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证显示斜杠命令菜单...');
// The slash menu should be visible
// Look for menu with heading options, list options, etc.
const menuSelectors = ['[role="menu"]', '[role="listbox"]', '.slash-menu', '[data-slash-menu]'];
let menuFound = false;
for (const selector of menuSelectors) {
const menu = this.page.locator(selector);
if ((await menu.count()) > 0 && (await menu.isVisible())) {
menuFound = true;
break;
}
}
// Alternative: look for menu items by text
if (!menuFound) {
const headingOption = this.page.getByText(/heading|标题/i).first();
const listOption = this.page.getByText(/list|列表/i).first();
menuFound =
((await headingOption.count()) > 0 && (await headingOption.isVisible())) ||
((await listOption.count()) > 0 && (await listOption.isVisible()));
}
expect(menuFound).toBe(true);
console.log(' ✅ 斜杠命令菜单已显示');
});
Then('编辑器应该包含一级标题', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含一级标题...');
// Check for h1 element in the editor
const editor = await getEditor(this);
const h1 = editor.locator('h1');
await expect(h1).toBeVisible({ timeout: 5000 });
console.log(' ✅ 编辑器包含一级标题');
});
Then('编辑器应该包含无序列表', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含无序列表...');
const editor = await getEditor(this);
const ul = editor.locator('ul');
await expect(ul).toBeVisible({ timeout: 5000 });
console.log(' ✅ 编辑器包含无序列表');
});
Then('编辑器应该包含任务列表', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含任务列表...');
const editor = await getEditor(this);
// Task list usually has checkbox elements
const checkboxSelectors = [
'input[type="checkbox"]',
'[role="checkbox"]',
'[data-lexical-check-list]',
'li[role="listitem"] input',
];
let found = false;
for (const selector of checkboxSelectors) {
const checkbox = editor.locator(selector);
if ((await checkbox.count()) > 0) {
found = true;
break;
}
}
// Alternative: check for specific class or structure
if (!found) {
const listItem = editor.locator('li');
found = (await listItem.count()) > 0;
}
expect(found).toBe(true);
console.log(' ✅ 编辑器包含任务列表');
});
Then('编辑器应该包含代码块', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证编辑器包含代码块...');
// Code block might be rendered inside the editor OR as a sibling element
// CodeMirror renders its own container
// First check inside the editor
const editor = await getEditor(this);
const codeBlockSelectors = [
'pre',
'code',
'.cm-editor', // CodeMirror
'[data-language]',
'.code-block',
];
let found = false;
for (const selector of codeBlockSelectors) {
const codeBlock = editor.locator(selector);
if ((await codeBlock.count()) > 0) {
found = true;
break;
}
}
// If not found inside editor, check the whole page
// CodeMirror might render outside the contenteditable
if (!found) {
for (const selector of codeBlockSelectors) {
const codeBlock = this.page.locator(selector);
if ((await codeBlock.count()) > 0 && (await codeBlock.isVisible())) {
found = true;
break;
}
}
}
expect(found).toBe(true);
console.log(' ✅ 编辑器包含代码块');
});
// ============================================
// Then Steps - Formatting
// ============================================
Then('选中的文本应该被加粗', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文本已加粗...');
const editor = await getEditor(this);
// Check for bold element (strong or b tag, or font-weight style)
const boldSelectors = [
'strong',
'b',
'[style*="font-weight: bold"]',
'[style*="font-weight: 700"]',
];
let found = false;
for (const selector of boldSelectors) {
const boldElement = editor.locator(selector);
if ((await boldElement.count()) > 0) {
found = true;
break;
}
}
expect(found).toBe(true);
console.log(' ✅ 文本已加粗');
});
Then('选中的文本应该变为斜体', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证文本已斜体...');
const editor = await getEditor(this);
// Check for italic element (em or i tag, or font-style style)
const italicSelectors = ['em', 'i', '[style*="font-style: italic"]'];
let found = false;
for (const selector of italicSelectors) {
const italicElement = editor.locator(selector);
if ((await italicElement.count()) > 0) {
found = true;
break;
}
}
expect(found).toBe(true);
console.log(' ✅ 文本已斜体');
});