234 lines
11 KiB
JavaScript
234 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("node:assert");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { chromium } = require("playwright");
|
|
|
|
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
|
const QUERY = process.env.MNOTE_KNOWLEDGE_RAG_REPOSITION_QUERY || "三甲基硅";
|
|
const EXPECTED_RESOURCE = process.env.MNOTE_KNOWLEDGE_RAG_EXPECTED_RESOURCE
|
|
|| "有机合成中的保护基/[OCR]_有机合成中的保护基-酚羰基羧基巯基的保护_20250201_1908.layered_删减-2025-02-04 18-59-42.docx";
|
|
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task541-knowledge-rag-search-panel-office-reposition-smoke");
|
|
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
|
const SEARCH_SCREENSHOT_PATH = path.join(OUTPUT_DIR, "search-restored.png");
|
|
const OPEN_SCREENSHOT_PATH = path.join(OUTPUT_DIR, "office-reposition.png");
|
|
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
|
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
|
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
|
|
.find((candidate) => fs.existsSync(candidate));
|
|
|
|
async function openKnowledgeSearch(page, query) {
|
|
await page.click('[data-mnote-action="open-search-modal"]');
|
|
await page.waitForSelector('[data-testid="wolai-search-input"]', { timeout: UI_TIMEOUT_MS });
|
|
await page.evaluate(() => {
|
|
const button = document.querySelector('[data-search-switch="knowledge"]');
|
|
if (button instanceof HTMLElement) {
|
|
button.setAttribute("aria-checked", "true");
|
|
button.classList.add("is-on");
|
|
}
|
|
});
|
|
await page.fill('[data-testid="wolai-search-input"]', query);
|
|
await page.evaluate(() => {
|
|
document.querySelector('[data-testid="wolai-search-input"]')
|
|
?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
async function waitForOfficeRows(page) {
|
|
await page.waitForFunction(
|
|
({ query, expectedResource }) => {
|
|
const rows = Array.from(document.querySelectorAll('[data-testid="wolai-search-result-row"]'));
|
|
return rows.some((row) => {
|
|
const locator = JSON.parse(row.getAttribute("data-evidence-locator") || "null");
|
|
const path = String(locator?.resourcePath || locator?.resource_path || "");
|
|
return path === expectedResource && row.textContent.includes(query);
|
|
});
|
|
},
|
|
{ query: QUERY, expectedResource: EXPECTED_RESOURCE },
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
}
|
|
|
|
async function pickOfficeResult(page, excludeBlockId = "") {
|
|
return page.evaluate(({ expectedResource, excludeBlockId }) => {
|
|
const rows = Array.from(document.querySelectorAll('[data-testid="wolai-search-result-row"]'));
|
|
for (const row of rows) {
|
|
const locator = JSON.parse(row.getAttribute("data-evidence-locator") || "null");
|
|
const resourcePath = String(locator?.resourcePath || locator?.resource_path || "");
|
|
const blockId = String(locator?.blockId || locator?.block_id || "");
|
|
if (resourcePath === expectedResource && blockId && blockId !== excludeBlockId) {
|
|
return {
|
|
index: Number(row.getAttribute("data-search-result-index") || -1),
|
|
blockId,
|
|
text: row.textContent,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}, { expectedResource: EXPECTED_RESOURCE, excludeBlockId });
|
|
}
|
|
|
|
async function clickSearchResultByIndex(page, index) {
|
|
await page.locator(`[data-testid="wolai-search-result-row"][data-search-result-index="${index}"]`).click();
|
|
}
|
|
|
|
async function waitForOfficeLocator(page, expectedBlockId) {
|
|
await page.waitForFunction(
|
|
({ expectedResource, expectedBlockId }) => {
|
|
const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
|
|
return panel && panel.getAttribute("data-mnote-evidence-block-id") === expectedBlockId;
|
|
},
|
|
{ expectedResource: EXPECTED_RESOURCE, expectedBlockId },
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
await page.waitForTimeout(1_500);
|
|
}
|
|
|
|
async function readOfficeState(page) {
|
|
return page.evaluate((expectedResource) => {
|
|
const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
|
|
const frame = panel?.querySelector("iframe.mnote-resource-tab-frame");
|
|
const result = {
|
|
searchOpen: !document.querySelector('[data-testid="wolai-search-modal"]')?.hidden,
|
|
panelBlockId: panel?.getAttribute("data-mnote-evidence-block-id") || "",
|
|
frameSrc: frame?.getAttribute("src") || "",
|
|
loadCount: window.__mnoteTask541LoadCount || 0,
|
|
};
|
|
if (frame?.contentDocument) {
|
|
const doc = frame.contentDocument;
|
|
const target = doc.querySelector('[data-mnote-office-evidence-target="true"]');
|
|
const marker = doc.querySelector('[data-mnote-office-evidence-marker="true"]');
|
|
result.iframe = {
|
|
status: doc.documentElement.getAttribute("data-mnote-office-preview-status") || "",
|
|
applied: doc.documentElement.getAttribute("data-mnote-office-evidence-applied") || "",
|
|
highlightedText: target?.textContent?.slice(0, 200) || "",
|
|
markerMode: marker?.getAttribute("data-mnote-office-evidence-marker-mode") || "",
|
|
markerTargetText: marker?.getAttribute("data-mnote-office-evidence-target-text") || "",
|
|
markerHeight: marker ? Math.round(marker.getBoundingClientRect().height) : 0,
|
|
};
|
|
}
|
|
return result;
|
|
}, EXPECTED_RESOURCE);
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
const browser = await chromium.launch({ headless: true, executablePath: CHROMIUM_EXECUTABLE_PATH });
|
|
const context = await browser.newContext({ viewport: { width: 1360, height: 900 } });
|
|
try {
|
|
const auth = await context.request.post(`${BASE_URL}/api/auth`, {
|
|
data: {
|
|
action: "auth:signIn",
|
|
args: {
|
|
provider: "password",
|
|
params: {
|
|
email: "mnote.e2e@example.com",
|
|
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
|
|
flow: "signIn",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert(auth.ok(), `登录失败: ${auth.status()} ${await auth.text()}`);
|
|
|
|
const page = await context.newPage();
|
|
const pageErrors = [];
|
|
const consoleErrors = [];
|
|
page.on("pageerror", (error) => pageErrors.push(String(error?.stack || error?.message || error)));
|
|
page.on("console", (message) => {
|
|
if (["error", "warning"].includes(message.type())) consoleErrors.push(`${message.type()}: ${message.text()}`);
|
|
});
|
|
|
|
await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
await page.evaluate(() => {
|
|
window.localStorage?.setItem("mnote.search.collapseSourcesDefault.v1", "0");
|
|
});
|
|
await page.waitForSelector('[data-mnote-action="open-search-modal"]', { timeout: UI_TIMEOUT_MS });
|
|
await openKnowledgeSearch(page, QUERY);
|
|
await waitForOfficeRows(page);
|
|
const beforeClose = await page.evaluate(() => ({
|
|
query: document.querySelector('[data-testid="wolai-search-input"]')?.value || "",
|
|
count: document.querySelectorAll('[data-testid="wolai-search-result-row"]').length,
|
|
meta: document.querySelector('[data-testid="wolai-search-result-meta"]')?.textContent || "",
|
|
}));
|
|
await page.click('[data-testid="wolai-search-close"]');
|
|
await page.click('[data-mnote-action="open-search-modal"]');
|
|
await page.waitForSelector('[data-testid="wolai-search-input"]', { timeout: UI_TIMEOUT_MS });
|
|
const afterReopen = await page.evaluate(() => ({
|
|
query: document.querySelector('[data-testid="wolai-search-input"]')?.value || "",
|
|
count: document.querySelectorAll('[data-testid="wolai-search-result-row"]').length,
|
|
meta: document.querySelector('[data-testid="wolai-search-result-meta"]')?.textContent || "",
|
|
}));
|
|
await page.screenshot({ path: SEARCH_SCREENSHOT_PATH, fullPage: true });
|
|
assert.equal(afterReopen.query, QUERY, `搜索面板未恢复 query: ${JSON.stringify({ beforeClose, afterReopen })}`);
|
|
assert(afterReopen.count >= beforeClose.count, `搜索面板重新打开后结果丢失: ${JSON.stringify({ beforeClose, afterReopen })}`);
|
|
|
|
const first = await pickOfficeResult(page);
|
|
assert(first, `没有找到可打开的 Office 搜索结果`);
|
|
await clickSearchResultByIndex(page, first.index);
|
|
await waitForOfficeLocator(page, first.blockId);
|
|
const firstState = await readOfficeState(page);
|
|
assert.equal(firstState.searchOpen, false, `点击搜索结果后搜索面板应关闭但保留状态: ${JSON.stringify(firstState, null, 2)}`);
|
|
await page.evaluate((expectedResource) => {
|
|
const panel = document.querySelector(`[data-mnote-resource-tab-panel][data-resource-path="${CSS.escape(expectedResource)}"]`);
|
|
const frame = panel?.querySelector("iframe.mnote-resource-tab-frame");
|
|
window.__mnoteTask541LoadCount = 0;
|
|
if (frame) frame.addEventListener("load", () => {
|
|
window.__mnoteTask541LoadCount = (window.__mnoteTask541LoadCount || 0) + 1;
|
|
});
|
|
}, EXPECTED_RESOURCE);
|
|
|
|
await page.click('[data-mnote-action="open-search-modal"]');
|
|
await waitForOfficeRows(page);
|
|
const second = await pickOfficeResult(page, first.blockId);
|
|
assert(second, `没有找到同一文件的第二个不同 block 搜索结果: ${JSON.stringify(first)}`);
|
|
await clickSearchResultByIndex(page, second.index);
|
|
await waitForOfficeLocator(page, second.blockId);
|
|
const secondState = await readOfficeState(page);
|
|
await page.screenshot({ path: OPEN_SCREENSHOT_PATH, fullPage: true });
|
|
|
|
assert.equal(secondState.loadCount, 0, `同一 Office 文件重定位触发了 iframe reload: ${JSON.stringify({ firstState, secondState }, null, 2)}`);
|
|
assert.equal(secondState.iframe?.applied, "true", `Office preview 未应用第二次定位: ${JSON.stringify(secondState, null, 2)}`);
|
|
const highlighted = `${secondState.iframe?.highlightedText || ""} ${secondState.iframe?.markerTargetText || ""}`;
|
|
assert(highlighted.includes(QUERY), `Office preview 高亮未包含搜索词: ${JSON.stringify(secondState, null, 2)}`);
|
|
assert((secondState.iframe?.markerTargetText || "").length <= 160, `Office preview range marker 文本过长: ${JSON.stringify(secondState, null, 2)}`);
|
|
assert((secondState.iframe?.markerHeight || 0) <= 140, `Office preview range marker 框选过高: ${JSON.stringify(secondState, null, 2)}`);
|
|
|
|
const result = {
|
|
ok: true,
|
|
baseUrl: BASE_URL,
|
|
query: QUERY,
|
|
expectedResource: EXPECTED_RESOURCE,
|
|
beforeClose,
|
|
afterReopen,
|
|
first,
|
|
second,
|
|
firstState,
|
|
secondState,
|
|
pageErrors,
|
|
consoleErrors,
|
|
screenshots: {
|
|
search: SEARCH_SCREENSHOT_PATH,
|
|
open: OPEN_SCREENSHOT_PATH,
|
|
},
|
|
};
|
|
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} finally {
|
|
await browser.close().catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(OUTPUT_DIR, "failure.json"),
|
|
`${JSON.stringify({ ok: false, error: error.stack || error.message || String(error) }, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
console.error(error.stack || error.message || String(error));
|
|
process.exit(1);
|
|
});
|