457 lines
15 KiB
JavaScript
457 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const vm = require("node:vm");
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, "..");
|
|
const RUNTIME_PATH = path.join(
|
|
REPO_ROOT,
|
|
"rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js",
|
|
);
|
|
const FILETREE_RUNTIME_PATH = path.join(
|
|
REPO_ROOT,
|
|
"rust/crates/mnote-web/browser/filetree-runtime.js",
|
|
);
|
|
const FILETREE_CONTEXT_MENU_RUNTIME_PATH = path.join(
|
|
REPO_ROOT,
|
|
"rust/crates/mnote-web/browser/filetree-context-menu-runtime.js",
|
|
);
|
|
|
|
function loadRuntimeFactory() {
|
|
const source = fs
|
|
.readFileSync(RUNTIME_PATH, "utf8")
|
|
.replace(
|
|
"export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {",
|
|
"const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {",
|
|
);
|
|
const context = {
|
|
console,
|
|
CustomEvent: class CustomEvent {
|
|
constructor(type, init) {
|
|
this.type = type;
|
|
this.detail = init && init.detail;
|
|
}
|
|
},
|
|
HTMLElement: class HTMLElement {},
|
|
navigator: {
|
|
clipboard: {
|
|
writeText: async (text) => {
|
|
context.__clipboardText = text;
|
|
},
|
|
},
|
|
},
|
|
document: {
|
|
body: {},
|
|
documentElement: {
|
|
attrs: {},
|
|
setAttribute(name, value) {
|
|
this.attrs[name] = String(value);
|
|
},
|
|
},
|
|
createElement() {
|
|
return {};
|
|
},
|
|
querySelector() {
|
|
return null;
|
|
},
|
|
querySelectorAll() {
|
|
return [];
|
|
},
|
|
},
|
|
window: {
|
|
alert() {},
|
|
confirm() {
|
|
return true;
|
|
},
|
|
dispatchEvent() {},
|
|
},
|
|
URL,
|
|
setTimeout,
|
|
clearTimeout,
|
|
};
|
|
context.globalThis = context;
|
|
vm.createContext(context);
|
|
vm.runInContext(fs.readFileSync(FILETREE_CONTEXT_MENU_RUNTIME_PATH, "utf8"), context, {
|
|
filename: FILETREE_CONTEXT_MENU_RUNTIME_PATH,
|
|
});
|
|
vm.runInContext(
|
|
`${source}\nglobalThis.__factory = createSidebarFileTreeCommandRuntime;`,
|
|
context,
|
|
{ filename: RUNTIME_PATH },
|
|
);
|
|
return context;
|
|
}
|
|
|
|
function loadFileTreeRuntime() {
|
|
const context = {
|
|
console,
|
|
HTMLElement: class HTMLElement {},
|
|
document: {},
|
|
window: {},
|
|
URL,
|
|
};
|
|
context.globalThis = context;
|
|
context.window = context;
|
|
vm.createContext(context);
|
|
vm.runInContext(fs.readFileSync(FILETREE_RUNTIME_PATH, "utf8"), context, {
|
|
filename: FILETREE_RUNTIME_PATH,
|
|
});
|
|
return context;
|
|
}
|
|
|
|
async function runCopyId(runtime, context, detail) {
|
|
context.__clipboardText = "";
|
|
runtime.handleTreeContextMenuAction("copy-id", detail, null);
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
return context.__clipboardText;
|
|
}
|
|
|
|
async function main() {
|
|
const fileTreeContext = loadFileTreeRuntime();
|
|
class FakeRow extends fileTreeContext.HTMLElement {
|
|
constructor(attrs, textContent = "") {
|
|
super();
|
|
this.attrs = attrs;
|
|
this.textContent = textContent;
|
|
}
|
|
getAttribute(name) {
|
|
return this.attrs[name] || "";
|
|
}
|
|
querySelector() {
|
|
return null;
|
|
}
|
|
}
|
|
const readWorkspacePath = (attrs, textContent = "") =>
|
|
fileTreeContext.window.__mnoteFileTreeRuntime.readWorkspacePathFromRow(
|
|
new FakeRow(attrs, textContent),
|
|
{
|
|
currentSourceKind: () => attrs.__sourceKind || "local_folder",
|
|
currentRootUri: () => attrs.__rootUri || "file:///mnt/Data1T/mnote",
|
|
resolveWorkspaceId: () => "local-folder",
|
|
rowTitle: (row) => row.textContent,
|
|
},
|
|
);
|
|
const workspacePath = readWorkspacePath({
|
|
"data-row-id": "local:asset:assets/report.pdf",
|
|
"data-row-kind": "asset",
|
|
"data-document-id": "local-md:README.md",
|
|
"data-asset-id": "local-file:assets/report.pdf",
|
|
"data-local-relative-path": "assets/report.pdf",
|
|
"data-object-identity": JSON.stringify({
|
|
objectKind: "attachment",
|
|
documentId: "local-md:README.md",
|
|
assetId: "local-file:assets/report.pdf",
|
|
}),
|
|
}, "report.pdf");
|
|
assert.equal(workspacePath.schema, "mnote.workspace_path.v1");
|
|
assert.equal(workspacePath.workspaceId, "local-folder");
|
|
assert.equal(workspacePath.sourceKind, "local_folder");
|
|
assert.equal(workspacePath.rootUri, "file:///mnt/Data1T/mnote");
|
|
assert.equal(workspacePath.relativePath, "assets/report.pdf");
|
|
assert.equal(workspacePath.documentId, "local-md:README.md");
|
|
assert.equal(workspacePath.assetId, "local-file:assets/report.pdf");
|
|
assert.equal(workspacePath.objectKind, "attachment");
|
|
assert.equal(workspacePath.resourceKind, "attachment");
|
|
assert.equal(workspacePath.objectIdentity.assetId, "local-file:assets/report.pdf");
|
|
|
|
const markdownWorkspacePath = readWorkspacePath({
|
|
"data-row-id": "local:markdown:docs/Page.md",
|
|
"data-row-kind": "markdown",
|
|
"data-document-id": "local-md:docs~2FPage.md",
|
|
"data-local-relative-path": "docs/Page.md",
|
|
"data-object-identity": JSON.stringify({
|
|
objectKind: "page",
|
|
documentId: "local-md:docs~2FPage.md",
|
|
assetId: null,
|
|
}),
|
|
}, "Page.md");
|
|
assert.equal(markdownWorkspacePath.relativePath, "docs/Page.md");
|
|
assert.equal(markdownWorkspacePath.objectKind, "page");
|
|
assert.equal(markdownWorkspacePath.isLocalFolder, true);
|
|
|
|
const folderWorkspacePath = readWorkspacePath({
|
|
"data-row-id": "local:folder:docs",
|
|
"data-row-kind": "folder",
|
|
"data-document-id": "local-dir:docs",
|
|
"data-local-relative-path": "docs",
|
|
"data-object-identity": JSON.stringify({
|
|
objectKind: "index",
|
|
documentId: "local-dir:docs",
|
|
assetId: null,
|
|
}),
|
|
}, "docs");
|
|
assert.equal(folderWorkspacePath.relativePath, "docs");
|
|
assert.equal(folderWorkspacePath.objectKind, "index");
|
|
|
|
const mindmapWorkspacePath = readWorkspacePath({
|
|
"data-row-id": "local:asset:Page/思维导图.json",
|
|
"data-row-kind": "asset",
|
|
"data-asset-id": "local-file:Page/思维导图.json",
|
|
"data-local-relative-path": "Page/思维导图.json",
|
|
"data-object-identity": JSON.stringify({
|
|
objectKind: "mindmap",
|
|
documentId: "local-md:Page~2FPage.md",
|
|
assetId: "local-file:Page/思维导图.json",
|
|
}),
|
|
}, "思维导图.json");
|
|
assert.equal(mindmapWorkspacePath.objectKind, "mindmap");
|
|
assert.equal(mindmapWorkspacePath.objectIdentity.assetId, "local-file:Page/思维导图.json");
|
|
|
|
const officeWorkspacePath = readWorkspacePath({
|
|
"data-row-id": "local:asset:Page/report.docx",
|
|
"data-row-kind": "asset",
|
|
"data-asset-id": "local-file:Page/report.docx",
|
|
"data-local-relative-path": "Page/report.docx",
|
|
"data-object-identity": JSON.stringify({
|
|
objectKind: "only_office",
|
|
documentId: "local-md:Page~2FPage.md",
|
|
assetId: "local-file:Page/report.docx",
|
|
}),
|
|
}, "report.docx");
|
|
assert.equal(officeWorkspacePath.objectKind, "only_office");
|
|
assert.equal(officeWorkspacePath.objectIdentity.assetId, "local-file:Page/report.docx");
|
|
|
|
const nonLocalWorkspacePath = readWorkspacePath({
|
|
__sourceKind: "cloud",
|
|
__rootUri: "",
|
|
"data-row-id": "doc:cloud-page",
|
|
"data-row-kind": "document",
|
|
"data-document-id": "local-cloud-compatible-id",
|
|
"data-object-identity": JSON.stringify({
|
|
objectKind: "page",
|
|
documentId: "local-cloud-compatible-id",
|
|
assetId: null,
|
|
}),
|
|
}, "cloud");
|
|
assert.equal(nonLocalWorkspacePath.isLocalFolder, false, "sourceKind 非 local_folder 时不应仅因 local-* documentId 误判成本地文件夹");
|
|
|
|
const context = loadRuntimeFactory();
|
|
class FakeCommandBadge extends context.HTMLElement {
|
|
constructor(kind) {
|
|
super();
|
|
this.kind = kind;
|
|
}
|
|
getAttribute(name) {
|
|
return name === "data-kind" ? this.kind : "";
|
|
}
|
|
}
|
|
class FakeCommandRow extends context.HTMLElement {
|
|
constructor(attrs, textContent = "", badgeKind = "") {
|
|
super();
|
|
this.attrs = attrs;
|
|
this.textContent = textContent;
|
|
this.badge = badgeKind ? new FakeCommandBadge(badgeKind) : null;
|
|
}
|
|
getAttribute(name) {
|
|
return this.attrs[name] || "";
|
|
}
|
|
querySelector(selector) {
|
|
return selector === ".tree-kind-badge" ? this.badge : null;
|
|
}
|
|
}
|
|
const runtime = context.__factory({
|
|
currentSourceKind: () => "local_folder",
|
|
currentRootUri: () => "file:///mnt/Data1T/mnote",
|
|
cssEscape: (value) => String(value).replace(/"/g, '\\"'),
|
|
fileTreeRuntimeFunction: () => null,
|
|
localFilePathFromAssetId: (assetId) =>
|
|
String(assetId || "").replace(/^local-file:/, ""),
|
|
resolveWorkspaceId: () => "local-folder",
|
|
runtimeState: { activeTreeContextMenu: null },
|
|
selectedSidebarFileTreeSelection: { selectedRowIds: new Set(), focusedRowId: null },
|
|
});
|
|
assert.equal(
|
|
runtime.classifySidebarFileTreeAsset(new FakeCommandRow({}, "legacy.luckysheet", "luckysheet")),
|
|
"table",
|
|
"legacy luckysheet 行仍应归为 table 资源",
|
|
);
|
|
const dirtyCommandRow = new FakeCommandRow({
|
|
"data-row-id": "local:markdown:docs/Page.md",
|
|
"data-row-kind": "markdown",
|
|
"data-document-id": "local-md:docs~2FPage.md",
|
|
"data-local-relative-path": "docs/Page.md",
|
|
}, "Page.md");
|
|
const commandContextRuntime = context.window.__mnoteFileTreeContextMenuRuntime;
|
|
const dirtyContext = commandContextRuntime.buildFileTreeCommandContext(
|
|
dirtyCommandRow,
|
|
{
|
|
schema: "mnote.open_editors_snapshot.v1",
|
|
editors: [
|
|
{
|
|
documentId: "local-md:docs~2FPage.md",
|
|
dirtyState: "dirty",
|
|
workspacePath: { relativePath: "docs/Page.md" },
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
{
|
|
currentSourceKind: () => "local_folder",
|
|
workspaceReadonly: () => false,
|
|
selection: { selectedRowIds: new Set(["local:markdown:docs/Page.md"]) },
|
|
queryRowById: () => dirtyCommandRow,
|
|
},
|
|
);
|
|
assert.equal(dirtyContext["workspace.sourceKind"], "local_folder");
|
|
assert.equal(dirtyContext["tree.selectionCount"], 1);
|
|
assert.equal(dirtyContext["tree.selectionResourceKind"], "page");
|
|
assert.equal(dirtyContext["tree.targetResourceKind"], "page");
|
|
assert.equal(dirtyContext["editor.dirty"], true);
|
|
assert.equal(dirtyContext["editor.dirtyState"], "dirty");
|
|
assert.equal(dirtyContext["ai.canWrite"], false);
|
|
assert.equal(
|
|
commandContextRuntime.evaluateSidebarFileTreeWhen(dirtyContext, "!workspace.readonly && !editor.dirty"),
|
|
false,
|
|
"dirty 目标下菜单与快捷键写操作应被统一 command context 禁用",
|
|
);
|
|
const readonlyContext = commandContextRuntime.buildFileTreeCommandContext(
|
|
dirtyCommandRow,
|
|
{ editors: [] },
|
|
null,
|
|
{
|
|
currentSourceKind: () => "local_folder",
|
|
workspaceReadonly: () => true,
|
|
selection: { selectedRowIds: new Set(["local:markdown:docs/Page.md"]) },
|
|
queryRowById: () => dirtyCommandRow,
|
|
},
|
|
);
|
|
assert.equal(readonlyContext["workspace.readonly"], true);
|
|
assert.equal(
|
|
commandContextRuntime.evaluateSidebarFileTreeWhen(readonlyContext, "!workspace.readonly"),
|
|
false,
|
|
"readonly 目标下菜单与快捷键写操作应被统一 command context 禁用",
|
|
);
|
|
const cleanTargetRow = new FakeCommandRow({
|
|
"data-row-id": "local:markdown:docs/Clean.md",
|
|
"data-row-kind": "markdown",
|
|
"data-document-id": "local-md:docs~2FClean.md",
|
|
"data-local-relative-path": "docs/Clean.md",
|
|
}, "Clean.md");
|
|
const selectedRows = new Map([
|
|
["local:markdown:docs/Clean.md", cleanTargetRow],
|
|
["local:markdown:docs/Page.md", dirtyCommandRow],
|
|
]);
|
|
const multiSelectDirtyContext = commandContextRuntime.buildFileTreeCommandContext(
|
|
cleanTargetRow,
|
|
{
|
|
editors: [
|
|
{
|
|
documentId: "local-md:docs~2FPage.md",
|
|
dirtyState: "Dirty",
|
|
workspacePath: { relativePath: "docs/Page.md" },
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
{
|
|
currentSourceKind: () => "local_folder",
|
|
workspaceReadonly: () => false,
|
|
selection: { selectedRowIds: new Set(["local:markdown:docs/Clean.md", "local:markdown:docs/Page.md"]) },
|
|
queryRowById: (rowId) => selectedRows.get(rowId),
|
|
},
|
|
);
|
|
assert.equal(multiSelectDirtyContext["tree.selectionCount"], 2);
|
|
assert.equal(multiSelectDirtyContext["tree.selectionResourceKind"], "page");
|
|
assert.equal(multiSelectDirtyContext["editor.dirty"], true);
|
|
assert.equal(multiSelectDirtyContext["editor.dirtyState"], "dirty");
|
|
assert.equal(
|
|
commandContextRuntime.evaluateSidebarFileTreeWhen(multiSelectDirtyContext, "!workspace.readonly && !editor.dirty"),
|
|
false,
|
|
"多选中任一非 target 行 dirty 时,菜单与快捷键写操作也必须禁用",
|
|
);
|
|
const dirtyAssetRow = new FakeCommandRow({
|
|
"data-row-id": "local:asset:assets/report.pdf",
|
|
"data-row-kind": "asset",
|
|
"data-asset-id": "local-file:assets/report.pdf",
|
|
"data-local-relative-path": "assets/report.pdf",
|
|
}, "report.pdf");
|
|
const dirtyAssetContext = commandContextRuntime.buildFileTreeCommandContext(
|
|
dirtyAssetRow,
|
|
{
|
|
resourceEditors: [
|
|
{
|
|
assetId: "local-file:assets/report.pdf",
|
|
dirtyState: "ExternalModified",
|
|
workspacePath: { relativePath: "assets/report.pdf", assetId: "local-file:assets/report.pdf" },
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
{
|
|
currentSourceKind: () => "local_folder",
|
|
workspaceReadonly: () => false,
|
|
selection: { selectedRowIds: new Set(["local:asset:assets/report.pdf"]) },
|
|
queryRowById: () => dirtyAssetRow,
|
|
},
|
|
);
|
|
assert.equal(dirtyAssetContext["tree.selectionResourceKind"], "asset");
|
|
assert.equal(dirtyAssetContext["tree.targetIsAsset"], true);
|
|
assert.equal(dirtyAssetContext["editor.dirty"], true);
|
|
assert.equal(dirtyAssetContext["editor.dirtyState"], "external_modified");
|
|
assert.equal(
|
|
commandContextRuntime.evaluateSidebarFileTreeWhen(dirtyAssetContext, "!workspace.readonly && !editor.dirty"),
|
|
false,
|
|
"dirty resource tab 对应 asset 的 rename/delete 也必须禁用",
|
|
);
|
|
|
|
assert.equal(
|
|
await runCopyId(runtime, context, {
|
|
contextKind: "filetree",
|
|
rowKind: "folder",
|
|
documentId: "local-dir:docs",
|
|
rowId: "local:folder:docs",
|
|
workspacePath: folderWorkspacePath,
|
|
title: "docs",
|
|
}),
|
|
"/mnt/Data1T/mnote/docs",
|
|
"复制本地文件夹页面 ID 应得到绝对路径",
|
|
);
|
|
|
|
assert.equal(
|
|
await runCopyId(runtime, context, {
|
|
contextKind: "filetree",
|
|
rowKind: "markdown",
|
|
documentId: "local-md:docs~2FPage.md",
|
|
rowId: "local:markdown:docs~2FPage.md",
|
|
workspacePath: markdownWorkspacePath,
|
|
title: "Page",
|
|
}),
|
|
"/mnt/Data1T/mnote/docs/Page.md",
|
|
"复制本地 Markdown 页面 ID 应得到绝对路径",
|
|
);
|
|
|
|
assert.equal(
|
|
await runCopyId(runtime, context, {
|
|
contextKind: "filetree",
|
|
rowKind: "asset",
|
|
assetId: "local-file:assets/report.pdf",
|
|
rowId: "local:asset:assets/report.pdf",
|
|
workspacePath,
|
|
title: "report.pdf",
|
|
}),
|
|
"/mnt/Data1T/mnote/assets/report.pdf",
|
|
"复制本地资源 ID 应得到绝对路径",
|
|
);
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
copied: [
|
|
"/mnt/Data1T/mnote/docs",
|
|
"/mnt/Data1T/mnote/docs/Page.md",
|
|
"/mnt/Data1T/mnote/assets/report.pdf",
|
|
],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|