199 lines
8.1 KiB
JavaScript
199 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|||
|
|
"use strict";
|
||
|
|
|
||
|
|
const assert = require("node:assert/strict");
|
||
|
|
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 ROOT_PATH = process.env.MNOTE_WOLAI_SEARCH_SMOKE_ROOT
|
||
|
|
|| "/mnt/Data1T/Mnote_data/users/liaibo/workspaces/my-space";
|
||
|
|
const WORKSPACE_ID = process.env.MNOTE_WOLAI_SEARCH_SMOKE_WORKSPACE_ID
|
||
|
|
|| "ws_c23b7696e2b64e4fb1a2b12761af4cfd";
|
||
|
|
const DOCUMENT_RELATIVE_PATH = process.env.MNOTE_WOLAI_SEARCH_SMOKE_DOCUMENT
|
||
|
|
|| "liaibo的个人空间/知识/实验资料-工具书/核磁常见杂质化学位移表(核磁溶剂峰)/核磁常见杂质化学位移表(核磁溶剂峰).md";
|
||
|
|
const QUERY = process.env.MNOTE_WOLAI_SEARCH_SMOKE_QUERY || "核磁溶剂峰";
|
||
|
|
const EXPECTED_TITLE = process.env.MNOTE_WOLAI_SEARCH_SMOKE_EXPECTED_TITLE
|
||
|
|
|| "核磁常见杂质化学位移表(核磁溶剂峰)";
|
||
|
|
const USERNAME = process.env.MNOTE_WOLAI_SEARCH_SMOKE_USER || "liaibo";
|
||
|
|
const PASSWORD = process.env.MNOTE_WOLAI_SEARCH_SMOKE_PASSWORD || "MnoteE2E123!";
|
||
|
|
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 45_000);
|
||
|
|
const OUT_DIR = path.join(process.cwd(), "tmp", "task801-local-search-default-smoke");
|
||
|
|
const RESULT_PATH = path.join(OUT_DIR, "result.json");
|
||
|
|
const SCREENSHOT_PATH = path.join(OUT_DIR, "local-search-default.png");
|
||
|
|
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||
|
|
|| ["/usr/bin/chromium-browser", "/usr/bin/chromium", "/usr/bin/google-chrome-stable", "/snap/bin/chromium"]
|
||
|
|
.find((candidate) => fs.existsSync(candidate));
|
||
|
|
|
||
|
|
function fileUrl(localPath) {
|
||
|
|
return `file://${localPath}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function encodeLocalIdSegment(value) {
|
||
|
|
const bytes = Buffer.from(String(value || ""), "utf8");
|
||
|
|
let encoded = "";
|
||
|
|
for (const byte of bytes) {
|
||
|
|
const character = String.fromCharCode(byte);
|
||
|
|
if (
|
||
|
|
(byte >= 48 && byte <= 57)
|
||
|
|
|| (byte >= 65 && byte <= 90)
|
||
|
|
|| (byte >= 97 && byte <= 122)
|
||
|
|
|| character === "."
|
||
|
|
|| character === "_"
|
||
|
|
|| character === "-"
|
||
|
|
) {
|
||
|
|
encoded += character;
|
||
|
|
} else {
|
||
|
|
encoded += `~${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return encoded;
|
||
|
|
}
|
||
|
|
|
||
|
|
function documentUrl() {
|
||
|
|
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(`local-md:${encodeLocalIdSegment(DOCUMENT_RELATIVE_PATH)}`)}`);
|
||
|
|
url.searchParams.set("sourceKind", "local_folder");
|
||
|
|
url.searchParams.set("rootUri", fileUrl(ROOT_PATH));
|
||
|
|
url.searchParams.set("workspaceId", WORKSPACE_ID);
|
||
|
|
url.searchParams.set("treeView", "filetree");
|
||
|
|
return url.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function signIn(request) {
|
||
|
|
const response = await request.post(`${BASE_URL}/api/auth`, {
|
||
|
|
headers: { "content-type": "application/json", accept: "application/json" },
|
||
|
|
data: {
|
||
|
|
action: "auth:signIn",
|
||
|
|
args: {
|
||
|
|
provider: "password",
|
||
|
|
params: {
|
||
|
|
account: USERNAME,
|
||
|
|
password: PASSWORD,
|
||
|
|
flow: "signIn",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
assert.equal(response.status(), 200, `登录失败: ${response.status()} ${await response.text()}`);
|
||
|
|
const whoami = await (await request.get(`${BASE_URL}/api/auth/whoami`, {
|
||
|
|
headers: { accept: "application/json" },
|
||
|
|
})).json();
|
||
|
|
assert.equal(whoami.userId, USERNAME);
|
||
|
|
return whoami;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
|
|
|
||
|
|
const settingsPath = path.join(ROOT_PATH, ".mnote", "index", "local-index-settings.json");
|
||
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
||
|
|
assert.deepEqual(settings.includePaths, ["."], "local-first 工作区默认应索引整个目录");
|
||
|
|
|
||
|
|
const browser = await chromium.launch({
|
||
|
|
headless: process.env.HEADFUL !== "1",
|
||
|
|
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
|
||
|
|
});
|
||
|
|
const context = await browser.newContext({
|
||
|
|
viewport: { width: 1440, height: 960 },
|
||
|
|
locale: "zh-CN",
|
||
|
|
});
|
||
|
|
const page = await context.newPage();
|
||
|
|
page.setDefaultTimeout(UI_TIMEOUT_MS);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const whoami = await signIn(context.request);
|
||
|
|
|
||
|
|
const apiSearch = await context.request.post(`${BASE_URL}/api/search/documents`, {
|
||
|
|
headers: { "content-type": "application/json", accept: "application/json" },
|
||
|
|
data: {
|
||
|
|
workspaceId: WORKSPACE_ID,
|
||
|
|
sourceKind: "local_folder",
|
||
|
|
rootUri: fileUrl(ROOT_PATH),
|
||
|
|
documentId: `local-md:${encodeLocalIdSegment(DOCUMENT_RELATIVE_PATH)}`,
|
||
|
|
query: QUERY,
|
||
|
|
limit: 30,
|
||
|
|
filters: {
|
||
|
|
titleOnly: false,
|
||
|
|
exact: false,
|
||
|
|
includeOcr: false,
|
||
|
|
onlyCurrentPage: false,
|
||
|
|
timeRange: "any",
|
||
|
|
timeField: "updated",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
assert.equal(apiSearch.status(), 200, `本地搜索 API 失败: ${apiSearch.status()} ${await apiSearch.text()}`);
|
||
|
|
const apiPayload = await apiSearch.json();
|
||
|
|
assert.ok(
|
||
|
|
(apiPayload.results || []).some((item) => String(item.title || "").includes(EXPECTED_TITLE)),
|
||
|
|
`本地搜索 API 未命中目标页: ${JSON.stringify(apiPayload.results?.slice(0, 5), null, 2)}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.goto(documentUrl(), { waitUntil: "domcontentloaded" });
|
||
|
|
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({ state: "visible" });
|
||
|
|
await page.keyboard.press("Control+P");
|
||
|
|
const modal = page.locator('[data-testid="wolai-search-modal"]');
|
||
|
|
await modal.waitFor({ state: "visible" });
|
||
|
|
const input = modal.locator('[data-testid="wolai-search-input"]');
|
||
|
|
const knowledgeSwitch = modal.locator('[data-search-switch="knowledge"]');
|
||
|
|
assert.equal(await knowledgeSwitch.getAttribute("aria-checked"), "false", "搜索默认不应走空的全盘知识库");
|
||
|
|
|
||
|
|
await input.fill(QUERY);
|
||
|
|
await page.waitForFunction(
|
||
|
|
(expectedTitle) => {
|
||
|
|
const meta = document.querySelector('[data-testid="wolai-search-result-meta"]')?.textContent || "";
|
||
|
|
const rows = Array.from(document.querySelectorAll('[data-testid="wolai-search-result-row"]'));
|
||
|
|
return meta.includes("工作区搜索")
|
||
|
|
&& /共\s*[1-9]\d*\s*条匹配结果/.test(meta)
|
||
|
|
&& rows.some((row) => (row.textContent || "").includes(expectedTitle));
|
||
|
|
},
|
||
|
|
EXPECTED_TITLE,
|
||
|
|
{ timeout: UI_TIMEOUT_MS },
|
||
|
|
);
|
||
|
|
|
||
|
|
const state = await page.evaluate((expectedTitle) => {
|
||
|
|
const rows = Array.from(document.querySelectorAll('[data-testid="wolai-search-result-row"]'));
|
||
|
|
return {
|
||
|
|
knowledgeChecked: document.querySelector('[data-search-switch="knowledge"]')?.getAttribute("aria-checked") || "",
|
||
|
|
meta: document.querySelector('[data-testid="wolai-search-result-meta"]')?.textContent || "",
|
||
|
|
rowCount: rows.length,
|
||
|
|
targetFound: rows.some((row) => (row.textContent || "").includes(expectedTitle)),
|
||
|
|
firstRows: rows.slice(0, 5).map((row) => row.textContent || ""),
|
||
|
|
};
|
||
|
|
}, EXPECTED_TITLE);
|
||
|
|
assert.equal(state.knowledgeChecked, "false", `全盘知识库默认状态被改回开启: ${JSON.stringify(state)}`);
|
||
|
|
assert.ok(state.meta.includes("工作区搜索"), `搜索 UI 未走工作区搜索: ${JSON.stringify(state)}`);
|
||
|
|
assert.ok(state.rowCount > 0, `搜索 UI 未返回结果: ${JSON.stringify(state)}`);
|
||
|
|
assert.equal(state.targetFound, true, `搜索 UI 未展示目标页: ${JSON.stringify(state, null, 2)}`);
|
||
|
|
|
||
|
|
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: false });
|
||
|
|
const result = {
|
||
|
|
ok: true,
|
||
|
|
whoami,
|
||
|
|
rootPath: ROOT_PATH,
|
||
|
|
workspaceId: WORKSPACE_ID,
|
||
|
|
query: QUERY,
|
||
|
|
expectedTitle: EXPECTED_TITLE,
|
||
|
|
apiCount: apiPayload.results?.length || 0,
|
||
|
|
state,
|
||
|
|
screenshotPath: SCREENSHOT_PATH,
|
||
|
|
};
|
||
|
|
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||
|
|
console.log(JSON.stringify(result, null, 2));
|
||
|
|
} finally {
|
||
|
|
await browser.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((error) => {
|
||
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(OUT_DIR, "failure.json"),
|
||
|
|
`${JSON.stringify({ ok: false, error: error.stack || error.message || String(error) }, null, 2)}\n`,
|
||
|
|
"utf8",
|
||
|
|
);
|
||
|
|
console.error(error && error.stack ? error.stack : error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|