Files
mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs
T

1267 lines
76 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! MNOTE 通用页面布局组件(Wolai / Notion 风格)
use crate::ssr::pages::admin::AdminAccessPolicyPanel;
use leptos::prelude::*;
const SIDEBAR_TREE_JS: &str = r##"
(function(){
if (window.__mnoteSidebarTreeRuntimeStarted) return;
// sidebar-tree-runtime.js module has NOT loaded yet.
// This inline script provides a minimal bootstrap until the module arrives.
// Module script is loaded via type="module" before this inline block.
window.__mnoteSidebarTreeRuntimeLoading = true;
var _checkInterval = setInterval(function() {
if (window.__mnoteSidebarTreeRuntimeStarted) {
clearInterval(_checkInterval);
window.__mnoteSidebarTreeRuntimeLoading = false;
}
}, 100);
// If module hasn't loaded after 5 seconds, stop polling
setTimeout(function() {
if (_checkInterval) clearInterval(_checkInterval);
}, 5000);
})();
"##;
/// MNOTE Wolai 风格页面布局
///
/// 包含左侧栏 + 内容区的双栏布局。
/// 侧栏显示品牌、导航链接和可选的页面树。
///
/// # 用法
///
/// ```ignore
/// view! {
/// <PageLayout current_nav="home" sidebar_tree_html={None}>
/// <section>...</section>
/// </PageLayout>
/// }
/// ```
#[component]
pub fn PageLayout(
children: Children,
current_nav: &'static str,
/// 侧栏页面树 HTML(可选),由路由 handler 渲染
#[prop(optional)]
sidebar_tree_html: Option<String>,
/// 工作区名称(可选),显示在侧栏顶部
#[prop(optional)]
workspace_name: Option<String>,
/// workspace shell 侧栏 sections HTML(可选),由 projection 渲染
#[prop(optional)]
workspace_sidebar_html: Option<String>,
/// 顶栏当前页面标题(可选)
#[prop(optional)]
topbar_title: Option<String>,
/// 是否显示管理员授权能力
#[prop(optional)]
show_admin_access_policy: bool,
/// 是否启用树实时流
#[prop(optional, default = true)]
enable_tree_live: bool,
) -> impl IntoView {
let _ = show_admin_access_policy;
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
let ws_name = workspace_name
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "开发用户 的空间".to_string());
let topbar_title = topbar_title
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "个人".to_string());
let sidebar_sections_html = workspace_sidebar_html
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| {
let dataset = serde_json::json!({
"workspaces": [{ "id": "default", "name": ws_name.clone() }],
"documents": []
});
let projection = crate::workspace_shell::build_workspace_shell_projection(
&dataset, "default", None, &ws_name,
);
crate::workspace_shell::render_workspace_shell_sidebar_html(
&projection,
Some(sidebar_tree_html.as_str()),
None,
None,
)
});
let tree_live_bootstrap = serde_json::json!({
"schema": "mnote.tree_live_bootstrap.v1",
"disabled": !enable_tree_live,
"transport": if enable_tree_live { "convex-command-log-ws" } else { "disabled" },
"workspaceId": null,
"rootIds": [],
"initialRevision": null,
"endpoint": "/api/tree/events",
"wsEndpoint": "/api/realtime/ws",
"views": ["page-tree", "file-tree"]
})
.to_string();
let admin_access_policy_template = crate::ssr::render_view(leptos::view! {
<AdminAccessPolicyPanel workspace_name={ws_name.clone()} is_admin=true boot_script=false />
});
let user_access_policy_template = crate::ssr::render_view(leptos::view! {
<AdminAccessPolicyPanel workspace_name={ws_name.clone()} is_admin=false boot_script=false />
});
view! {
<div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
<aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar">
<div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity">
<a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a>
<button
type="button"
class="mnote-workspace-source-trigger"
data-testid="mnote-workspace-source-trigger"
data-mnote-action="open-workspace-source-menu"
aria-haspopup="menu"
aria-expanded="false"
title="切换我的空间或本地文件夹"
>
<span class="sidebar-workspace-name">{ws_name.clone()}</span>
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
</button>
</div>
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作" data-testid="wolai-sidebar-quick-actions">
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
<button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
<button
type="button"
title="更多"
aria-label="更多"
data-testid="mnote-account-menu-trigger"
data-mnote-action="open-account-menu"
aria-haspopup="menu"
aria-expanded="false"
>
<span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span>
</button>
</nav>
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
<div hidden data-testid="mnote-admin-access-policy-template-admin" inner_html={admin_access_policy_template}></div>
<div hidden data-testid="mnote-admin-access-policy-template-user" inner_html={user_access_policy_template}></div>
<script inner_html={crate::ssr::pages::admin::ADMIN_POLICY_SCRIPT.to_string()}></script>
<script type="module" src="/api/mnote-browser-runtime/resource-open-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/local-upload-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/filetree-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/filetree-selection-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/filetree-context-menu-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/filetree-dnd-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/filetree-keyboard-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/sidebar-shell-runtime.js"></script>
<script type="module" src="/api/mnote-browser-runtime/sidebar-tree-runtime.js"></script>
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
<script type="module" src="/api/mnote-browser-runtime/tree-live-controller.js"></script>
</aside>
<div class="mnote-sidebar-resizer" data-mnote-sidebar-resizer="true" role="separator" aria-orientation="vertical" aria-label="调整侧栏宽度"></div>
<div class="mnote-main">
<header class="wolai-topbar" data-testid="wolai-topbar">
<div class="wolai-topbar-left">
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏" aria-pressed="false" data-mnote-action="toggle-sidebar" data-testid="wolai-sidebar-toggle"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
<nav class="wolai-breadcrumb" aria-label="页面路径">
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
<span class="wolai-breadcrumb-separator" aria-hidden="true">""</span>
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
</nav>
</div>
<div class="wolai-topbar-actions" aria-label="页面操作">
<span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="页面历史" aria-label="历史" data-mnote-action="open-page-history"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="页面选项和全局选项" aria-label="更多" data-testid="wolai-page-settings-trigger" data-mnote-action="open-page-settings"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
</div>
</header>
<article class="mnote-content">
{children()}
</article>
<div class="wolai-floating-actions" aria-label="浮动操作">
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
</div>
</div>
</div>
}
}
#[cfg(test)]
mod tests {
const SIDEBAR_TREE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-tree-runtime.js");
const SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-tree-live-apply-runtime.js");
const SIDEBAR_FILETREE_OPEN_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-filetree-open-runtime.js");
const SIDEBAR_FILETREE_COMMAND_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-filetree-command-runtime.js");
const SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-filetree-upload-runtime.js");
const SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-attachment-open-runtime.js");
const SIDEBAR_SHELL_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-shell-runtime.js");
const SIDEBAR_WORKSPACE_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-workspace-runtime.js");
const SIDEBAR_PAGE_TREE_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-tree-runtime.js");
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-runtime.js");
const SIDEBAR_PAGE_SETTINGS_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-settings-runtime.js");
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
include_str!("../../../browser/filetree-context-menu-runtime.js");
const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js");
const FILETREE_KEYBOARD_RUNTIME_JS: &str =
include_str!("../../../browser/filetree-keyboard-runtime.js");
const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js");
const FILETREE_SELECTION_RUNTIME_JS: &str =
include_str!("../../../browser/filetree-selection-runtime.js");
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
fn js_function_body(source: &str, name: &str) -> String {
let marker = format!("function {name}(");
let start = source.find(&marker).expect("js function exists");
let rest = &source[start..];
let brace = rest.find('{').expect("js function body starts");
let mut depth = 0usize;
let mut end = None;
for (offset, ch) in rest[brace..].char_indices() {
if ch == '{' {
depth += 1;
} else if ch == '}' {
depth -= 1;
if depth == 0 {
end = Some(brace + offset + 1);
break;
}
}
}
rest[..end.expect("js function body ends")].to_string()
}
#[test]
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnoteNavigationInFlight"));
assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("data-mnote-navigation-pending"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS
.contains("data-global-option-checkbox=\"showHeadingNumbers\""));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'create"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'remove"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'rename"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-tree-local-command-applied', 'move"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function sortOrderFromDelta(data)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:assets-changed"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyAssetsChangedToFileTree"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installMindmapAssetFetchObserver"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("parseMindmapApiTarget"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-assets-local-applied"));
assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("application/x-mnote-page-tree-node"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("dragstart"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("drop"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-drop-feedback"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("action: 'move'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebar-file-tree-root"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.open"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.asset.open"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openConvexAssetFromFileTree"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildMindmapOpenPath"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isMindmapAssetDetail"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("^思维导图"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openMindmapAssetInDocumentShell"));
assert!(
SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("data-mnote-last-mindmap-asset-open-mode")
);
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("mindmap-object-shell"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("navigateToMindmapObject(documentId, assetId"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("__mnoteDocumentPaneRuntime?.openPrimaryMindmap"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shortMindmapFileName"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("assetType: assetType || null"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-object-identity"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("readFileTreeObjectIdentity"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("objectIdentity: objectIdentity"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("workspaceId: resolveWorkspaceId(fileRow)"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/media/sign?assetId="));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function localFilePathFromAssetId"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/local-folder/files/open"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("local-file:"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fetchCurrentOnlyOfficeUserId"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/auth/whoami"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildOnlyOfficeOpenUrl"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("target.searchParams.set('userId'"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("window.open(officeUrl,"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.internal-drop"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.external-drop"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("beginFileTreeInlineRename"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("tree-rename-input"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebarFileTreeClipboard"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pasteSidebarFileTreeClipboard"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function recentLocalRootsStorageKey"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("data-mnote-actor-id"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("requestOpenLocalFolder"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("autoOpenRecentLocalRootOnHome"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("sourceKind', 'local_folder"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("switchToCloudWorkspace"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-switch-cloud-workspace"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-recent-local-root"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-local-folder-authorized-roots"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-local-folder-authorized-root"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("已授权文件夹"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("fetch('/api/user/access-policy'"));
}
#[test]
fn page_ai_context_uses_page_aggregate_subtree_as_single_truth() {
assert!(
!SIDEBAR_TREE_RUNTIME_JS.contains("pageSubtreeSource: 'local'"),
"页面 AI context 不能从编辑器 DOM 派生 local page subtree;必须以 Rust Page Aggregate projection 为准"
);
assert!(
!SIDEBAR_TREE_RUNTIME_JS.contains("buildPageAiLocalSubtree(localBlocks"),
"页面 AI context 不应调用本地 page subtree builder"
);
}
#[test]
fn page_ai_fast_path_is_not_local_first_main_path() {
assert!(
SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local-first 页面 AI 不应继续加厚 page-ai fast-path;本地编辑应走受控文件工具"
);
}
#[test]
fn page_ai_local_source_passes_file_reference_fields_to_agent_run() {
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentRootUri()"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sourceKind: currentSourceKind()"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (rootUri && !payload.rootUri) payload.rootUri = rootUri"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageContext: scopedContext.pageContext"));
assert!(
SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
);
}
#[test]
fn page_ai_uses_backend_acp_session_runtime_store() {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/hermes/client/sessions?"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("params.set('source', 'acp')"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-rename"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-delete"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-resume"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-search"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiUsageSummary"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("usage.updated"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("thought.delta"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permission.requested"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiHidePermissionDialog"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("if (!message.resolved)"));
assert!(
!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"),
"已决 ACP permission 事件不能继续展示假审批按钮"
);
assert!(
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("session.info.updated"),
"ACP SessionInfoUpdate 事件应通过 session.info.updated SSE 转发到前端"
);
assert!(
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("plan.updated"),
"ACP PlanUpdate 事件应通过 plan.updated SSE 转发到前端"
);
assert!(
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-plan"),
"plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板"
);
assert!(
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("执行计划 · "),
"plan 面板标题应显示执行计划和步数"
);
}
#[test]
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_private"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_shared"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("convex_acp_runtime_store"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("本地私有"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("共享会话"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("云端会话"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sessionStorage:"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permissionLevel:"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("shareId:"));
}
#[test]
fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'"));
assert!(!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("默认 (Hermes HTTP)"));
}
#[test]
fn sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("usePreferredSidebarSnapshot"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("preferredSidebarSnapshot"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("liveSidebarTitle"));
}
#[test]
fn page_layout_sidebar_toggle_is_wired_to_shell_state() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function toggleWorkspaceSidebar"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("sidebarShellRuntimeFunction('toggleWorkspaceSidebar')"));
assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function toggleWorkspaceSidebar"));
assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("data-mnote-sidebar-collapsed"));
assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function installWorkspaceSidebarResizer"));
assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-mnote-action=\"toggle-sidebar\"]"));
}
#[test]
fn sidebar_workspace_source_switch_remembers_real_cloud_workspace_before_local_folder() {
assert!(
!SIDEBAR_TREE_RUNTIME_JS.contains("rememberCloudWorkspaceId(resolveWorkspaceId(document.body))"),
"进入本地文件夹前不能用 document.body 推导 workspaceId;它会在无 workspaceId URL 时回退成 default"
);
assert!(
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("rememberCurrentCloudWorkspaceId()"),
"进入本地文件夹前应从 URL 或 DOM 子树读取真实 workspaceId"
);
assert!(
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("normalized.indexOf('local-ws:') === 0"),
"本地文件夹 workspaceId 不能写入 lastCloudWorkspaceId"
);
assert!(
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("stored.trim().indexOf('local-ws:') !== 0"),
"切回工作区时不能复用 local-ws:* 作为旧云 workspace"
);
assert!(
!SIDEBAR_TREE_RUNTIME_JS
.contains("targetUrl.searchParams.set('sourceKind', 'convex_workspace')"),
"默认来源菜单不能跳到已退出 dev 主链的 convex_workspace"
);
}
#[test]
fn sidebar_tree_runtime_polls_local_folder_without_browser_reload() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("startLocalFolderSidebarWatch"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentWorkspaceId()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileProjection(filePayload.result || filePayload)"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
);
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fetch(window.location.href, { headers: { accept: 'text/html' } })"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("replaceSidebarTreeFromDocument(nextDocument, 'sidebar-tree-root')"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("window.location.reload"));
}
#[test]
fn sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder() {
let current_document_function =
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "currentDocumentId");
let upload_function = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "uploadFileToMediaAsset");
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentSourceKind() === 'local_folder'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/local-folder/assets/upload"));
assert!(
upload_function.contains("var rootUri = currentRootUri();"),
"本地上传必须复用 currentRootUri(),否则 / 页面由 body dataset 提供 rootUri 时 .md 上传会失败"
);
assert!(
current_document_function.contains("params.get('pageId')"),
"SQLite/local-first 根入口可能通过 pageId 或 DOM 暴露当前页面,不能只解析 /documents/:id"
);
assert!(
current_document_function.contains("data-pane-document-id")
&& current_document_function.contains("data-document-id"),
"主编辑器上传应能从当前文档 DOM 回退解析 documentId"
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("localForm.append('rootUri', rootUri)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("localForm.append('uploadIntent', uploadIntent)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("uploadIntent: 'editor.markdown.attach'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("uploadIntent: 'filetree.folder.drop'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("localForm.append('documentId', documentId)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isLocalUploadedAsset(asset)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildLocalOnlyOfficeOpenUrl"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("localOfficeUrl"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeIconKindForFileName(title)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:local-assets-changed"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function resolveEditorUploadContext"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("localUploadRuntimeFunction('resolveEditorUploadContext')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("localUploadRuntimeFunction('openEditorUploadFilePicker')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("localUploadRuntimeFunction('insertUploadedAssetIntoEditor')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("localUploadRuntimeFunction('uploadFileToMediaAsset')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("localUploadRuntimeFunction('uploadMediaAsset')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("localUploadRuntimeFunction('uploadFilesWithResolvedTarget')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-pane-role=\"primary\""));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options))"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("editorRoot: uploadContext.root"));
}
#[test]
fn local_upload_runtime_contains_editor_upload_context_helpers() {
const LOCAL_UPLOAD_RUNTIME_JS: &str =
include_str!("../../../browser/local-upload-runtime.js");
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function resolveEditorUploadContext"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function editorRootFromUploadOptions"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function openEditorUploadFilePicker"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function fetchWithTimeout"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadLocalFolderAsset"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadMediaAsset"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function insertUploadedAssetIntoEditor"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function dispatchUploadedEditorChange"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function persistUploadedEditorChange"));
assert!(
LOCAL_UPLOAD_RUNTIME_JS.contains("function persistLocalFolderSelfChangeSuppression")
);
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("editorSource: 'local-upload-runtime'"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("mnote:leptos-tiptap-spike:change"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFileToMediaAsset"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFilesWithResolvedTarget"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-last-upload-inserted"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFileToMediaAsset: uploadFileToMediaAsset"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
}
#[test]
fn sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeRuntimeFunction"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeRowDocumentId')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeRowAssetId')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeRowLocalRelativePath')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeAssetDownloadDetail')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeChildCount')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('revealFileTreeRow')"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('revealFileTreeAssetRow')")
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('appendUploadedAssetRow')")
);
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeUploadTargetFallback')"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight')"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight')"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('readProjection')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('readSidebarDataset')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('readDatasetProjection')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('projectionItems')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('hasProjectionItems')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('nodeIdOf')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('rowIdOf')"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')"));
let document_id_body =
js_function_body(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, "fileTreeRowDocumentId");
assert!(
document_id_body.contains("data-document-id")
&& document_id_body.contains("data-doc-id"),
"inline fallback 必须保留旧 data attribute 解析"
);
let replace_body = js_function_body(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
"replaceSidebarTreeFromDocument",
);
assert!(
replace_body.contains("current.innerHTML = next.innerHTML")
&& replace_body.contains("current.setAttribute(attr.name, attr.value)"),
"inline fallback 必须保留 DOM copy 行为"
);
let apply_body = js_function_body(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
"applyLocalFolderSidebarSnapshot",
);
assert!(
apply_body.contains("options.pageRootId || 'sidebar-tree-root'")
&& apply_body.contains("options.fileRootId || 'sidebar-file-tree-root'")
&& apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, pageRootId)")
&& apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, fileRootId)"),
"inline fallback 必须保留 sidebar/page tree DOM 应用行为"
);
let watch_applied_body = js_function_body(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
"markLocalFolderWatchApplied",
);
assert!(
watch_applied_body.contains("data-mnote-local-folder-watch-applied")
&& watch_applied_body.contains("String(value || 'projection')"),
"inline fallback 必须保留 local folder watch projection 标记"
);
}
#[test]
fn filetree_runtime_contains_row_accessor_helpers() {
assert!(FILETREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowDocumentId"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowAssetId"));
assert!(FILETREE_RUNTIME_JS.contains("function decodeLocalEncodedPath"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowLocalRelativePath"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowLocalUploadTargetRelativePath"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeAssetDownloadDetail"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowsByRowIds"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeChildCount"));
assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeRow"));
assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeAssetRow"));
assert!(FILETREE_RUNTIME_JS.contains("function appendUploadedAssetRow"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetFallback"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowsForUploadPreflight"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeDocumentParentsForPreflight"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeTargetChildrenForPreflight"));
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeDropPreflightRows"));
assert!(
FILETREE_RUNTIME_JS.contains("function fileTreeDocumentWorkspacesForUploadPreflight")
);
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetPreflightBody"));
assert!(FILETREE_RUNTIME_JS.contains("function shortMindmapFileName"));
assert!(FILETREE_RUNTIME_JS.contains("function parseMindmapApiTarget"));
assert!(FILETREE_RUNTIME_JS.contains("function requestMethod"));
assert!(FILETREE_RUNTIME_JS.contains("function requestBodyHasMindmapCreateOnly"));
assert!(FILETREE_RUNTIME_JS.contains("function readProjection"));
assert!(FILETREE_RUNTIME_JS.contains("function readSidebarDataset"));
assert!(FILETREE_RUNTIME_JS.contains("function readDatasetProjection"));
assert!(FILETREE_RUNTIME_JS.contains("function projectionItems"));
assert!(FILETREE_RUNTIME_JS.contains("function hasProjectionItems"));
assert!(FILETREE_RUNTIME_JS.contains("function nodeIdOf"));
assert!(FILETREE_RUNTIME_JS.contains("function rowIdOf"));
assert!(FILETREE_RUNTIME_JS.contains("function parentIdOf"));
assert!(FILETREE_RUNTIME_JS.contains("function titleOf"));
assert!(FILETREE_RUNTIME_JS.contains("function fileWorkspaceRelativePath"));
assert!(FILETREE_RUNTIME_JS.contains("function groupRowsByParent"));
assert!(FILETREE_RUNTIME_JS.contains("function replaceSidebarTreeFromDocument"));
assert!(FILETREE_RUNTIME_JS.contains("function applyLocalFolderSidebarSnapshot"));
assert!(FILETREE_RUNTIME_JS.contains("function markLocalFolderWatchApplied"));
assert!(FILETREE_RUNTIME_JS.contains("appendUploadedAssetRow: appendUploadedAssetRow"));
assert!(FILETREE_RUNTIME_JS.contains("groupRowsByParent: groupRowsByParent"));
assert!(FILETREE_RUNTIME_JS
.contains("replaceSidebarTreeFromDocument: replaceSidebarTreeFromDocument"));
assert!(FILETREE_RUNTIME_JS
.contains("applyLocalFolderSidebarSnapshot: applyLocalFolderSidebarSnapshot"));
assert!(FILETREE_RUNTIME_JS
.contains("markLocalFolderWatchApplied: markLocalFolderWatchApplied"));
assert!(FILETREE_RUNTIME_JS
.contains("fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight"));
assert!(FILETREE_RUNTIME_JS
.contains("fileTreeDocumentParentsForPreflight: fileTreeDocumentParentsForPreflight"));
assert!(FILETREE_RUNTIME_JS
.contains("fileTreeTargetChildrenForPreflight: fileTreeTargetChildrenForPreflight"));
assert!(
FILETREE_RUNTIME_JS.contains("fileTreeDropPreflightRows: fileTreeDropPreflightRows")
);
assert!(FILETREE_RUNTIME_JS.contains(
"fileTreeDocumentWorkspacesForUploadPreflight: fileTreeDocumentWorkspacesForUploadPreflight"
));
assert!(FILETREE_RUNTIME_JS
.contains("fileTreeUploadTargetPreflightBody: fileTreeUploadTargetPreflightBody"));
assert!(FILETREE_RUNTIME_JS.contains("shortMindmapFileName: shortMindmapFileName"));
assert!(FILETREE_RUNTIME_JS.contains("parseMindmapApiTarget: parseMindmapApiTarget"));
assert!(FILETREE_RUNTIME_JS.contains("requestMethod: requestMethod"));
assert!(FILETREE_RUNTIME_JS
.contains("requestBodyHasMindmapCreateOnly: requestBodyHasMindmapCreateOnly"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight')"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody')"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS
.contains("fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('shortMindmapFileName')"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('parseMindmapApiTarget')")
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('requestMethod')"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("fileTreeRuntimeFunction('requestBodyHasMindmapCreateOnly')"));
assert!(FILETREE_RUNTIME_JS
.contains("uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')"));
assert!(FILETREE_RUNTIME_JS
.contains("uploadIntent: String(detail.uploadIntent || 'editor.markdown.attach')"));
}
#[test]
fn sidebar_filetree_selection_runtime_helpers_are_externalized_with_inline_fallback() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeSelectionRuntimeFunction"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeSelectionRuntimeFunction('visibleFileTreeRows')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows')"));
let select_body = js_function_body(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS,
"selectSidebarFileTreeRow",
);
assert!(
select_body.contains("shiftKey && sidebarFileTreeSelection.anchorRowId")
&& select_body.contains("ctrlKey"),
"inline fallback 必须保留 shift range 与 ctrl/meta toggle"
);
let select_document_body = js_function_body(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS,
"selectSidebarFileTreeDocument",
);
assert!(
select_document_body
.contains("selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false })"),
"document selection 应复用 filetree row selection runtime/fallback"
);
}
#[test]
fn filetree_selection_runtime_contains_selection_helpers() {
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function visibleFileTreeRows"));
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function syncSidebarFileTreeSelection"));
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function selectSidebarFileTreeRow"));
assert!(
FILETREE_SELECTION_RUNTIME_JS.contains("function selectedSidebarFileTreeRowIdsForDrag")
);
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function selectedSidebarFileTreeRows"));
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("tree.filetree.selection.changed"));
}
#[test]
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("mnote.pendingLocalFolderRestoreFiletreeRowId"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("data-mnote-local-folder-restore-focused-row-id"));
}
#[test]
fn sidebar_runtime_routes_local_mindmap_and_office_assets() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("withLocalMindmapSourceParams"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("url.searchParams.set('sourceKind', 'local_folder')")
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("url.searchParams.set('rootUri', rootUri)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (!parentRow && currentSourceKind() === 'local_folder' && objectKind === 'mindmap') return false;"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (!appended && currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("data-mnote-last-mindmap-asset-open-mode', 'local-mindmap-object-shell'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildLocalOnlyOfficeOpenUrl"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isLocalAsset ? onlyOfficeUrl"));
}
#[test]
fn sidebar_local_folder_authorized_root_normalizes_plain_path_root_uri() {
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("function normalizeGrantedLocalFolderRootUri(grant)"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (rootPath) return pathToFileRootUri(rootPath);"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (rootUri) return pathToFileRootUri(rootUri);"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("var rootUri = normalizeGrantedLocalFolderRootUri(grant);"));
assert!(
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function isDefaultWorkspaceAutoGrant(grant)")
);
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (isDefaultWorkspaceAutoGrant(grant)) return;"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function recentLocalRootLabel(rootUri)"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("button.textContent = recentLocalRootLabel(rootUri);"));
assert!(!SIDEBAR_TREE_RUNTIME_JS
.contains("button.textContent = rootUri.replace(/^file:\\/\\//, '')"));
}
#[test]
fn sidebar_runtime_supports_resizable_filetree_and_hover_titles() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function installWorkspaceSidebarResizer()"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("sidebarShellRuntimeFunction('installWorkspaceSidebarResizer')"));
assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("data-mnote-sidebar-resizer"));
assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("mnote.workspace.sidebarWidth.v1"));
assert!(
SIDEBAR_SHELL_RUNTIME_JS.contains("shell.style.setProperty('--mnote-sidebar-width'")
);
assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime"));
assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#)
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"class="tree-link-title" title="' + escapeHtml(title) + '""#));
}
#[test]
fn sidebar_runtime_opens_local_pdf_assets_in_active_resource_tab_by_default() {
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("function shouldOpenLocalResourceInNewWindow(fileName)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("return false;"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("var shouldOpenInNewWindow = forceNewWindow || shouldOpenLocalResourceInNewWindow(localFileName);"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("if (!shouldOpenInNewWindow && await openLocalResourceInActiveTab({"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
"openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab'"
));
}
#[test]
fn sidebar_tree_runtime_contains_dev_hot_reload_client() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installMnoteDevHotReload"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/dev/hot-reload"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-dev-hot-reload"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.clearInterval(timer)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote:primary-document-activated"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageUiState.pageOptions = null"));
}
#[test]
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openTreeContextMenu"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("mnote-tree-context-menu"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("复制访问链接"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("删除到垃圾桶"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function createFileTreeFolder"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("action: 'create_folder'"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function fileTreeMenuTargetParentId"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("if (action === 'new-folder')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("fileTreeCopyPath(detail, trigger)"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("if (detail.contextKind === 'filetree' && action === 'download')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("downloadSelectedFileTreeAssetRows(detail, trigger)"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("function selectedSidebarFileTreeRowsForDownload"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("function selectedSidebarFileTreeAssetRowsForDownload"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-local-relative-path"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("targetRelativePath"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains(
"recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary)"
));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-filetree-download-count"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function triggerBrowserDownload"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY });"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("var localFilePath = localFilePathFromAssetId(assetId);"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("attachmentMetaCache[assetId] = localMeta;"));
assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains(
"var pageRow = closestAction(event.target, '.tree-row[data-shell-mode=\"page\"]');"
));
assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS
.contains("var action = btn ? btn.getAttribute('data-rust-action') : 'open';"));
assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("var openTrigger = btn || pageRow;"));
assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS
.contains("var workspaceId = resolveWorkspaceId(openTrigger);"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#".wolai-breadcrumb-current [data-page-title-current]"#));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("renderSidebarSnapshot"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("kernel_file_tree_projection"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("deltaNeedsProjectionRefresh"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("upsert_documents"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setTreeLiveApplyError"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("scheduleProjectionRefresh"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
}
#[test]
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreePageTitle(title)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeFileTreePageRenameTitle"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("validateFileTreeRename"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("同级已存在同名页面"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("文件名不能包含"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'doc:' + activeId"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentFileTreeActiveRowId"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("window.location.pathname.match(/^\\/mindmap\\/([^\\/]+)\\/([^\\/]+)/)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
"var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId"
));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileRows('', groupRowsByParent(rows), activeId, activeRowId)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("normalizeMindmapFileTreeTitle"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("shortMindmapFileName(assetId)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("title: 'index.md'"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
}
#[test]
fn sidebar_filetree_runtime_supports_lazy_children_projection() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file/children"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("parentRelativePath"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-filetree-children-loaded"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeLazyChildrenCache"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeExpandedRelativePaths"));
}
#[test]
fn sidebar_tree_delete_to_trash_dispatches_archive_not_purge() {
let delete_trash_start = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.find("if (action === 'delete-trash'")
.expect("delete-trash branch");
let delete_trash_end = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[delete_trash_start..]
.find("function appendTreeContextMenuButton")
.map(|offset| delete_trash_start + offset)
.expect("delete-trash branch end");
let delete_trash_branch =
&SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[delete_trash_start..delete_trash_end];
assert!(delete_trash_branch.contains("action: 'archive'"));
assert!(!delete_trash_branch.contains("action: 'purge'"));
}
#[test]
fn sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection() {
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("function deleteSelectedSidebarFileTreeRows"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("event.key === 'Delete' || event.key === 'Backspace'")
);
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("selectedFileTreeRows.length > 1"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("deleteSelectedSidebarFileTreeRows(trigger || document.body)"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains(
"currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset'"
));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("documentId: fileAssetIds[lf]"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("/api/media/batch"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("/api/mindmap/"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("/api/tables/"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("确认删除选中的 "));
}
#[test]
fn sidebar_filetree_blocks_readonly_paste_and_drop_with_action_status() {
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("function blockReadonlyFileTreeAction"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("data-mnote-filetree-readonly-blocked"));
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("data-mnote-filetree-readonly-message"));
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'")
);
assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight"));
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'")
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('drop'"));
}
#[test]
fn sidebar_filetree_command_context_helper_functions_exist() {
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function buildSidebarFileTreeContext")
);
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen")
);
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'workspace.sourceKind'"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'workspace.readonly'"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.focusKind'"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.selectionCount'"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.selectionResourceKind'"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-workspace-readonly"));
}
#[test]
fn filetree_context_menu_runtime_contains_command_context_helpers() {
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function buildSidebarFileTreeContext"));
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen"));
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function tokenizeWhenExpression"));
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime"));
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'workspace.sourceKind'"));
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'workspace.readonly'"));
assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'tree.selectionCount'"));
}
#[test]
fn sidebar_filetree_context_menu_items_have_when_for_readonly() {
// FileTree 分支的写操作必须带 when: '!workspace.readonly'。
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("when: '!workspace.readonly'"),
"上下文菜单写操作应携带 when: '!workspace.readonly'"
);
// Verify at least one delete-trash item has the when expression
let delete_trash_items = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.match_indices("'delete-trash'")
.count();
assert!(delete_trash_items > 0, "应至少有一个 delete-trash 菜单项");
}
#[test]
fn sidebar_filetree_context_evaluator_uses_readonly_and_selection_count_for_delete_key() {
assert!(
SIDEBAR_TREE_RUNTIME_JS
.contains("evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')"),
"Delete/Backspace 快捷键应至少评估 workspace.readonly 上下文"
);
}
#[test]
fn sidebar_filetree_local_table_bulk_delete_uses_tree_command() {
let table_loop_start = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.find("for (var t = 0; t < plan.tableRows.length; t += 1)")
.expect("table bulk delete loop");
let table_loop_end = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[table_loop_start..]
.find("sidebarFileTreeSelection.selectedRowIds")
.map(|offset| table_loop_start + offset)
.expect("table bulk delete loop end");
let table_loop = &SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[table_loop_start..table_loop_end];
assert!(table_loop.contains("currentSourceKind() === 'local_folder'"));
assert!(table_loop.contains("dispatchTreeCommand(trigger || tableRow"));
assert!(table_loop.contains("action: 'archive'"));
assert!(table_loop.contains("documentId: tableId"));
assert!(table_loop.contains("/api/tables/"));
}
#[test]
fn sidebar_filetree_asset_open_uses_owner_document_id() {
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains(
"var ownerDocumentId = fileRow.getAttribute('data-owner-document-id') || documentId;"
),
"打开资源行时必须优先使用资源 owner document,不能用当前页面 documentId"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("documentId: ownerDocumentId || null"),
"tree.asset.open detail 应携带资源 owner document"
);
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"),
"SSR filetree 行应输出 data-owner-document-id"
);
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"),
"local_folder bundle 资源应从 local-file 路径推导 owner markdown document"
);
}
#[test]
fn sidebar_tree_runtime_keeps_pdf_and_code_assets_out_of_onlyoffice() {
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function inferOnlyOfficeFileType"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("isNonOfficeAttachmentName(name, ext)"));
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("if (ext === 'pdf') return ext;"));
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("mt.indexOf('pdf') >= 0"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote-uploaded-attachment-pdf"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote-uploaded-attachment-code"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'toml'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'json'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'yaml'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'md'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'vue'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'svelte'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'proto'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'dockerfile'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'.gitignore'"));
}
#[test]
fn sidebar_tree_runtime_opens_office_assets_through_resource_shell() {
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function buildOnlyOfficeOpenPath"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains(
"return '/office/' + encodeURIComponent(input.documentId) + '/' + encodeURIComponent(input.assetId)"
));
assert!(
SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("return '/onlyoffice?' + params.toString();")
);
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("target.searchParams.set('mode', input.mode || 'view');"));
assert!(
SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("params.set('mode', input.mode || 'view');")
);
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains(
"{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }"
));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("{ action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("{ action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("function withOfficeEditModeGuard(callback)"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("data-mnote-last-office-edit-mode-requested"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("function openEditorAttachmentEditTab(detail)"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("void openEditorAttachmentEditTab(detail);"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("openLocalOfficeFileInActiveTab(detail, 'edit')"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("openEditorAttachmentNewWindow(detail, 'edit')"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("url.searchParams.set('mode', requestedMode);"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("mode: requestedMode"));
}
#[test]
fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() {
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openPdfEditorAttachment"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openCodeEditorAttachment"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function resolveEditorAttachmentUrl"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("type: 'codeBlock'"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attrs: { language: language }"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("if (isPdfAttachmentFileName(detail.fileName))"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("if (isCodeAttachmentFileName(detail.fileName))"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));
let attachment_class_index = SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.find("attachmentClassForFileName(fileName).split")
.expect("editor attachment links apply type-specific classes");
let local_refresh_index = SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.find("if (localFilePath && !isOnlyOfficeAttachmentHref(href))")
.expect("local file links refresh existence after enhancement");
assert!(attachment_class_index < local_refresh_index);
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("'mnote:leptos-tiptap-spike:ready'"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("window.setTimeout(function()"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("[120, 500, 1200, 2500]"));
assert!(
SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attachmentInitialEnhanceAttempts >= 120")
);
}
#[test]
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-ws"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("new WebSocket(url.toString())"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("startWithSseFallback"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("liveDeltaNeedsResync(payload)"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("op === 'resync_required'"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("resyncEndpoint"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("tree:resync-requested"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("Delta indicates something changed"));
}
}