feat: integrate pi rust lab runtime
This commit is contained in:
@@ -0,0 +1,699 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const TIMEOUT_MS = 30_000;
|
||||
const SCREENSHOT_DIR = path.join(
|
||||
process.env.MNOTE_QA_SCREENSHOT_DIR || path.resolve(__dirname, "../tmp/qa-block-handle"),
|
||||
);
|
||||
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const TEST_EMAIL = "mnote.e2e@example.com";
|
||||
const TEST_PASSWORD = "MnoteE2E123!";
|
||||
|
||||
const WORKSPACE_DIR = path.join(
|
||||
process.env.MNOTE_QA_WORKSPACE_DIR || path.resolve(__dirname, "../tmp/mnote-qa-handle"),
|
||||
);
|
||||
|
||||
const TEST_MARKDOWN = `\
|
||||
# 块手柄测试页
|
||||
|
||||
第一段正文,用于测试段落块的手柄和菜单。
|
||||
|
||||
第二段正文,用于测试第二个段落块。
|
||||
|
||||
## 二级标题块
|
||||
|
||||
标题下方正文段落。
|
||||
|
||||
- 列表项 A
|
||||
- 列表项 B
|
||||
|
||||
\`\`\`
|
||||
代码块示例
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
function uniqueSuffix() {
|
||||
return Math.random().toString(36).slice(2, 8);
|
||||
}
|
||||
|
||||
function ensureDirSync(dir) {
|
||||
const fs = require("fs");
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function writeWsManifest() {
|
||||
const fs = require("fs");
|
||||
ensureDirSync(path.join(WORKSPACE_DIR, ".mnote"));
|
||||
fs.writeFileSync(
|
||||
path.join(WORKSPACE_DIR, ".mnote/workspace.json"),
|
||||
JSON.stringify({
|
||||
name: "QA Block Handle Test",
|
||||
version: 1,
|
||||
createdAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(path.join(WORKSPACE_DIR, "BlockHandleTest.md"), TEST_MARKDOWN);
|
||||
}
|
||||
|
||||
async function ensureAuthenticated(page, requestContext) {
|
||||
const response = await requestContext.fetch(`${BASE_URL}/api/auth/whoami`, {
|
||||
method: "GET",
|
||||
timeout: 10_000,
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
if (body?.user) return body.user;
|
||||
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: TIMEOUT_MS });
|
||||
await page.waitForTimeout(1000);
|
||||
const btn = page.getByRole("button", { name: "测试账号快速登录" });
|
||||
if (await btn.isVisible().catch(() => false)) {
|
||||
await btn.click();
|
||||
await page.waitForURL("**/documents/**", { timeout: TIMEOUT_MS }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
} else {
|
||||
await page.getByLabel("邮箱").fill(TEST_EMAIL);
|
||||
await page.getByLabel("密码").fill(TEST_PASSWORD);
|
||||
await page.getByRole("button", { name: /登录|Login|Sign in/i }).click();
|
||||
await page.waitForURL("**/documents/**", { timeout: TIMEOUT_MS }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fs = require("fs");
|
||||
ensureDirSync(SCREENSHOT_DIR);
|
||||
writeWsManifest();
|
||||
|
||||
const suffix = uniqueSuffix();
|
||||
const rootUri = `file://${WORKSPACE_DIR}`;
|
||||
const docUrl =
|
||||
`${BASE_URL}/documents/local-md%3ABlockHandleTest.md` +
|
||||
`?sourceKind=local_folder&rootUri=${encodeURIComponent(rootUri)}&treeView=filetree`;
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(TIMEOUT_MS);
|
||||
|
||||
const consoleErrors = [];
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") consoleErrors.push(msg.text());
|
||||
});
|
||||
|
||||
const result = {
|
||||
pass: false,
|
||||
steps: [],
|
||||
screenshots: [],
|
||||
consoleErrors,
|
||||
failures: [],
|
||||
};
|
||||
|
||||
const step = (name, extra = {}) => {
|
||||
const s = { step: name, status: "ok", ...extra };
|
||||
result.steps.push(s);
|
||||
return s;
|
||||
};
|
||||
|
||||
const fail = (name, message, extra = {}) => {
|
||||
const s = { step: name, status: "fail", message, ...extra };
|
||||
result.steps.push(s);
|
||||
result.failures.push({ step: name, status: "fail", ...extra });
|
||||
return s;
|
||||
};
|
||||
|
||||
try {
|
||||
await ensureAuthenticated(page, page.request);
|
||||
step("auth");
|
||||
|
||||
await page.goto(docUrl, { waitUntil: "domcontentloaded", timeout: TIMEOUT_MS });
|
||||
await page
|
||||
.locator('.tiptap.ProseMirror, [contenteditable="true"]')
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: TIMEOUT_MS })
|
||||
.catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
step("navigate", { url: docUrl });
|
||||
|
||||
// ─────────────── Helper functions ───────────────
|
||||
|
||||
async function screenshot(name) {
|
||||
const filePath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: filePath, fullPage: false });
|
||||
result.screenshots.push(filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function getEditor() {
|
||||
const editor = page.locator('.tiptap.ProseMirror, [contenteditable="true"]').first();
|
||||
await editor.waitFor({ state: "visible", timeout: 5000 });
|
||||
return editor;
|
||||
}
|
||||
|
||||
async function getAllBlocks() {
|
||||
const editor = await getEditor();
|
||||
const children = editor.locator("> *").filter({
|
||||
has: page.locator(
|
||||
".ProseMirror-gapcursor, .ProseMirror-selection, .ProseMirror-cursor",
|
||||
),
|
||||
});
|
||||
// Fallback: just get top-level children
|
||||
const blocks = editor.locator("> *");
|
||||
const count = await blocks.count();
|
||||
return { editor, blocks, count };
|
||||
}
|
||||
|
||||
async function closeAnyOpenMenu() {
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
async function getBlockHandle() {
|
||||
// The block handle (grip icon) appears on hover near the left edge of a block
|
||||
// It's typically a div with a drag handle icon
|
||||
const handle = page.locator('[class*="block-handle"], [class*="drag-handle"], [data-block-handle]').first();
|
||||
if (await handle.isVisible().catch(() => false)) return handle;
|
||||
|
||||
// Try finding by the grip icon pattern
|
||||
const grip = page.locator('.editor-block-handle, .ProseMirror .block-handle, .ProseMirror [contenteditable] > div > div:first-child').first();
|
||||
return grip;
|
||||
}
|
||||
|
||||
async function findHandleNearBlock(blockEl, blockIndex) {
|
||||
// Hover over the block to trigger handle visibility
|
||||
const box = await blockEl.boundingBox();
|
||||
if (!box) return null;
|
||||
|
||||
// Hover at the left edge of the block to trigger the handle
|
||||
await page.mouse.move(box.x - 10, box.y + box.height / 2);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Look for visible handles
|
||||
const handles = page.locator('[class*="block-handle"], [class*="drag-handle"], .drag-handle, .block-handle-wrapper, [data-testid*="handle"]');
|
||||
const count = await handles.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const h = handles.nth(i);
|
||||
if (await h.isVisible().catch(() => false)) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function openMenuViaHover(blockEl) {
|
||||
const box = await blockEl.boundingBox();
|
||||
if (!box) throw new Error("Block has no bounding box");
|
||||
|
||||
// Move to left edge to trigger handle
|
||||
await page.mouse.move(box.x - 5, box.y + box.height / 2);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Find the visible handle
|
||||
const handles = page.locator('[class*="block-handle"], [class*="drag-handle"], .drag-handle, .block-handle-wrapper, [data-testid*="handle"]');
|
||||
const count = await handles.count();
|
||||
let handle = null;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const h = handles.nth(i);
|
||||
if (await h.isVisible().catch(() => false)) {
|
||||
handle = h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handle) {
|
||||
// Try clicking at the left edge area
|
||||
await page.mouse.click(box.x - 2, box.y + box.height / 2);
|
||||
await waitForMenuVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
// Click the handle to open the menu
|
||||
const handleBox = await handle.boundingBox();
|
||||
if (handleBox) {
|
||||
await page.mouse.click(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2);
|
||||
} else {
|
||||
await handle.click();
|
||||
}
|
||||
|
||||
await waitForMenuVisible();
|
||||
}
|
||||
|
||||
async function waitForMenuVisible() {
|
||||
// Wait for menu container to appear
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Try various menu selectors
|
||||
const menuSelectors = [
|
||||
'[class*="block-menu"]',
|
||||
'[class*="block-action"]',
|
||||
'[class*="slash-menu"]',
|
||||
'[class*="floating-menu"]',
|
||||
'[role="menu"]',
|
||||
'[data-testid*="menu"]',
|
||||
'.ProseMirror [contenteditable="false"] [class*="menu"]',
|
||||
];
|
||||
|
||||
for (const sel of menuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─────────────── TEST 1: Block Handle Visibility ───────────────
|
||||
|
||||
{
|
||||
const { editor, blocks, count } = await getAllBlocks();
|
||||
step("editor-visible", { blockCount: count });
|
||||
|
||||
if (count === 0) {
|
||||
throw new Error("No blocks found in editor");
|
||||
}
|
||||
|
||||
// Test first paragraph block
|
||||
const firstBlock = blocks.nth(0);
|
||||
const firstBox = await firstBlock.boundingBox();
|
||||
|
||||
if (!firstBox) {
|
||||
throw new Error("First block has no bounding box");
|
||||
}
|
||||
|
||||
// Hover over left edge of first block
|
||||
await page.mouse.move(firstBox.x - 5, firstBox.y + firstBox.height / 2);
|
||||
await page.waitForTimeout(800);
|
||||
await screenshot("01-handle-visible");
|
||||
|
||||
// Check if any handle appeared
|
||||
const handleSelectors = [
|
||||
'[class*="block-handle"]',
|
||||
'[class*="drag-handle"]',
|
||||
'.drag-handle',
|
||||
'[data-testid*="handle"]',
|
||||
'svg[data-block-handle]',
|
||||
'[contenteditable="true"] ~ div',
|
||||
];
|
||||
|
||||
let handleFound = false;
|
||||
for (const sel of handleSelectors) {
|
||||
const els = page.locator(sel);
|
||||
const cnt = await els.count();
|
||||
for (let i = 0; i < cnt; i++) {
|
||||
if (await els.nth(i).isVisible().catch(() => false)) {
|
||||
handleFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (handleFound) break;
|
||||
}
|
||||
|
||||
if (handleFound) {
|
||||
step("handle-visible", { selector: "matched" });
|
||||
} else {
|
||||
fail("handle-visible", "Block handle did not appear on hover");
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── TEST 2: Open Menu ───────────────
|
||||
|
||||
{
|
||||
const { editor, blocks, count } = await getAllBlocks();
|
||||
const firstBlock = blocks.nth(0);
|
||||
const firstBox = await firstBlock.boundingBox();
|
||||
|
||||
if (firstBox) {
|
||||
// Hover to show handle
|
||||
await page.mouse.move(firstBox.x - 5, firstBox.y + firstBox.height / 2);
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
// Find handle and click it
|
||||
const handles = page.locator('[class*="block-handle"], [class*="drag-handle"], .drag-handle, [data-testid*="handle"]');
|
||||
const hCount = await handles.count();
|
||||
let clicked = false;
|
||||
for (let i = 0; i < hCount; i++) {
|
||||
const h = handles.nth(i);
|
||||
if (await h.isVisible().catch(() => false)) {
|
||||
const hBox = await h.boundingBox();
|
||||
if (hBox) {
|
||||
await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2);
|
||||
clicked = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
// Fallback: click near the left edge
|
||||
await page.mouse.click(firstBox.x - 2, firstBox.y + firstBox.height / 2);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(800);
|
||||
await screenshot("02-menu-opened");
|
||||
|
||||
// Find the menu
|
||||
const menuSelectors = [
|
||||
'[class*="block-menu"]',
|
||||
'[class*="block-action"]',
|
||||
'[class*="slash-menu"]',
|
||||
'[class*="floating-menu"]',
|
||||
'[role="menu"]',
|
||||
];
|
||||
|
||||
let menuEl = null;
|
||||
for (const sel of menuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
menuEl = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuEl) {
|
||||
const menuBox = await menuEl.boundingBox();
|
||||
step("menu-opened", {
|
||||
width: Math.round(menuBox?.width || 0),
|
||||
height: Math.round(menuBox?.height || 0),
|
||||
});
|
||||
|
||||
// Measure menu items
|
||||
const menuItems = menuEl.locator('[class*="menu-item"], [role="menuitem"], [class*="action-item"]');
|
||||
const itemCount = await menuItems.count();
|
||||
step("menu-item-count", { count: itemCount });
|
||||
|
||||
// Check menu items text
|
||||
const itemTexts = [];
|
||||
for (let i = 0; i < Math.min(itemCount, 20); i++) {
|
||||
const text = await menuItems.nth(i).innerText().catch(() => "");
|
||||
itemTexts.push(text.trim());
|
||||
}
|
||||
step("menu-items-text", { items: itemTexts });
|
||||
|
||||
await screenshot("03-menu-detail");
|
||||
} else {
|
||||
fail("menu-opened", "Menu did not appear after clicking handle");
|
||||
}
|
||||
|
||||
// Close menu
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── TEST 3: Heading Block Handle ───────────────
|
||||
|
||||
{
|
||||
const { editor, blocks, count } = await getAllBlocks();
|
||||
|
||||
// Find the heading block (h2)
|
||||
let headingBlock = null;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const block = blocks.nth(i);
|
||||
const tagName = await block.evaluate((el) => el.tagName.toLowerCase());
|
||||
if (tagName === "h2" || tagName === "h1" || tagName === "h3") {
|
||||
headingBlock = block;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!headingBlock) {
|
||||
fail("heading-block", "Could not find heading block");
|
||||
} else {
|
||||
const hBox = await headingBlock.boundingBox();
|
||||
if (hBox) {
|
||||
// Hover over heading block
|
||||
await page.mouse.move(hBox.x - 5, hBox.y + hBox.height / 2);
|
||||
await page.waitForTimeout(600);
|
||||
await screenshot("04-heading-hover");
|
||||
|
||||
// Find and click handle
|
||||
const handles = page.locator('[class*="block-handle"], [class*="drag-handle"], .drag-handle, [data-testid*="handle"]');
|
||||
const hCount = await handles.count();
|
||||
let clicked = false;
|
||||
for (let i = 0; i < hCount; i++) {
|
||||
const h = handles.nth(i);
|
||||
if (await h.isVisible().catch(() => false)) {
|
||||
const hBox2 = await h.boundingBox();
|
||||
if (hBox2) {
|
||||
await page.mouse.click(hBox2.x + hBox2.width / 2, hBox2.y + hBox2.height / 2);
|
||||
clicked = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
await page.mouse.click(hBox.x - 2, hBox.y + hBox.height / 2);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(800);
|
||||
await screenshot("05-heading-menu");
|
||||
|
||||
// Find the menu
|
||||
const menuSelectors = [
|
||||
'[class*="block-menu"]',
|
||||
'[class*="block-action"]',
|
||||
'[class*="slash-menu"]',
|
||||
'[class*="floating-menu"]',
|
||||
'[role="menu"]',
|
||||
];
|
||||
|
||||
let menuEl = null;
|
||||
for (const sel of menuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
menuEl = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuEl) {
|
||||
const menuBox = await menuEl.boundingBox();
|
||||
step("heading-menu-opened", {
|
||||
width: Math.round(menuBox?.width || 0),
|
||||
height: Math.round(menuBox?.height || 0),
|
||||
});
|
||||
|
||||
// Check menu items for heading-specific items
|
||||
const menuItems = menuEl.locator('[class*="menu-item"], [role="menuitem"], [class*="action-item"]');
|
||||
const itemCount = await menuItems.count();
|
||||
step("heading-menu-item-count", { count: itemCount });
|
||||
|
||||
// Get menu items text
|
||||
const itemTexts = [];
|
||||
for (let i = 0; i < Math.min(itemCount, 20); i++) {
|
||||
const text = await menuItems.nth(i).innerText().catch(() => "");
|
||||
itemTexts.push(text.trim());
|
||||
}
|
||||
step("heading-menu-items-text", { items: itemTexts });
|
||||
|
||||
await screenshot("06-heading-menu-detail");
|
||||
} else {
|
||||
fail("heading-menu-opened", "Menu did not appear for heading block");
|
||||
}
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── TEST 4: List Block Handle ───────────────
|
||||
|
||||
{
|
||||
const { editor, blocks, count } = await getAllBlocks();
|
||||
|
||||
// Find list item block
|
||||
let listBlock = null;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const block = blocks.nth(i);
|
||||
const tagName = await block.evaluate((el) => el.tagName.toLowerCase());
|
||||
if (tagName === "li" || tagName === "ul" || tagName === "ol") {
|
||||
listBlock = block;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!listBlock) {
|
||||
fail("list-block", "Could not find list block");
|
||||
} else {
|
||||
const lBox = await listBlock.boundingBox();
|
||||
if (lBox) {
|
||||
await page.mouse.move(lBox.x - 5, lBox.y + lBox.height / 2);
|
||||
await page.waitForTimeout(600);
|
||||
await screenshot("06-list-hover");
|
||||
|
||||
const handles = page.locator('[class*="block-handle"], [class*="drag-handle"], .drag-handle, [data-testid*="handle"]');
|
||||
const hCount = await handles.count();
|
||||
let clicked = false;
|
||||
for (let i = 0; i < hCount; i++) {
|
||||
const h = handles.nth(i);
|
||||
if (await h.isVisible().catch(() => false)) {
|
||||
const hBox = await h.boundingBox();
|
||||
if (hBox) {
|
||||
await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2);
|
||||
clicked = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
await page.mouse.click(lBox.x - 2, lBox.y + lBox.height / 2);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(800);
|
||||
await screenshot("07-list-menu");
|
||||
|
||||
const menuSelectors = [
|
||||
'[class*="block-menu"]',
|
||||
'[class*="block-action"]',
|
||||
'[class*="slash-menu"]',
|
||||
'[class*="floating-menu"]',
|
||||
'[role="menu"]',
|
||||
];
|
||||
|
||||
let menuEl = null;
|
||||
for (const sel of menuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
menuEl = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuEl) {
|
||||
const menuBox = await menuEl.boundingBox();
|
||||
step("list-menu-opened", {
|
||||
width: Math.round(menuBox?.width || 0),
|
||||
height: Math.round(menuBox?.height || 0),
|
||||
});
|
||||
|
||||
const menuItems = menuEl.locator('[class*="menu-item"], [role="menuitem"], [class*="action-item"]');
|
||||
const itemCount = await menuItems.count();
|
||||
step("list-menu-item-count", { count: itemCount });
|
||||
|
||||
await screenshot("08-list-menu-detail");
|
||||
} else {
|
||||
fail("list-menu-opened", "Menu did not appear for list block");
|
||||
}
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────── TEST 5: Code Block Handle ───────────────
|
||||
|
||||
{
|
||||
const { editor, blocks, count } = await getAllBlocks();
|
||||
|
||||
let codeBlock = null;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const block = blocks.nth(i);
|
||||
const tagName = await block.evaluate((el) => el.tagName.toLowerCase());
|
||||
if (tagName === "pre" || tagName === "code") {
|
||||
codeBlock = block;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeBlock) {
|
||||
fail("code-block", "Could not find code block");
|
||||
} else {
|
||||
const cBox = await codeBlock.boundingBox();
|
||||
if (cBox) {
|
||||
await page.mouse.move(cBox.x - 5, cBox.y + cBox.height / 2);
|
||||
await page.waitForTimeout(600);
|
||||
await screenshot("09-code-hover");
|
||||
|
||||
const handles = page.locator('[class*="block-handle"], [class*="drag-handle"], .drag-handle, [data-testid*="handle"]');
|
||||
const hCount = await handles.count();
|
||||
let clicked = false;
|
||||
for (let i = 0; i < hCount; i++) {
|
||||
const h = handles.nth(i);
|
||||
if (await h.isVisible().catch(() => false)) {
|
||||
const hBox = await h.boundingBox();
|
||||
if (hBox) {
|
||||
await page.mouse.click(hBox.x + hBox.width / 2, hBox.y + hBox.height / 2);
|
||||
clicked = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
await page.mouse.click(cBox.x - 2, cBox.y + cBox.height / 2);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(800);
|
||||
await screenshot("10-code-menu");
|
||||
|
||||
const menuSelectors = [
|
||||
'[class*="block-menu"]',
|
||||
'[class*="block-action"]',
|
||||
'[class*="slash-menu"]',
|
||||
'[class*="floating-menu"]',
|
||||
'[role="menu"]',
|
||||
];
|
||||
|
||||
let menuEl = null;
|
||||
for (const sel of menuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
menuEl = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuEl) {
|
||||
const menuBox = await menuEl.boundingBox();
|
||||
step("code-menu-opened", {
|
||||
width: Math.round(menuBox?.width || 0),
|
||||
height: Math.round(menuBox?.height || 0),
|
||||
});
|
||||
|
||||
const menuItems = menuEl.locator('[class*="menu-item"], [role="menuitem"], [class*="action-item"]');
|
||||
const itemCount = await menuItems.count();
|
||||
step("code-menu-item-count", { count: itemCount });
|
||||
|
||||
await screenshot("11-code-menu-detail");
|
||||
} else {
|
||||
fail("code-menu-opened", "Menu did not appear for code block");
|
||||
}
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.pass = result.failures.length === 0;
|
||||
} catch (err) {
|
||||
result.steps.push({ step: "exception", status: "fail", message: String(err) });
|
||||
await screenshot("99-error").catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// Cleanup temp workspace
|
||||
try {
|
||||
const fs = require("fs");
|
||||
fs.rmSync(WORKSPACE_DIR, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
||||
process.exit(result.pass ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user