test: organize current smoke baseline
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const OUTPUT_DIR = path.resolve(__dirname, "..", "tmp", "task490-runtime-surfaces-smoke");
|
||||
const TASK = "task490-runtime-surfaces-smoke";
|
||||
const TARGET_BLOCK_ID = "task490-runtime-surface-target";
|
||||
|
||||
function screenshotPath(name) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
return path.join(OUTPUT_DIR, `${name}.png`);
|
||||
}
|
||||
|
||||
function fileUrl(localPath) {
|
||||
return `file://${localPath}`;
|
||||
}
|
||||
|
||||
function localMdDocumentId(relativePath) {
|
||||
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
||||
}
|
||||
|
||||
function documentUrl(baseUrl, root, relativePath) {
|
||||
const url = new URL(`${baseUrl}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", fileUrl(root));
|
||||
url.searchParams.set("treeView", "filetree");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function writeWorkspaceManifest(root, ownerId) {
|
||||
const metadataDir = path.join(root, ".mnote");
|
||||
fs.mkdirSync(metadataDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(metadataDir, "workspace.json"),
|
||||
`${JSON.stringify({
|
||||
workspaceId: `local-ws:${ownerId}:task490`,
|
||||
ownerId,
|
||||
createdAt: new Date().toISOString(),
|
||||
capabilities: ["local_files", "markdown_edit", "asset_upload"],
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function saveScreenshot(page, name) {
|
||||
const target = screenshotPath(name);
|
||||
await page.screenshot({ path: target, fullPage: false });
|
||||
return target;
|
||||
}
|
||||
|
||||
async function waitForRuntimeIsland(page, uiTimeoutMs) {
|
||||
const root = page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first();
|
||||
await root.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const editor = page
|
||||
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
||||
.first();
|
||||
await editor.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
||||
const editorNode = host?.querySelector('.editor-surface .ProseMirror[contenteditable="true"]');
|
||||
return host?.getAttribute("data-runtime-editor-status") !== "error"
|
||||
&& editorNode instanceof HTMLElement
|
||||
&& editorNode.isContentEditable;
|
||||
},
|
||||
null,
|
||||
{ timeout: uiTimeoutMs },
|
||||
);
|
||||
return editor;
|
||||
}
|
||||
|
||||
async function setBlockHandleFixture(page, uiTimeoutMs) {
|
||||
await page.evaluate((targetBlockId) => {
|
||||
const editor = document.querySelector(".editor-surface .ProseMirror")?.editor;
|
||||
if (!editor) {
|
||||
throw new Error("找不到 Tiptap editor");
|
||||
}
|
||||
editor.commands.setContent(
|
||||
{
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
attrs: { blockId: targetBlockId },
|
||||
content: [{ type: "text", text: "Task490 block handle target" }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
attrs: { blockId: "task490-second-block" },
|
||||
content: [{ type: "text", text: "Task490 slash target" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
);
|
||||
editor.commands.focus("end");
|
||||
}, TARGET_BLOCK_ID);
|
||||
|
||||
await page.waitForFunction(
|
||||
(targetBlockId) => document.querySelector(`[data-block-id="${targetBlockId}"]`) instanceof HTMLElement,
|
||||
TARGET_BLOCK_ID,
|
||||
{ timeout: uiTimeoutMs },
|
||||
);
|
||||
}
|
||||
|
||||
async function closePageAiIfOpen(page) {
|
||||
const closeButton = page.locator('[data-page-ai-action="close"]').first();
|
||||
if (await closeButton.isVisible().catch(() => false)) {
|
||||
await closeButton.click().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
delete require.cache[require.resolve("./tree-shell-smoke-helpers")];
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
assert,
|
||||
ensureAuthenticated,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
locale: "zh-CN",
|
||||
extraHTTPHeaders: {
|
||||
"x-mnote-actor-id": "mnote-e2e",
|
||||
"x-mnote-actor-type": "user",
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const screenshots = {};
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task490-"));
|
||||
const relativePath = "RuntimeSurfaces.md";
|
||||
writeWorkspaceManifest(root, "mnote-e2e");
|
||||
fs.writeFileSync(
|
||||
path.join(root, relativePath),
|
||||
["# Runtime Surfaces", "", "Task490 initial paragraph", ""].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
let caughtError = null;
|
||||
|
||||
try {
|
||||
await ensureAuthenticated(page, context.request);
|
||||
|
||||
const url = documentUrl(BASE_URL, root, relativePath);
|
||||
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
assert(response, "文档页没有返回响应");
|
||||
assert(response.status() === 200, `文档页状态码异常: ${response.status()}`);
|
||||
const editor = await waitForRuntimeIsland(page, UI_TIMEOUT_MS);
|
||||
await setBlockHandleFixture(page, UI_TIMEOUT_MS);
|
||||
|
||||
const moreButton = page.locator('[data-testid="wolai-page-settings-trigger"]').first();
|
||||
await moreButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await moreButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
const settingsPopover = page.locator('[data-testid="wolai-page-settings-popover"]').first();
|
||||
await settingsPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const settingsText = (await settingsPopover.textContent()) || "";
|
||||
assert(settingsText.includes("页面选项"), `页面设置 popover 未显示页面选项: ${settingsText}`);
|
||||
screenshots.pageSettings = await saveScreenshot(page, "01-page-settings");
|
||||
await page.keyboard.press("Escape").catch(() => undefined);
|
||||
|
||||
const aiButton = page.locator('[data-testid="wolai-floating-ai"]').first();
|
||||
await aiButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await aiButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
const aiDrawer = page.locator('[data-testid="wolai-page-ai-drawer"]').first();
|
||||
await aiDrawer.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const aiText = (await aiDrawer.textContent()) || "";
|
||||
assert(aiText.includes("Hermes"), `Page AI drawer 必须显示 Hermes 入口: ${aiText}`);
|
||||
assert(await page.locator("[data-page-ai-input]").first().isVisible(), "Page AI drawer 必须显示输入框");
|
||||
screenshots.pageAi = await saveScreenshot(page, "02-page-ai");
|
||||
await closePageAiIfOpen(page);
|
||||
|
||||
await editor.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.type("/");
|
||||
const slashMenu = page.locator('[data-testid="mnote-leptos-tiptap-slash-menu"]').first();
|
||||
await slashMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const slashText = (await slashMenu.textContent()) || "";
|
||||
assert(slashText.trim().length > 0, "slash menu 必须显示可选项");
|
||||
screenshots.slashMenu = await saveScreenshot(page, "03-slash-menu");
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
const target = page.locator(`[data-block-id="${TARGET_BLOCK_ID}"]`).first();
|
||||
await target.scrollIntoViewIfNeeded({ timeout: UI_TIMEOUT_MS });
|
||||
const targetBox = await target.boundingBox();
|
||||
assert(targetBox, "block handle 目标块缺少可测量区域");
|
||||
await page.mouse.move(targetBox.x + 8, targetBox.y + Math.min(12, Math.max(4, targetBox.height / 2)));
|
||||
const blockHandle = page.locator('[data-testid="block-drag-handle-trigger"]').first();
|
||||
await blockHandle.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await blockHandle.click({ timeout: UI_TIMEOUT_MS });
|
||||
const blockMenu = page.locator('[data-testid="block-drag-menu"]').first();
|
||||
await blockMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const blockMenuText = (await blockMenu.textContent()) || "";
|
||||
assert(blockMenuText.includes("删除") || blockMenuText.includes("Delete"), `block handle menu 缺少基础操作: ${blockMenuText}`);
|
||||
screenshots.blockHandleMenu = await saveScreenshot(page, "04-block-handle-menu");
|
||||
|
||||
const result = {
|
||||
ok: true,
|
||||
task: TASK,
|
||||
baseUrl: BASE_URL,
|
||||
root,
|
||||
relativePath,
|
||||
documentId: localMdDocumentId(relativePath),
|
||||
screenshotDir: OUTPUT_DIR,
|
||||
screenshots,
|
||||
};
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, "result.json"), `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
await saveScreenshot(page, "failure").catch(() => undefined);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
if (caughtError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user