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

2052 lines
123 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 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);
})();
"##;
fn browser_runtime_src(asset: &str) -> String {
let base = format!("/api/mnote-browser-runtime/{asset}");
if let Some(cache_buster) = crate::routes::dev_hot::dev_hot_cache_buster() {
format!("{base}?devHot={cache_buster}")
} else {
base
}
}
/// 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,
None,
)
});
let tree_live_bootstrap = serde_json::json!({
"schema": "mnote.tree_live_bootstrap.v1",
"disabled": !enable_tree_live,
"transport": if enable_tree_live { "tree-live-ws" } else { "disabled" },
"workspaceId": null,
"rootIds": [],
"initialRevision": null,
"endpoint": "/api/tree/events",
"wsEndpoint": "/api/realtime/ws",
"views": ["page-tree", "file-tree"]
})
.to_string();
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"
data-state="closed"
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>
<button type="button" title="导航页" aria-label="导航页" data-mnote-action="open-navigation-page"><span class="material-symbols-outlined nav-icon" data-icon="home" aria-hidden="true"></span></button>
<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"
data-state="closed"
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>
<script type="module" src={browser_runtime_src("mnote-ui-runtime.js")}></script>
<script type="module" src={browser_runtime_src("resource-open-runtime.js")}></script>
<script type="module" src={browser_runtime_src("local-upload-runtime.js")}></script>
<script type="module" src={browser_runtime_src("filetree-runtime.js")}></script>
<script type="module" src={browser_runtime_src("filetree-selection-runtime.js")}></script>
<script type="module" src={browser_runtime_src("filetree-context-menu-runtime.js")}></script>
<script type="module" src={browser_runtime_src("filetree-dnd-runtime.js")}></script>
<script type="module" src={browser_runtime_src("filetree-keyboard-runtime.js")}></script>
<script type="module" src={browser_runtime_src("sidebar-shell-runtime.js")}></script>
<script type="module" src={browser_runtime_src("sidebar-tree-runtime.js")}></script>
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
<script type="module" src={browser_runtime_src("local-folder-event-bus-runtime.js")}></script>
<script type="module" src={browser_runtime_src("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="切换侧栏" data-state="closed" aria-haspopup="true" aria-expanded="false" 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" hidden></span>
<button type="button" class="wolai-icon-button" title="星标置顶" aria-label="星标置顶" data-state="closed" aria-haspopup="true" aria-expanded="false" data-mnote-action="toggle-sidebar-shortcut" data-mnote-shortcut-kind="page"><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-state="closed" aria-haspopup="menu" aria-expanded="false" 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="浮动操作">
<a href="/knowledge" data-testid="mnote-floating-knowledge" class="wolai-floating-button wolai-floating-button--knowledge" aria-label="知识库" title="打开知识库" data-mnote-action="open-knowledge-host"><span class="material-symbols-outlined material-symbols-filled" data-icon="travel_explore" aria-hidden="true"></span></a>
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai wolai-floating-button--pi" aria-label="Pi Lab" data-state="closed" aria-haspopup="dialog" aria-expanded="false" title="打开 Pi Lab" data-mnote-action="open-page-ai-pi-lab">{"\u{03c0}"}</button>
</div>
</div>
<div id="mnote-portal-root" data-mnote-portal-root="true"></div>
</div>
}
}
#[cfg(test)]
mod tests {
use leptos::prelude::ElementChild;
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");
// Browser Page AI runtime is served as a module; keep this include explicit so
// resume/journal contract checks inspect the actual shipped JS.
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-runtime.js");
const MNOTE_UI_RUNTIME_JS: &str = include_str!("../../../browser/mnote-ui-runtime.js");
const SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-markdown-runtime.js");
const SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-render-runtime.js");
const SIDEBAR_PAGE_AI_PERMISSION_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-permission-runtime.js");
const SIDEBAR_PAGE_AI_PROFILE_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-profile-runtime.js");
const SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-session-runtime.js");
const SIDEBAR_PAGE_AI_SKILL_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-skill-runtime.js");
const SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-target-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 LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS: &str =
include_str!("../../../browser/local-folder-event-bus-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_PAGE_SETTINGS_RUNTIME_JS.contains("/api/ui/preferences"));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("localStorage.setItem"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("applySearchSwitchState(overlay, { knowledge: false"));
assert!(!SIDEBAR_TREE_RUNTIME_JS
.contains("applySearchSwitchState(overlay, { knowledge: true"));
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains("data-search-switch=\"knowledge\" role=\"switch\" aria-checked=\"false\""));
assert!(!SIDEBAR_TREE_RUNTIME_JS
.contains("data-search-switch=\"knowledge\" role=\"switch\" aria-checked=\"true\""));
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("data-mnote-media-sign-retired"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("/api/media/sign?assetId="));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("/api/media/upload"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-media-upload-retired"));
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 sidebar_live_apply_runtime_uses_persisted_tree_view_state() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("SIDEBAR_TREE_VIEW_STATE_KEY"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function loadSidebarTreeViewState"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function persistSidebarTreeViewState"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function applySidebarTreeViewState"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/view-state"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("mnote.sidebarTreeViewState.v1:{userId}:{workspaceId}:{sourceKind}:{treeKind}:{rootUriHash}:{scopeHash}"));
let render_page_rows =
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "renderPageRows");
assert!(render_page_rows.contains("sidebarTreeViewStateFor('pagetree'"));
assert!(render_page_rows.contains("expandedIds"));
assert!(
!render_page_rows
.contains("var expanded = expandable && item.expandedByDefault !== false"),
"PageTree 不能只依赖 projection expandedByDefault 决定展开"
);
let render_file_rows =
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "renderFileRows");
assert!(render_file_rows.contains("sidebarTreeViewStateFor('filetree'"));
assert!(render_file_rows.contains("expandedRelativePaths"));
}
#[test]
fn page_layout_hides_public_state_and_exposes_sidebar_shortcut_star() {
let html = crate::ssr::render_view(leptos::view! {
<super::PageLayout current_nav="documents" topbar_title={"个人".to_string()}>
<main>"正文"</main>
</super::PageLayout>
});
assert!(!html.contains(r#"data-testid="wolai-public-state">全网公开"#));
assert!(html.contains(r#"data-mnote-action="toggle-sidebar-shortcut""#));
assert!(html.contains(r#"data-mnote-shortcut-kind="page""#));
assert!(html.contains(r#"id="mnote-portal-root""#));
assert!(html.contains("/api/mnote-browser-runtime/mnote-ui-runtime.js"));
}
#[test]
fn mnote_ui_runtime_exposes_toast_api() {
assert!(MNOTE_UI_RUNTIME_JS.contains("window.mnote.toast"));
assert!(MNOTE_UI_RUNTIME_JS.contains("data-mnote-toast-region"));
assert!(MNOTE_UI_RUNTIME_JS.contains("mnote:toast"));
}
#[test]
fn page_layout_adds_dev_hot_cache_buster_to_browser_runtime_scripts() {
let _guard = crate::test_support::hermes_env_lock()
.lock()
.expect("env lock");
std::env::set_var("MNOTE_WEB_DEV_HOT_RELOAD", "1");
let html = crate::ssr::render_view(leptos::view! {
<super::PageLayout current_nav="documents" topbar_title={"个人".to_string()}>
<main>"正文"</main>
</super::PageLayout>
});
std::env::remove_var("MNOTE_WEB_DEV_HOT_RELOAD");
assert!(
html.contains("/api/mnote-browser-runtime/tree-live-controller.js?devHot="),
"dev:hot 下 tree live controller URL 必须带 cache buster,避免浏览器继续执行旧 WS/SSE 逻辑"
);
assert!(
html.contains("/api/mnote-browser-runtime/sidebar-tree-runtime.js?devHot="),
"dev:hot 下 sidebar runtime URL 也必须带 cache buster"
);
assert!(
html.contains("/api/mnote-browser-runtime/local-folder-event-bus-runtime.js?devHot="),
"dev:hot 下 local-folder event bus URL 必须带 cache buster,避免复用旧连接编排逻辑"
);
assert!(
html.contains("/api/mnote-browser-runtime/mnote-ui-runtime.js?devHot="),
"dev:hot 下 UI runtime URL 必须带 cache buster,避免 toast/portal 使用旧版本"
);
}
#[test]
fn page_layout_loads_local_folder_event_bus_before_tree_live_controller() {
let html = crate::ssr::render_view(leptos::view! {
<super::PageLayout current_nav="documents" topbar_title={"个人".to_string()}>
<main>"正文"</main>
</super::PageLayout>
});
let bus_index = html
.find("/api/mnote-browser-runtime/local-folder-event-bus-runtime.js")
.expect("local-folder event bus runtime should be loaded");
let controller_index = html
.find("/api/mnote-browser-runtime/tree-live-controller.js")
.expect("tree live controller runtime should be loaded");
assert!(
bus_index < controller_index,
"local-folder event bus 必须先于 tree-live-controller 加载,确保 local-folder watcher 连接由 bus 接管"
);
}
#[test]
fn sidebar_runtime_supports_shortcuts_and_scoped_filetree() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/sidebar/shortcuts"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("toggle-sidebar-shortcut"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openStarredFolderShortcut"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openNavigationPageForFolder"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openCurrentNavigationPage"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("openCurrentNavigationPage(navigationPageTrigger)")
);
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/navigation/recent"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("await renderPageProjection(sidebarProjection)"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("authRedirectUrl"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("window.location.assign(authRedirectUrl())"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.location.assign(url)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var rootUri = readShortcutRootUri(row);"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("inferLocalRootUriFromWorkspaceId"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-scope"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("toggle-sidebar-folder-shortcut"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
let scope_start = SIDEBAR_TREE_RUNTIME_JS
.find("persistStarredFolderScope(workspaceId, rootUri, relativePath);")
.expect("starred folder should persist scope before loading");
let sidebar_fetch_start = SIDEBAR_TREE_RUNTIME_JS[scope_start..]
.find("fetch(sidebarUrl.toString()")
.expect("starred folder should fetch local page projection")
+ scope_start;
let fetch_start = SIDEBAR_TREE_RUNTIME_JS[scope_start..]
.find("fetch(url.toString()")
.expect("starred folder should fetch scoped projection")
+ scope_start;
assert!(
sidebar_fetch_start < fetch_start,
"星标文件夹请求 scoped filetree 前必须先拉本地页面树投影,避免我的空间旧页面残留"
);
assert!(
scope_start < fetch_start,
"星标文件夹应先写入 fileTreeScope,再请求大目录,避免 live refresh 把视图折回根目录"
);
}
#[test]
fn page_layout_quick_action_opens_current_navigation_page() {
let html = crate::ssr::render_view(leptos::view! {
<super::PageLayout current_nav="documents" topbar_title={"个人".to_string()}>
<main>"正文"</main>
</super::PageLayout>
});
assert!(html.contains(r#"data-mnote-action="open-navigation-page""#));
assert!(html.contains(r#"title="导航页""#));
assert!(!html.contains(r#"href="/actions""#));
}
#[test]
fn page_layout_exposes_provider_neutral_knowledge_rag_settings_only() {
let html = crate::ssr::render_view(leptos::view! {
<super::PageLayout current_nav="documents" topbar_title={"个人".to_string()}>
<main>"正文"</main>
</super::PageLayout>
});
assert!(!html.contains(r#"data-testid="mnote-local-index-settings-toggle""#));
assert!(!html.contains(r#"data-mnote-action="open-index-settings""#));
assert!(!html.contains(r#"data-testid="mnote-knowledge-rag-settings-toggle""#));
assert!(html.contains(r#"data-testid="mnote-floating-knowledge""#));
assert!(html.contains(r#"href="/knowledge""#));
assert!(html.contains(r#"data-mnote-action="open-knowledge-host""#));
assert!(
html.find(r#"data-testid="mnote-floating-knowledge""#)
< html.find(r#"data-testid="wolai-floating-ai""#)
);
assert!(html.contains(r#"data-mnote-action="open-page-ai-pi-lab""#));
assert!(!html.contains(r#"data-mnote-action="open-page-ai""#));
assert!(!html.contains(r#"data-mnote-action="open-knowledge-rag-settings""#));
assert!(html.contains(r#"data-icon="travel_explore""#));
assert!(crate::ssr::styles::MNOTE_CSS.contains(r#"data-icon="travel_explore""#));
assert!(!html.contains(r#"data-testid="mnote-local-ocr-task-toggle""#));
assert!(!html.contains(r#"data-mnote-action="open-ocr-settings""#));
assert!(!html.contains(r#"data-testid="mnote-floating-task-toggle""#));
assert!(!html.contains(r#"data-mnote-action="toggle-ocr-tasks""#));
}
#[test]
fn sidebar_settings_runtime_routes_index_and_ocr_to_knowledge_rag_settings() {
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("mnote-knowledge-rag-settings-popover"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/status?"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/pipeline-events"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/ingest"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/knowledge-rag/prune-registry"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("use-filetree-selection"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-knowledge-rag-action=\"prune\""));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("mnote-knowledge-rag-source-filters"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("filter-sources"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("未关联资料"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("Rerank"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("scheduleKnowledgeRagStatusBridge"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagStatusHasInFlight"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("new EventSource(url.toString())"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("pipeline_status"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("pipeline.pendingEnqueues"));
assert!(
SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-mnote-knowledge-rag-watcher-required")
);
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("runKnowledgeRagStatusBridge"));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagBridgeDelays"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagRegistryStatusDigest"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("registry-status-changed"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagIndeterminateProgress"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-progress-mode"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("indeterminate"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("analyzing"));
assert!(crate::ssr::styles::MNOTE_CSS
.contains("mnote-knowledge-rag-source-progress[data-progress-mode=\"indeterminate\"]"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("delete_submitted"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("delete_completed"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("publicKnowledgeRagDashboardUrl"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("label.hidden = status === 'idle'"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-knowledge-rag-input-status-label"));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("localOcrAutoEnabled"));
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-mnote-local-ocr-auto-retired"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function knowledgeHostUrl"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("knowledgeUrl.searchParams.set('workspaceId'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("knowledgeUrl.searchParams.set('sourceKind'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("knowledgeUrl.searchParams.set('rootUri'"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("window.location.href = '/knowledge'"));
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("var knowledgeUrl = '/knowledge'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("open-knowledge-rag-debug-settings"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/debug/knowledge-rag"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-knowledge-rag-action]"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("useKnowledgeRagFileTreeSelection"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("reindex-source"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("deleteKnowledgeRagSource"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pruneKnowledgeRagRegistry"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setKnowledgeRagSourceFilter"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote:knowledge-rag-source-updated"));
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains(r#"<option value="exact" selected>关键词</option>"#)
);
assert!(SIDEBAR_TREE_RUNTIME_JS
.contains(r#"data-search-switch="knowledge" role="switch" aria-checked="false""#));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("knowledgeRagSourceRelativePath"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("supportsKnowledgeRagSource"));
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("runLocalOcr"));
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("mnote:local-ocr-job-updated"));
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-local-ocr-menu-status"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openPageIndexSettingsPopover();"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openLocalOcrSettingsPopover();"));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("data-page-settings-tab=\"index\""));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("本地索引仅在本地工作区页面可用"));
assert!(
!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("wolai-page-settings-local-index-backlinks")
);
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("wolai-page-settings-local-index-tags"));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("backlinksUrl"));
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("tagsUrl"));
}
#[test]
fn sidebar_filetree_folder_click_toggles_instead_of_navigation_page() {
let folder_branch = SIDEBAR_TREE_RUNTIME_JS
.split("if (fileAction === 'open' && rowKind === 'folder') {")
.nth(1)
.and_then(|rest| rest.split("return;").next())
.expect("filetree folder open branch exists");
assert!(folder_branch.contains("toggleChildren(fileRow"));
assert!(!folder_branch.contains("openNavigationPageForFolder"));
assert!(!folder_branch.contains("window.location.assign"));
}
#[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"
);
assert!(
!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiEnrichOcrContextRefs"),
"Page AI 目标包不应保留已退役 OCR sidecar context enrichment"
);
assert!(
!SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("ocrRootRelativePath"),
"Page AI target runtime 不应再注入旧 OCR sidecar 路径"
);
}
#[test]
fn page_ai_agent_target_picker_contract_is_visible_and_serialized() {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("createSidebarPageAiRenderRuntime"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS
.contains("export function createSidebarPageAiRenderRuntime"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-target-button"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-target-popover"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-target-option"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-target-chip"));
assert!(SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("primaryTargetId"));
assert!(SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("targets: ["));
assert!(SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("policy: {"));
}
#[test]
fn page_ai_uses_backend_acp_session_runtime_store() {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("createSidebarPageAiSessionRuntime"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("createSidebarPageAiSkillRuntime"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("createSidebarPageAiTargetRuntime"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function renderPageAiControls"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function ensurePageAiDrawer"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function renderPageAiConversation"));
assert!(SIDEBAR_PAGE_AI_SKILL_RUNTIME_JS
.contains("export function createSidebarPageAiSkillRuntime"));
assert!(SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS
.contains("export function createSidebarPageAiTargetRuntime"));
assert!(
SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("function currentPageAiOpenEditorsSnapshot")
);
assert!(SIDEBAR_PAGE_AI_TARGET_RUNTIME_JS.contains("mnote.agent_target_package.v1"));
assert!(SIDEBAR_PAGE_AI_SKILL_RUNTIME_JS.contains("function pageAiSkillSourceOptions"));
assert!(SIDEBAR_PAGE_AI_SKILL_RUNTIME_JS.contains("function pageAiSkillPreferenceTable"));
assert!(SIDEBAR_PAGE_AI_SKILL_RUNTIME_JS.contains("ai.agent.reasonix.memory_enabled"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/api/page-ai/sessions?"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("params.set('source', 'acp')"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains(
"var draftSessions = pageAiNormalizeArray(pageUiState.pageAiSessions).filter"
));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS
.contains("pageAiDedupeSessions(backendSessions.concat(draftSessions))"));
assert!(
SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail")
);
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS
.contains("function pageAiDeleteSelectedBackendSessions"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiCheckActiveRun"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/api/page-ai/sessions/"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/active-run?"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-rename"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-delete"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-select"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-delete-selected"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-resume"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-search"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function pageAiRenderThoughtGroup"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-thought-card"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-collapse-card"));
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_PERMISSION_RUNTIME_JS
.contains("data-page-ai-permission-action=\"allow\""));
assert!(SIDEBAR_PAGE_AI_PERMISSION_RUNTIME_JS
.contains("data-page-ai-permission-action=\"deny\""));
assert!(
SIDEBAR_PAGE_AI_PERMISSION_RUNTIME_JS.contains("function pageAiHidePermissionDialog")
);
assert!(SIDEBAR_PAGE_AI_PERMISSION_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_RENDER_RUNTIME_JS.contains("data-page-ai-plan"),
"plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板"
);
assert!(
SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("执行计划 · "),
"plan 面板标题应显示执行计划和步数"
);
}
#[test]
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("local_private"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("local_shared"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("sqlite_control_plane"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("convex_acp_runtime_store"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("本地私有"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("共享会话"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("账号会话"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("云端会话"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("sessionStorage:"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("permissionLevel:"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("shareId:"));
}
#[test]
fn page_ai_acp_runtime_legacy_selector_contract_is_explicit() {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 4"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function ensurePageAiStateFacade"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("createSidebarPageAiPermissionRuntime"));
assert!(SIDEBAR_PAGE_AI_PERMISSION_RUNTIME_JS.contains("function pageAiResolvePermission"));
assert!(SIDEBAR_PAGE_AI_PERMISSION_RUNTIME_JS.contains("resolve-permission"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("createSidebarPageAiProfileRuntime"));
assert!(SIDEBAR_PAGE_AI_PROFILE_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
assert!(SIDEBAR_PAGE_AI_PROFILE_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiClick"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiPointerDown"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("mnote.page_ai.drawer_width"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-resize-handle"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiOpenCitationUrl"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("a[data-page-ai-citation-link=\"true\"]"));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("data-page-ai-citation-link=\"true\""));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("renderPageAiMarkdownTable"));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("wolai-page-ai-markdown-table-wrap"));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("target=\"_blank\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiChange"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function installPageAiDelegates"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("document.addEventListener('visibilitychange'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiCheckActiveRun(sessionId || undefined)"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiCheckAndResumeActiveRun"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiResumeActiveRunJournal"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiTrackStreamingRunEvent"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("void pageAiCheckAndResumeActiveRun().catch"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("mnote.page_ai_active_run_snapshot.v1"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-mnote-page-ai-active-run-last-seq"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/page-ai/runs/"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("afterSeq="));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiCheckAndResumeActiveRun()"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/page-ai/agents/descriptors"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadAgentDescriptors"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-descriptor-field"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function pageAiRenderDescriptorField"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-agent-descriptor-card"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebarPageAi.installPageAiDelegates()"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("sidebarPageAi.handlePageAi"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("closestAction(e.target, '[data-page-ai-action"));
assert!(SIDEBAR_PAGE_AI_SESSION_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_uses_local_folder_events_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/projections/sidebar"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("var resolvedSidebarPayload = sidebarPayload ? (sidebarPayload.result || sidebarPayload) : null"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var renderedPage = false;"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderedPage = await renderPageProjection(resolvedSidebarPayload);"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("refreshFileTreeParent(fileTreeScope)"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
);
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-disabled")
);
assert!(
TREE_LIVE_CONTROLLER_JS.contains("body.getAttribute('data-mnote-source-kind')"),
"根入口由 SSR body 暴露 local_folder 时,tree live 不能误走 retired Convex WS/SSE"
);
assert!(
TREE_LIVE_CONTROLLER_JS.contains("bootstrap.transport === 'local-folder-events'"),
"本地文件夹 bootstrap transport 应直接选择 /api/local-folder/events"
);
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("startLocalFolderWatcher"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("connections.has(key)"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("function pathArrayOf"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("function queueSidebarRefresh"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS
.contains("mnote:local-folder:sidebar-refresh-requested"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS
.contains("data-mnote-local-folder-event-bus-connections"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("tree:local-folder-watch-batch"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("viaEventBus: true"));
assert!(LOCAL_FOLDER_EVENT_BUS_RUNTIME_JS.contains("emitSyntheticWatchBatch"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("__mnoteLocalFolderEventBus"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("synthetic_page_ai_receipt"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteLocalFolderEventBus"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("startLocalFolderWatcher"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("mnote:local-folder:sidebar-refresh-requested"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("event.detail.viaEventBus === true"));
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
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 sidebar_attachment_open_runtime_receives_closest_action_dependency() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function closestAction"));
let injection_start = SIDEBAR_TREE_RUNTIME_JS
.find("const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({")
.expect("attachment runtime injection");
let injection_end = SIDEBAR_TREE_RUNTIME_JS[injection_start..]
.find(" });")
.map(|offset| injection_start + offset)
.expect("attachment runtime injection end");
let injection = &SIDEBAR_TREE_RUNTIME_JS[injection_start..injection_end];
assert!(
injection.contains("closestAction"),
"attachment open runtime 需要显式注入 closestAction,避免首屏点击监听安装时 ReferenceError"
);
assert!(
injection.contains("hydrateEditorAttachmentMeta"),
"attachment open runtime 需要显式注入 hydrateEditorAttachmentMeta,避免上传附件增强时 ReferenceError"
);
assert!(
SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("closestAction = typeof injectedClosestAction === 'function'")
|| SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("dependencies.closestAction"),
"attachment open runtime 应从 dependencies 读取 closestAction,而不是依赖外层闭包"
);
}
#[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("/api/media/upload"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-media-upload-retired"));
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"));
assert!(
LOCAL_UPLOAD_RUNTIME_JS
.contains("var imageUrl = localOpenUrl || fallbackUrl || markdownHref;"),
"图片上传首屏必须使用本地 open URL 渲染,避免相对 href 在 /documents 下命中退役 Convex 兼容路径"
);
assert!(
LOCAL_UPLOAD_RUNTIME_JS.contains("setImage({ src: imageUrl"),
"插入图片时不能直接把 ./asset.png 作为 img.src,否则刷新前会显示破损"
);
}
#[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("activateSidebarFileTreeRow(row, options)")
&& SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function activateSidebarFileTreeRow")
&& SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.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_FILETREE_OPEN_RUNTIME_JS
.contains("var localOfficeUrl = buildLocalOnlyOfficeOpenUrl"));
}
#[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("import.meta.url"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnoteDevHotReloadEnabled()"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("\n installMnoteDevHotReload();\n"));
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("mnote:page-aggregate-synced"));
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("rowsByParent: new Map()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("loadedParents: new Set()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("loadingParents: new Map()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("dirtyParents: new Set()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeExpandedRelativePaths"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("if (fileTreeState.loadingParents.has(key))"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("return fileTreeState.loadingParents.get(key);"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("function markExistingFileTreeChildrenLoaded"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("if (!stale && markExistingFileTreeChildrenLoaded(row, button, options))"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("scheduleRestorePersistedFileTreeExpansionState();"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function patchFileTreeParentChildren"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("return patchFileTreeParentChildren(parentRelativePath, rows);"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var renderedPage = false;"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("renderedPage = await renderPageProjection(resolvedSidebarPayload);"));
let render_start = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.find("function renderFileProjection(projection)")
.expect("renderFileProjection");
let render_end = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[render_start..]
.find("function renderSidebarSnapshot(payload)")
.map(|offset| render_start + offset)
.expect("renderFileProjection end");
let render_body = &SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[render_start..render_end];
assert!(!render_body.contains("tree.innerHTML ="));
assert!(render_body.contains("tree.replaceChildren"));
let patch_branch = render_body
.find("patchFileTreeParentChildren(parentRelativePath, rows)")
.expect("non-root parent patch");
let root_replace = render_body
.find("tree.replaceChildren")
.expect("root replacement");
assert!(
patch_branch < root_replace,
"非 scope parent projection 必须先局部 patch,不能替换整棵 filetree"
);
let load_start = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.find("async function loadFileTreeChildren(row, button, options)")
.expect("lazy children loader");
let optimistic_expand = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[load_start..]
.find("setTreeRowExpanded(row, button, true, options);")
.expect("lazy loading should mark the requested folder expanded before fetch")
+ load_start;
let fetch_start = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[load_start..]
.find("getFileTreeChildren(relativePath)")
.expect("lazy children datasource call")
+ load_start;
assert!(
optimistic_expand < fetch_start,
"慢目录加载期间必须先保存 expanded 状态,否则 refresh/create-page 会把刚点开的文件夹折叠"
);
}
#[test]
fn sidebar_filetree_runtime_discards_stale_generation_results() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("staleParents: new Set()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("requestGeneration: 0"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function beginFileTreeRequest"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function isLatestFileTreeRequest"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("if (!isLatestFileTreeRequest(key, generation))"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeState.staleParents.add(key)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeState.requestGeneration += 1"));
}
#[test]
fn sidebar_runtime_applies_page_projection_with_generation_and_yield() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("pageTreeRequestGeneration: 0"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function beginPageTreeRequest"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function isLatestPageTreeRequest"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("async function renderPageProjection"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("await yieldForPageTreeRender()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("if (!isLatestPageTreeRequest(generation))"));
}
#[test]
fn sidebar_filetree_runtime_refreshes_scoped_page_tree_projection() {
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("sidebarUrl.searchParams.set('parentRelativePath', fileTreeScope)"),
"scoped filetree 入口刷新 PageTree 时必须请求同 scope projection"
);
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("sidebarTreeViewStateFor('pagetree')"),
"SSR 初始 PageTree 也必须触发 pagetree 用户保存态加载"
);
}
#[test]
fn sidebar_page_create_does_not_full_refresh_local_folder_sidebar() {
assert!(
!SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("await refreshLocalFolderSidebarSnapshot();"),
"本地新建页面不能全量刷新 sidebar,否则会折叠/重建其它已展开节点"
);
assert!(
SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("await refreshLocalFolderAfterCommand('create'"),
"本地新建页面应等待局部 parent refresh,而不是全量刷新 sidebar"
);
assert!(
SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("selectSidebarFileTreeDocument(nextDocumentId"),
"本地新建页面仍应只 reveal/select 新节点"
);
}
#[test]
fn sidebar_filetree_runtime_prefers_command_affected_parents() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("function addAffectedParentsFromCommandResult"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("Array.isArray(result && result.affectedParents)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("result && result.execution && result.execution.affectedParents"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("addAffectedParentsFromCommandResult(parents, result)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-filetree-command-refresh-fallback"));
}
#[test]
fn sidebar_filetree_runtime_batches_local_command_refresh() {
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function nextFileTreeOperationBatchId")
);
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("removeFileTreeAssetRow"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("removeFileTreeAssetRow,"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("batchId: batchId"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("tree:local-command-batch-complete"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function queueFileTreeBatchRefresh"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function flushFileTreeBatchRefresh"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-filetree-batch-refresh-pending"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-filetree-batch-refresh-applied"));
}
#[test]
fn sidebar_filetree_runtime_handles_watch_batch_and_structured_error() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("watch_batch"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:local-folder-watch-batch"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree_error"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:error"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function applyLocalFolderWatchBatch"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function watchBatchNeedsPageTreeRefresh")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-local-folder-watch-sidebar-refresh-skipped"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("data-mnote-local-folder-watch-batch-applied"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-tree-live-error-schema"));
}
#[test]
fn sidebar_filetree_runtime_has_view_state_namespace() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var fileTreeViewState = {"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("expandedParents: new Set()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("selectedRowIds: new Set()"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("focusedRowId: ''"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("activeRowId: ''"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var fileTreeState = fileTreeViewState;")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("var fileTreeExpandedRelativePaths = fileTreeViewState.expandedParents;"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
"if (rootUri === fileTreeLazyCacheRootUri && scope === fileTreeState.scope) return;"
));
}
#[test]
fn sidebar_filetree_runtime_hydrates_visible_expanded_rows_on_idle() {
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("function scheduleHydrateVisibleExpandedFileTreeRows"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("function hydrateVisibleExpandedFileTreeRows"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("FILETREE_VISIBLE_EXPANDED_HYDRATE_MAX")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("requestIdleCallback"));
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-filetree-idle-hydrate-applied")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("row.getAttribute('aria-expanded') === 'true'"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("row.getAttribute('data-filetree-children-loaded') !== 'true'"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("loadFileTreeChildren(row, button, { persist: false, idleHydrate: true })"));
let restore_start = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.find("function restorePersistedFileTreeExpansionState")
.expect("restorePersistedFileTreeExpansionState");
let restore_end = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[restore_start..]
.find("function installTreeLiveApplyEventListeners")
.map(|offset| restore_start + offset)
.expect("restore function end");
let restore_body = &SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[restore_start..restore_end];
assert!(restore_body.contains("scheduleHydrateVisibleExpandedFileTreeRows('restore')"));
assert!(
!restore_body.contains("then(function(loaded)"),
"恢复 view-state 不能递归拉取历史 expanded path"
);
}
#[test]
fn sidebar_filetree_runtime_reprojects_selection_focus_after_patch() {
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function rememberFileTreeSelectionState")
);
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function reprojectFileTreeSelectionState")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeViewState.selectedRowIds.add(rowId)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
"row.setAttribute('data-selected', String(fileTreeViewState.selectedRowIds.has(rowId)))"
));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
"row.setAttribute('data-focused', String(rowId === fileTreeViewState.focusedRowId))"
));
}
#[test]
fn sidebar_filetree_runtime_can_reveal_unloaded_resource_path() {
assert!(
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("async function revealFileTreeResource")
);
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("function fileTreeParentChainForRelativePath"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("await getFileTreeChildren(parentRelativePath)"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
.contains("fileTreeViewState.selectedRowIds = new Set([targetRowId])"));
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("revealFileTreeResource,"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("const revealFileTreeResource = (...args) => sidebarTreeLiveApply.revealFileTreeResource(...args);"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("revealFileTreeResource({"));
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("decodeLocalEncodedPath(id.slice('local-md:'.length))"));
assert!(
SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function localMarkdownBundleParentPath")
);
assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.contains("visibleFileTreeRowByRelativePath(bundleParentPath)"));
}
#[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_bulk_delete_refreshes_local_folder_after_success() {
let bulk_delete_start = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS
.find("async function deleteSelectedSidebarFileTreeRows")
.expect("bulk delete function");
let bulk_delete_end = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[bulk_delete_start..]
.find("return {")
.map(|offset| bulk_delete_start + offset)
.expect("bulk delete function end");
let bulk_delete = &SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[bulk_delete_start..bulk_delete_end];
assert!(bulk_delete.contains(
"if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();"
));
assert!(!bulk_delete.contains(
"if (currentSourceKind() !== 'local_folder') void refreshLocalFolderSidebarSnapshot();"
));
}
#[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("moveSidebarFileTreeRows(runtimeState.sidebarFileTreeClipboard.rowIds"));
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 && !editor.dirty')"
),
"Delete/Backspace 快捷键应评估 workspace.readonly 与 editor.dirty 上下文"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains(
"evaluateSidebarFileTreeWhen(renameCtx, '!workspace.readonly && !editor.dirty')"
),
"F2 快捷键应与菜单重命名共用 command context"
);
}
#[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_runtime_does_not_keep_retired_table_engine_branches() {
let retired_table_engine = ["lucky", "sheet"].concat();
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains(&retired_table_engine));
let retired_api = ["/api/", "lucky"].concat();
let retired_constructor = ["create", "Luck"].concat();
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains(&retired_constructor));
assert!(!SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains(&retired_api));
}
#[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("var requestedOfficeMode = forceEditMode ? 'edit' : 'view';"));
assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains(
"var requestedOfficeMode = forceNewWindow || forceEditMode ? 'edit' : 'view';"
));
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_FILETREE_OPEN_RUNTIME_JS.contains("function buildPdfPreviewOpenUrl"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("buildPdfPreviewOpenUrl"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
.contains("var localOpenUrl = isPdfAttachmentFileName(localFileName) ? buildPdfPreviewOpenUrl(localFileUrl, localFileName) : localFileUrl;"));
let open_pdf_body = js_function_body(
SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS,
"openPdfEditorAttachment",
);
assert!(open_pdf_body.contains("buildPdfPreviewOpenUrl(resolved.url, title)"));
assert!(
!open_pdf_body.contains("window.open(resolved.url, '_blank', 'noopener,noreferrer');")
);
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("/api/media/sign?assetId="));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains(
"var localDownloadUrl = buildLocalFileOpenUrlForRoot(localFilePath, detail.localRootUri || currentRootUri(), 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({"));
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("data-mnote-media-sign-retired"));
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("enhanceEditorAttachmentLinks();"));
}
#[test]
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree-live-sse"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree-live-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("__mnoteLocalFolderEventBus"));
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"));
}
}