//! MNOTE 通用页面布局组件(Wolai / Notion 风格) use crate::ssr::pages::admin::AdminAccessPolicyPanel; use leptos::prelude::*; const SIDEBAR_TREE_JS: &str = r##" (function(){ if (window.__mnoteSidebarTreeRuntimeStarted) return; // sidebar-tree-runtime.js module has NOT loaded yet. // This inline script provides a minimal bootstrap until the module arrives. // Module script is loaded via type="module" before this inline block. window.__mnoteSidebarTreeRuntimeLoading = true; var _checkInterval = setInterval(function() { if (window.__mnoteSidebarTreeRuntimeStarted) { clearInterval(_checkInterval); window.__mnoteSidebarTreeRuntimeLoading = false; } }, 100); // If module hasn't loaded after 5 seconds, stop polling setTimeout(function() { if (_checkInterval) clearInterval(_checkInterval); }, 5000); })(); "##; 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! { /// ///
...
///
/// } /// ``` #[component] pub fn PageLayout( children: Children, current_nav: &'static str, /// 侧栏页面树 HTML(可选),由路由 handler 渲染 #[prop(optional)] sidebar_tree_html: Option, /// 工作区名称(可选),显示在侧栏顶部 #[prop(optional)] workspace_name: Option, /// workspace shell 侧栏 sections HTML(可选),由 projection 渲染 #[prop(optional)] workspace_sidebar_html: Option, /// 顶栏当前页面标题(可选) #[prop(optional)] topbar_title: Option, /// 是否显示管理员授权能力 #[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(); let admin_access_policy_template = crate::ssr::render_view(leptos::view! { }); let user_access_policy_template = crate::ssr::render_view(leptos::view! { }); view! {
{children()}
} } #[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"); const SIDEBAR_PAGE_AI_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-page-ai-runtime.js"); const SIDEBAR_PAGE_SETTINGS_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-page-settings-runtime.js"); const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str = include_str!("../../../browser/filetree-context-menu-runtime.js"); const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js"); const FILETREE_KEYBOARD_RUNTIME_JS: &str = include_str!("../../../browser/filetree-keyboard-runtime.js"); const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js"); const FILETREE_SELECTION_RUNTIME_JS: &str = include_str!("../../../browser/filetree-selection-runtime.js"); const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js"); fn js_function_body(source: &str, name: &str) -> String { let marker = format!("function {name}("); let start = source.find(&marker).expect("js function exists"); let rest = &source[start..]; let brace = rest.find('{').expect("js function body starts"); let mut depth = 0usize; let mut end = None; for (offset, ch) in rest[brace..].char_indices() { if ch == '{' { depth += 1; } else if ch == '}' { depth -= 1; if depth == 0 { end = Some(brace + offset + 1); break; } } } rest[..end.expect("js function body ends")].to_string() } #[test] fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnoteNavigationInFlight")); assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("data-mnote-navigation-pending")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY")); assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS .contains("data-global-option-checkbox=\"showHeadingNumbers\"")); assert!(SIDEBAR_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("treeView")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("data-mnote-tree-local-command-applied', 'create")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("data-mnote-tree-local-command-applied', 'remove")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("data-mnote-tree-local-command-applied', 'rename")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("data-mnote-tree-local-command-applied', 'move")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function sortOrderFromDelta(data)")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data.sortOrder ?? data.sort_order")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function insertTreeNodeAtSortOrder")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:assets-changed")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyAssetsChangedToFileTree")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installMindmapAssetFetchObserver")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("parseMindmapApiTarget")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-assets-local-applied")); assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("application/x-mnote-page-tree-node")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("dragstart")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("drop")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-drop-feedback")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("action: 'move'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebar-file-tree-root")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.open")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.asset.open")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openConvexAssetFromFileTree")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildMindmapOpenPath")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isMindmapAssetDetail")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("^思维导图")); assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openMindmapAssetInDocumentShell")); assert!( SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("data-mnote-last-mindmap-asset-open-mode") ); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("mindmap-object-shell")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("navigateToMindmapObject(documentId, assetId")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("__mnoteDocumentPaneRuntime?.openPrimaryMindmap")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shortMindmapFileName")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("assetType: assetType || null")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-object-identity")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("readFileTreeObjectIdentity")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("objectIdentity: objectIdentity")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("workspaceId: resolveWorkspaceId(fileRow)")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/media/sign?assetId=")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function localFilePathFromAssetId")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/local-folder/files/open")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("local-file:")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fetchCurrentOnlyOfficeUserId")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/auth/whoami")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildOnlyOfficeOpenUrl")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("target.searchParams.set('userId'")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("window.open(officeUrl,")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.internal-drop")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.external-drop")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("beginFileTreeInlineRename")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("tree-rename-input")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebarFileTreeClipboard")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pasteSidebarFileTreeClipboard")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function recentLocalRootsStorageKey")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("data-mnote-actor-id")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("requestOpenLocalFolder")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("autoOpenRecentLocalRootOnHome")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("sourceKind', 'local_folder")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("switchToCloudWorkspace")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-switch-cloud-workspace")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-recent-local-root")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-local-folder-authorized-roots")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("mnote-local-folder-authorized-root")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("已授权文件夹")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("fetch('/api/user/access-policy'")); } #[test] fn 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! {
"正文"
}); 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""#)); } #[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! {
"正文"
}); 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" ); } #[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("/api/navigation/recent")); 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 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" ); } #[test] fn page_ai_uses_backend_acp_session_runtime_store() { assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessions")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/hermes/client/sessions?")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("params.set('source', 'acp')")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSearchBackendSessions")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-rename")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-delete")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-resume")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-search")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiUsageSummary")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("usage.updated")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("thought.delta")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permission.requested")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\"")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\"")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiHidePermissionDialog")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("if (!message.resolved)")); assert!( !SIDEBAR_PAGE_AI_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"), "已决 ACP permission 事件不能继续展示假审批按钮" ); assert!( SIDEBAR_PAGE_AI_RUNTIME_JS.contains("session.info.updated"), "ACP SessionInfoUpdate 事件应通过 session.info.updated SSE 转发到前端" ); assert!( SIDEBAR_PAGE_AI_RUNTIME_JS.contains("plan.updated"), "ACP PlanUpdate 事件应通过 plan.updated SSE 转发到前端" ); assert!( SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-plan"), "plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板" ); assert!( SIDEBAR_PAGE_AI_RUNTIME_JS.contains("执行计划 · "), "plan 面板标题应显示执行计划和步数" ); } #[test] fn page_ai_session_ui_labels_local_shared_and_cloud_storage() { assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSessionStorageLabel")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_private")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_shared")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("convex_acp_runtime_store")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("本地私有")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("共享会话")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("云端会话")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sessionStorage:")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permissionLevel:")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("shareId:")); } #[test] fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() { assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("return ['reasonix', 'hermes']")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'")); assert!(SIDEBAR_PAGE_AI_RUNTIME_JS .contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'")); assert!(!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("默认 (Hermes HTTP)")); } #[test] fn sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector() { assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot")); assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta")); assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("usePreferredSidebarSnapshot")); assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("preferredSidebarSnapshot")); assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("liveSidebarTitle")); } #[test] fn page_layout_sidebar_toggle_is_wired_to_shell_state() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function toggleWorkspaceSidebar")); assert!(SIDEBAR_TREE_RUNTIME_JS .contains("sidebarShellRuntimeFunction('toggleWorkspaceSidebar')")); assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function toggleWorkspaceSidebar")); assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("data-mnote-sidebar-collapsed")); assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function installWorkspaceSidebarResizer")); assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("function restoreSidebarTreeTab")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-mnote-action=\"toggle-sidebar\"]")); } #[test] fn sidebar_workspace_source_switch_remembers_real_cloud_workspace_before_local_folder() { assert!( !SIDEBAR_TREE_RUNTIME_JS.contains("rememberCloudWorkspaceId(resolveWorkspaceId(document.body))"), "进入本地文件夹前不能用 document.body 推导 workspaceId;它会在无 workspaceId URL 时回退成 default" ); assert!( SIDEBAR_WORKSPACE_RUNTIME_JS.contains("rememberCurrentCloudWorkspaceId()"), "进入本地文件夹前应从 URL 或 DOM 子树读取真实 workspaceId" ); assert!( SIDEBAR_WORKSPACE_RUNTIME_JS.contains("normalized.indexOf('local-ws:') === 0"), "本地文件夹 workspaceId 不能写入 lastCloudWorkspaceId" ); assert!( SIDEBAR_WORKSPACE_RUNTIME_JS.contains("stored.trim().indexOf('local-ws:') !== 0"), "切回工作区时不能复用 local-ws:* 作为旧云 workspace" ); assert!( !SIDEBAR_TREE_RUNTIME_JS .contains("targetUrl.searchParams.set('sourceKind', 'convex_workspace')"), "默认来源菜单不能跳到已退出 dev 主链的 convex_workspace" ); } #[test] fn sidebar_tree_runtime_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("void 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!(!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("function insertUploadedAssetIntoEditor")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function dispatchUploadedEditorChange")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function persistUploadedEditorChange")); assert!( LOCAL_UPLOAD_RUNTIME_JS.contains("function persistLocalFolderSelfChangeSuppression") ); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("editorSource: 'local-upload-runtime'")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("mnote:leptos-tiptap-spike:change")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFileToMediaAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFilesWithResolvedTarget")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-last-upload-inserted")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFileToMediaAsset: uploadFileToMediaAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot")); } #[test] fn sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeRuntimeFunction")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowDocumentId')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowAssetId')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowLocalRelativePath')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeAssetDownloadDetail')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeChildCount')")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('revealFileTreeRow')")); assert!( SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('revealFileTreeAssetRow')") ); assert!( SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('appendUploadedAssetRow')") ); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeUploadTargetFallback')")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight')")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight')")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('readProjection')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('readSidebarDataset')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('readDatasetProjection')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('projectionItems')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('hasProjectionItems')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('nodeIdOf')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('rowIdOf')")); assert!( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')") ); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('groupRowsByParent')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')")); let document_id_body = js_function_body(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, "fileTreeRowDocumentId"); assert!( document_id_body.contains("data-document-id") && document_id_body.contains("data-doc-id"), "inline fallback 必须保留旧 data attribute 解析" ); let replace_body = js_function_body( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "replaceSidebarTreeFromDocument", ); assert!( replace_body.contains("current.innerHTML = next.innerHTML") && replace_body.contains("current.setAttribute(attr.name, attr.value)"), "inline fallback 必须保留 DOM copy 行为" ); let apply_body = js_function_body( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "applyLocalFolderSidebarSnapshot", ); assert!( apply_body.contains("options.pageRootId || 'sidebar-tree-root'") && apply_body.contains("options.fileRootId || 'sidebar-file-tree-root'") && apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, pageRootId)") && apply_body.contains("replaceSidebarTreeFromDocument(nextDocument, fileRootId)"), "inline fallback 必须保留 sidebar/page tree DOM 应用行为" ); let watch_applied_body = js_function_body( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "markLocalFolderWatchApplied", ); assert!( watch_applied_body.contains("data-mnote-local-folder-watch-applied") && watch_applied_body.contains("String(value || 'projection')"), "inline fallback 必须保留 local folder watch projection 标记" ); } #[test] fn filetree_runtime_contains_row_accessor_helpers() { assert!(FILETREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowDocumentId")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowAssetId")); assert!(FILETREE_RUNTIME_JS.contains("function decodeLocalEncodedPath")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowLocalRelativePath")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowLocalUploadTargetRelativePath")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeAssetDownloadDetail")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowsByRowIds")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeChildCount")); assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeRow")); assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeAssetRow")); assert!(FILETREE_RUNTIME_JS.contains("function appendUploadedAssetRow")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetFallback")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeRowsForUploadPreflight")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeDocumentParentsForPreflight")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeTargetChildrenForPreflight")); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeDropPreflightRows")); assert!( FILETREE_RUNTIME_JS.contains("function fileTreeDocumentWorkspacesForUploadPreflight") ); assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetPreflightBody")); assert!(FILETREE_RUNTIME_JS.contains("function shortMindmapFileName")); assert!(FILETREE_RUNTIME_JS.contains("function parseMindmapApiTarget")); assert!(FILETREE_RUNTIME_JS.contains("function requestMethod")); assert!(FILETREE_RUNTIME_JS.contains("function requestBodyHasMindmapCreateOnly")); assert!(FILETREE_RUNTIME_JS.contains("function readProjection")); assert!(FILETREE_RUNTIME_JS.contains("function readSidebarDataset")); assert!(FILETREE_RUNTIME_JS.contains("function readDatasetProjection")); assert!(FILETREE_RUNTIME_JS.contains("function projectionItems")); assert!(FILETREE_RUNTIME_JS.contains("function hasProjectionItems")); assert!(FILETREE_RUNTIME_JS.contains("function nodeIdOf")); assert!(FILETREE_RUNTIME_JS.contains("function rowIdOf")); assert!(FILETREE_RUNTIME_JS.contains("function parentIdOf")); assert!(FILETREE_RUNTIME_JS.contains("function titleOf")); assert!(FILETREE_RUNTIME_JS.contains("function fileWorkspaceRelativePath")); assert!(FILETREE_RUNTIME_JS.contains("function groupRowsByParent")); assert!(FILETREE_RUNTIME_JS.contains("function replaceSidebarTreeFromDocument")); assert!(FILETREE_RUNTIME_JS.contains("function applyLocalFolderSidebarSnapshot")); assert!(FILETREE_RUNTIME_JS.contains("function markLocalFolderWatchApplied")); assert!(FILETREE_RUNTIME_JS.contains("appendUploadedAssetRow: appendUploadedAssetRow")); assert!(FILETREE_RUNTIME_JS.contains("groupRowsByParent: groupRowsByParent")); assert!(FILETREE_RUNTIME_JS .contains("replaceSidebarTreeFromDocument: replaceSidebarTreeFromDocument")); assert!(FILETREE_RUNTIME_JS .contains("applyLocalFolderSidebarSnapshot: applyLocalFolderSidebarSnapshot")); assert!(FILETREE_RUNTIME_JS .contains("markLocalFolderWatchApplied: markLocalFolderWatchApplied")); assert!(FILETREE_RUNTIME_JS .contains("fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight")); assert!(FILETREE_RUNTIME_JS .contains("fileTreeDocumentParentsForPreflight: fileTreeDocumentParentsForPreflight")); assert!(FILETREE_RUNTIME_JS .contains("fileTreeTargetChildrenForPreflight: fileTreeTargetChildrenForPreflight")); assert!( FILETREE_RUNTIME_JS.contains("fileTreeDropPreflightRows: fileTreeDropPreflightRows") ); assert!(FILETREE_RUNTIME_JS.contains( "fileTreeDocumentWorkspacesForUploadPreflight: fileTreeDocumentWorkspacesForUploadPreflight" )); assert!(FILETREE_RUNTIME_JS .contains("fileTreeUploadTargetPreflightBody: fileTreeUploadTargetPreflightBody")); assert!(FILETREE_RUNTIME_JS.contains("shortMindmapFileName: shortMindmapFileName")); assert!(FILETREE_RUNTIME_JS.contains("parseMindmapApiTarget: parseMindmapApiTarget")); assert!(FILETREE_RUNTIME_JS.contains("requestMethod: requestMethod")); assert!(FILETREE_RUNTIME_JS .contains("requestBodyHasMindmapCreateOnly: requestBodyHasMindmapCreateOnly")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight')")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody')")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS .contains("fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('shortMindmapFileName')")); assert!( SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('parseMindmapApiTarget')") ); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('requestMethod')")); assert!(SIDEBAR_TREE_RUNTIME_JS .contains("fileTreeRuntimeFunction('requestBodyHasMindmapCreateOnly')")); assert!(FILETREE_RUNTIME_JS .contains("uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')")); assert!(FILETREE_RUNTIME_JS .contains("uploadIntent: String(detail.uploadIntent || 'editor.markdown.attach')")); } #[test] fn sidebar_filetree_selection_runtime_helpers_are_externalized_with_inline_fallback() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeSelectionRuntimeFunction")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('visibleFileTreeRows')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows')")); let select_body = js_function_body( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, "selectSidebarFileTreeRow", ); assert!( select_body.contains("shiftKey && sidebarFileTreeSelection.anchorRowId") && select_body.contains("ctrlKey"), "inline fallback 必须保留 shift range 与 ctrl/meta toggle" ); let select_document_body = js_function_body( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, "selectSidebarFileTreeDocument", ); assert!( select_document_body.contains("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_TREE_RUNTIME_JS.contains("isLocalAsset ? onlyOfficeUrl")); } #[test] fn sidebar_local_folder_authorized_root_normalizes_plain_path_root_uri() { assert!(SIDEBAR_WORKSPACE_RUNTIME_JS .contains("function normalizeGrantedLocalFolderRootUri(grant)")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS .contains("if (rootPath) return pathToFileRootUri(rootPath);")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS .contains("if (rootUri) return pathToFileRootUri(rootUri);")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS .contains("var rootUri = normalizeGrantedLocalFolderRootUri(grant);")); assert!( SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function isDefaultWorkspaceAutoGrant(grant)") ); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS .contains("if (isDefaultWorkspaceAutoGrant(grant)) return;")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function recentLocalRootLabel(rootUri)")); assert!(SIDEBAR_WORKSPACE_RUNTIME_JS .contains("button.textContent = recentLocalRootLabel(rootUri);")); assert!(!SIDEBAR_TREE_RUNTIME_JS .contains("button.textContent = rootUri.replace(/^file:\\/\\//, '')")); } #[test] fn sidebar_runtime_supports_resizable_filetree_and_hover_titles() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function installWorkspaceSidebarResizer()")); assert!(SIDEBAR_TREE_RUNTIME_JS .contains("sidebarShellRuntimeFunction('installWorkspaceSidebarResizer')")); assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("data-mnote-sidebar-resizer")); assert!(SIDEBAR_SHELL_RUNTIME_JS.contains("mnote.workspace.sidebarWidth.v1")); assert!( SIDEBAR_SHELL_RUNTIME_JS.contains("shell.style.setProperty('--mnote-sidebar-width'") ); assert!(FILETREE_DND_RUNTIME_JS.contains("window.__mnoteFileTreeDndRuntime")); assert!(FILETREE_KEYBOARD_RUNTIME_JS.contains("window.__mnoteFileTreeKeyboardRuntime")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();")); assert!( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#) ); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains(r#"class="tree-link-title" title="' + escapeHtml(title) + '""#)); } #[test] fn sidebar_runtime_opens_local_pdf_assets_in_active_resource_tab_by_default() { assert!(SIDEBAR_TREE_RUNTIME_JS .contains("function shouldOpenLocalResourceInNewWindow(fileName)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("return false;")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("var shouldOpenInNewWindow = forceNewWindow || shouldOpenLocalResourceInNewWindow(localFileName);")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("if (!shouldOpenInNewWindow && await openLocalResourceInActiveTab({")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains( "openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab'" )); } #[test] fn sidebar_tree_runtime_contains_dev_hot_reload_client() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installMnoteDevHotReload")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/dev/hot-reload")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-dev-hot-reload")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.clearInterval(timer)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote:primary-document-activated")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("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 (markExistingFileTreeChildrenLoaded(row, button))")); 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("void 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)") .expect("lazy children loader"); let optimistic_expand = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS[load_start..] .find("setTreeRowExpanded(row, button, true);") .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("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_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_blocks_readonly_paste_and_drop_with_action_status() { assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("function blockReadonlyFileTreeAction")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("data-mnote-filetree-readonly-blocked")); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("data-mnote-filetree-readonly-message")); assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'") ); assert!(SIDEBAR_FILETREE_UPLOAD_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight")); assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'") ); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('drop'")); } #[test] fn sidebar_filetree_command_context_helper_functions_exist() { assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function buildSidebarFileTreeContext") ); assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen") ); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'workspace.sourceKind'")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'workspace.readonly'")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.focusKind'")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.selectionCount'")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.selectionResourceKind'")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-workspace-readonly")); } #[test] fn filetree_context_menu_runtime_contains_command_context_helpers() { assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function buildSidebarFileTreeContext")); assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen")); assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("function tokenizeWhenExpression")); assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime")); assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'workspace.sourceKind'")); assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'workspace.readonly'")); assert!(FILETREE_CONTEXT_MENU_RUNTIME_JS.contains("'tree.selectionCount'")); } #[test] fn sidebar_filetree_context_menu_items_have_when_for_readonly() { // FileTree 分支的写操作必须带 when: '!workspace.readonly'。 assert!( SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("when: '!workspace.readonly'"), "上下文菜单写操作应携带 when: '!workspace.readonly'" ); // Verify at least one delete-trash item has the when expression let delete_trash_items = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .match_indices("'delete-trash'") .count(); assert!(delete_trash_items > 0, "应至少有一个 delete-trash 菜单项"); } #[test] fn sidebar_filetree_context_evaluator_uses_readonly_and_selection_count_for_delete_key() { assert!( SIDEBAR_TREE_RUNTIME_JS.contains( "evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly && !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)); } #[test] fn sidebar_filetree_asset_open_uses_owner_document_id() { assert!( SIDEBAR_TREE_RUNTIME_JS.contains( "var ownerDocumentId = fileRow.getAttribute('data-owner-document-id') || documentId;" ), "打开资源行时必须优先使用资源 owner document,不能用当前页面 documentId" ); assert!( SIDEBAR_TREE_RUNTIME_JS.contains("documentId: ownerDocumentId || null"), "tree.asset.open detail 应携带资源 owner document" ); assert!( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"), "SSR filetree 行应输出 data-owner-document-id" ); assert!( SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"), "local_folder bundle 资源应从 local-file 路径推导 owner markdown document" ); } #[test] fn sidebar_tree_runtime_keeps_pdf_and_code_assets_out_of_onlyoffice() { assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function inferOnlyOfficeFileType")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("isNonOfficeAttachmentName(name, ext)")); assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("if (ext === 'pdf') return ext;")); assert!(!SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("mt.indexOf('pdf') >= 0")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote-uploaded-attachment-pdf")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote-uploaded-attachment-code")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'toml'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'json'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'yaml'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'md'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'vue'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'svelte'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'proto'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'dockerfile'")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'.gitignore'")); } #[test] fn sidebar_tree_runtime_opens_office_assets_through_resource_shell() { assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function buildOnlyOfficeOpenPath")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains( "return '/office/' + encodeURIComponent(input.documentId) + '/' + encodeURIComponent(input.assetId)" )); assert!( SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("return '/onlyoffice?' + params.toString();") ); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("target.searchParams.set('mode', input.mode || 'view');")); assert!( SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("params.set('mode', input.mode || 'view');") ); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains( "{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }" )); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("{ action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("{ action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("function withOfficeEditModeGuard(callback)")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("data-mnote-last-office-edit-mode-requested")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("function openEditorAttachmentEditTab(detail)")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("void openEditorAttachmentEditTab(detail);")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("openLocalOfficeFileInActiveTab(detail, 'edit')")); assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("openEditorAttachmentNewWindow(detail, 'edit')")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("url.searchParams.set('mode', requestedMode);")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("mode: requestedMode")); } #[test] fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() { assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openPdfEditorAttachment")); assert!(SIDEBAR_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("var localFilePath = localFilePathFromAssetId(assetId)")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("type: 'codeBlock'")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attrs: { language: language }")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("if (isPdfAttachmentFileName(detail.fileName))")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("if (isCodeAttachmentFileName(detail.fileName))")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({")); let attachment_class_index = SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .find("attachmentClassForFileName(fileName).split") .expect("editor attachment links apply type-specific classes"); let local_refresh_index = SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .find("if (localFilePath && !isOnlyOfficeAttachmentHref(href))") .expect("local file links refresh existence after enhancement"); assert!(attachment_class_index < local_refresh_index); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("'mnote:leptos-tiptap-spike:ready'")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("window.setTimeout(function()")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("[120, 500, 1200, 2500]")); assert!( SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attachmentInitialEnhanceAttempts >= 120") ); } #[test] fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() { assert!(TREE_LIVE_CONTROLLER_JS.contains("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("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")); } }