2026-04-29 12:24:44 +08:00
|
|
|
|
//! MNOTE 通用页面布局组件(Wolai / Notion 风格)
|
|
|
|
|
|
|
2026-05-22 17:45:22 +08:00
|
|
|
|
use crate::ssr::pages::admin::AdminAccessPolicyPanel;
|
2026-04-29 12:24:44 +08:00
|
|
|
|
use leptos::prelude::*;
|
|
|
|
|
|
|
|
|
|
|
|
const SIDEBAR_TREE_JS: &str = r##"
|
|
|
|
|
|
(function(){
|
2026-04-30 05:46:36 +08:00
|
|
|
|
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
2026-05-25 17:36:17 +08:00
|
|
|
|
// 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);
|
2026-04-30 05:46:36 +08:00
|
|
|
|
})();
|
2026-05-25 17:36:17 +08:00
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
"##;
|
|
|
|
|
|
|
2026-04-29 12:24:44 +08:00
|
|
|
|
/// 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>,
|
2026-04-29 14:36:24 +08:00
|
|
|
|
/// 顶栏当前页面标题(可选)
|
|
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
topbar_title: Option<String>,
|
2026-05-22 17:45:22 +08:00
|
|
|
|
/// 是否显示管理员授权能力
|
2026-05-19 08:07:17 +08:00
|
|
|
|
#[prop(optional)]
|
|
|
|
|
|
show_admin_access_policy: bool,
|
2026-05-20 10:43:38 +08:00
|
|
|
|
/// 是否启用树实时流
|
|
|
|
|
|
#[prop(optional, default = true)]
|
|
|
|
|
|
enable_tree_live: bool,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
) -> impl IntoView {
|
2026-05-22 17:45:22 +08:00
|
|
|
|
let _ = show_admin_access_policy;
|
2026-04-29 12:24:44 +08:00
|
|
|
|
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());
|
2026-04-29 14:36:24 +08:00
|
|
|
|
let topbar_title = topbar_title
|
|
|
|
|
|
.map(|value| value.trim().to_string())
|
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
|
.unwrap_or_else(|| "个人".to_string());
|
2026-04-29 12:24:44 +08:00
|
|
|
|
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(
|
2026-04-29 14:36:24 +08:00
|
|
|
|
&dataset, "default", None, &ws_name,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
);
|
|
|
|
|
|
crate::workspace_shell::render_workspace_shell_sidebar_html(
|
|
|
|
|
|
&projection,
|
|
|
|
|
|
Some(sidebar_tree_html.as_str()),
|
2026-04-29 14:36:24 +08:00
|
|
|
|
None,
|
2026-04-29 12:24:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
});
|
2026-04-30 05:46:36 +08:00
|
|
|
|
let tree_live_bootstrap = serde_json::json!({
|
|
|
|
|
|
"schema": "mnote.tree_live_bootstrap.v1",
|
2026-05-20 10:43:38 +08:00
|
|
|
|
"disabled": !enable_tree_live,
|
|
|
|
|
|
"transport": if enable_tree_live { "convex-command-log-ws" } else { "disabled" },
|
2026-04-30 05:46:36 +08:00
|
|
|
|
"workspaceId": null,
|
|
|
|
|
|
"rootIds": [],
|
|
|
|
|
|
"initialRevision": null,
|
|
|
|
|
|
"endpoint": "/api/tree/events",
|
2026-05-17 12:58:27 +08:00
|
|
|
|
"wsEndpoint": "/api/realtime/ws",
|
2026-04-30 05:46:36 +08:00
|
|
|
|
"views": ["page-tree", "file-tree"]
|
|
|
|
|
|
})
|
|
|
|
|
|
.to_string();
|
2026-05-22 17:45:22 +08:00
|
|
|
|
let admin_access_policy_template = crate::ssr::render_view(leptos::view! {
|
|
|
|
|
|
<AdminAccessPolicyPanel workspace_name={ws_name.clone()} is_admin=true boot_script=false />
|
|
|
|
|
|
});
|
|
|
|
|
|
let user_access_policy_template = crate::ssr::render_view(leptos::view! {
|
|
|
|
|
|
<AdminAccessPolicyPanel workspace_name={ws_name.clone()} is_admin=false boot_script=false />
|
|
|
|
|
|
});
|
2026-04-29 12:24:44 +08:00
|
|
|
|
|
|
|
|
|
|
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>
|
2026-05-09 19:05:06 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="mnote-workspace-source-trigger"
|
|
|
|
|
|
data-testid="mnote-workspace-source-trigger"
|
|
|
|
|
|
data-mnote-action="open-workspace-source-menu"
|
|
|
|
|
|
aria-haspopup="menu"
|
|
|
|
|
|
aria-expanded="false"
|
2026-05-24 02:23:48 +08:00
|
|
|
|
title="切换我的空间或本地文件夹"
|
2026-05-09 19:05:06 +08:00
|
|
|
|
>
|
|
|
|
|
|
<span class="sidebar-workspace-name">{ws_name.clone()}</span>
|
|
|
|
|
|
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
|
|
|
|
|
|
</button>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</div>
|
2026-05-21 23:53:39 +08:00
|
|
|
|
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作" data-testid="wolai-sidebar-quick-actions">
|
2026-04-30 16:18:54 +08:00
|
|
|
|
<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>
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
|
|
|
|
|
|
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
|
|
|
|
|
|
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
|
|
|
|
|
|
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
|
2026-05-08 00:41:03 +08:00
|
|
|
|
<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>
|
2026-05-21 23:53:39 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
title="更多"
|
|
|
|
|
|
aria-label="更多"
|
|
|
|
|
|
data-testid="mnote-account-menu-trigger"
|
|
|
|
|
|
data-mnote-action="open-account-menu"
|
|
|
|
|
|
aria-haspopup="menu"
|
|
|
|
|
|
aria-expanded="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span>
|
|
|
|
|
|
</button>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</nav>
|
|
|
|
|
|
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
2026-04-30 05:46:36 +08:00
|
|
|
|
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
2026-05-22 17:45:22 +08:00
|
|
|
|
<div hidden data-testid="mnote-admin-access-policy-template-admin" inner_html={admin_access_policy_template}></div>
|
|
|
|
|
|
<div hidden data-testid="mnote-admin-access-policy-template-user" inner_html={user_access_policy_template}></div>
|
|
|
|
|
|
<script inner_html={crate::ssr::pages::admin::ADMIN_POLICY_SCRIPT.to_string()}></script>
|
2026-05-24 21:03:33 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/resource-open-runtime.js"></script>
|
2026-05-24 22:42:53 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/local-upload-runtime.js"></script>
|
2026-05-25 00:22:08 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/filetree-runtime.js"></script>
|
2026-05-25 00:37:47 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/filetree-selection-runtime.js"></script>
|
2026-05-25 00:22:08 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/filetree-context-menu-runtime.js"></script>
|
2026-05-25 17:36:17 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/filetree-dnd-runtime.js"></script>
|
|
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/filetree-keyboard-runtime.js"></script>
|
2026-05-25 22:53:15 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/sidebar-shell-runtime.js"></script>
|
2026-05-25 17:36:17 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/sidebar-tree-runtime.js"></script>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
2026-05-24 21:35:21 +08:00
|
|
|
|
<script type="module" src="/api/mnote-browser-runtime/tree-live-controller.js"></script>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</aside>
|
2026-05-23 23:38:42 +08:00
|
|
|
|
<div class="mnote-sidebar-resizer" data-mnote-sidebar-resizer="true" role="separator" aria-orientation="vertical" aria-label="调整侧栏宽度"></div>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
<div class="mnote-main">
|
|
|
|
|
|
<header class="wolai-topbar" data-testid="wolai-topbar">
|
2026-04-29 16:23:49 +08:00
|
|
|
|
<div class="wolai-topbar-left">
|
2026-05-18 17:01:35 +08:00
|
|
|
|
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏" aria-pressed="false" data-mnote-action="toggle-sidebar" data-testid="wolai-sidebar-toggle"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
2026-04-29 16:23:49 +08:00
|
|
|
|
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
|
|
|
|
|
|
<span class="wolai-breadcrumb-separator" aria-hidden="true">"›"</span>
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<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>
|
2026-04-29 16:23:49 +08:00
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
<div class="wolai-topbar-actions" aria-label="页面操作">
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
|
2026-04-30 18:36:50 +08:00
|
|
|
|
<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>
|
2026-05-06 21:44:20 +08:00
|
|
|
|
<button type="button" class="wolai-icon-button" title="页面历史" aria-label="历史" data-mnote-action="open-page-history"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
|
|
|
|
|
|
<button type="button" class="wolai-icon-button" title="页面选项和全局选项" aria-label="更多" data-testid="wolai-page-settings-trigger" data-mnote-action="open-page-settings"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<article class="mnote-content">
|
|
|
|
|
|
{children()}
|
|
|
|
|
|
</article>
|
|
|
|
|
|
<div class="wolai-floating-actions" aria-label="浮动操作">
|
2026-04-30 06:58:17 +08:00
|
|
|
|
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
|
2026-05-06 21:44:20 +08:00
|
|
|
|
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
|
2026-04-29 12:24:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-30 05:46:36 +08:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
const SIDEBAR_TREE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-tree-runtime.js");
|
2026-05-26 01:51:07 +08:00
|
|
|
|
const SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/sidebar-tree-live-apply-runtime.js");
|
2026-05-26 01:58:00 +08:00
|
|
|
|
const SIDEBAR_FILETREE_OPEN_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/sidebar-filetree-open-runtime.js");
|
2026-05-26 02:05:36 +08:00
|
|
|
|
const SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/sidebar-attachment-open-runtime.js");
|
2026-05-25 22:53:15 +08:00
|
|
|
|
const SIDEBAR_SHELL_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/sidebar-shell-runtime.js");
|
2026-05-26 01:31:28 +08:00
|
|
|
|
const SIDEBAR_WORKSPACE_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/sidebar-workspace-runtime.js");
|
2026-05-25 00:22:08 +08:00
|
|
|
|
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/filetree-context-menu-runtime.js");
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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");
|
2026-05-25 00:22:08 +08:00
|
|
|
|
const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js");
|
2026-05-25 00:37:47 +08:00
|
|
|
|
const FILETREE_SELECTION_RUNTIME_JS: &str =
|
|
|
|
|
|
include_str!("../../../browser/filetree-selection-runtime.js");
|
2026-05-24 21:35:21 +08:00
|
|
|
|
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
|
2026-04-30 05:46:36 +08:00
|
|
|
|
|
2026-05-23 23:38:42 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnoteNavigationInFlight"));
|
|
|
|
|
|
assert!(SIDEBAR_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_TREE_RUNTIME_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\"")
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("treeView"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree:local-command"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyCreatedDocumentLocally"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
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
|
2026-05-16 22:03:14 +08:00
|
|
|
|
.contains("applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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_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"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("^思维导图"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("openMindmapAssetInDocumentShell"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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)"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
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
|
2026-05-20 12:58:29 +08:00
|
|
|
|
.contains("value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length)"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fetchCurrentOnlyOfficeUserId"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("/api/auth/whoami"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildOnlyOfficeOpenUrl"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("target.searchParams.set('userId'"));
|
|
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("window.open(officeUrl,"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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_TREE_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"));
|
2026-05-26 01:31:28 +08:00
|
|
|
|
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'"));
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_ai_context_uses_page_aggregate_subtree_as_single_truth() {
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
!SIDEBAR_TREE_RUNTIME_JS.contains("pageSubtreeSource: 'local'"),
|
2026-05-18 17:01:35 +08:00
|
|
|
|
"页面 AI context 不能从编辑器 DOM 派生 local page subtree;必须以 Rust Page Aggregate projection 为准"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
!SIDEBAR_TREE_RUNTIME_JS.contains("buildPageAiLocalSubtree(localBlocks"),
|
2026-05-18 17:01:35 +08:00
|
|
|
|
"页面 AI context 不应调用本地 page subtree builder"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 08:07:17 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_ai_fast_path_is_not_local_first_main_path() {
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("if (currentSourceKind() === 'local_folder') return false;"),
|
2026-05-19 08:07:17 +08:00
|
|
|
|
"local-first 页面 AI 不应继续加厚 page-ai fast-path;本地编辑应走受控文件工具"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_ai_local_source_passes_file_reference_fields_to_agent_run() {
|
2026-05-26 01:31:28 +08:00
|
|
|
|
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentRootUri()"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sourceKind: currentSourceKind()"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rootUri: currentRootUri()"));
|
2026-05-26 01:31:28 +08:00
|
|
|
|
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
|
2026-05-20 10:43:38 +08:00
|
|
|
|
.contains("if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind"));
|
2026-05-26 01:31:28 +08:00
|
|
|
|
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("if (rootUri && !payload.rootUri) payload.rootUri = rootUri"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageContext: scopedContext.pageContext"));
|
2026-05-20 10:43:38 +08:00
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("if (currentSourceKind() === 'local_folder') return false;"),
|
2026-05-19 08:07:17 +08:00
|
|
|
|
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_ai_uses_backend_acp_session_runtime_store() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/hermes/client/sessions?"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("params.set('source', 'acp')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-rename"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-delete"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-resume"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-search"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiUsageSummary"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("usage.updated"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("thought.delta"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("permission.requested"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\""));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\""));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiHidePermissionDialog"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (!message.resolved)"));
|
2026-05-18 17:01:35 +08:00
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
!SIDEBAR_TREE_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"),
|
2026-05-18 17:01:35 +08:00
|
|
|
|
"已决 ACP permission 事件不能继续展示假审批按钮"
|
|
|
|
|
|
);
|
2026-05-21 05:40:06 +08:00
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("session.info.updated"),
|
2026-05-21 05:40:06 +08:00
|
|
|
|
"ACP SessionInfoUpdate 事件应通过 session.info.updated SSE 转发到前端"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("plan.updated"),
|
2026-05-21 05:40:06 +08:00
|
|
|
|
"ACP PlanUpdate 事件应通过 plan.updated SSE 转发到前端"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-plan"),
|
2026-05-21 05:40:06 +08:00
|
|
|
|
"plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("执行计划 · "),
|
2026-05-21 05:40:06 +08:00
|
|
|
|
"plan 面板标题应显示执行计划和步数"
|
|
|
|
|
|
);
|
2026-05-18 17:01:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 08:07:17 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("local_private"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("local_shared"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("convex_acp_runtime_store"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("本地私有"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("共享会话"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("云端会话"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sessionStorage:"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("permissionLevel:"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shareId:"));
|
2026-05-19 08:07:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-19 08:07:17 +08:00
|
|
|
|
.contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("默认 (Hermes HTTP)"));
|
2026-05-19 08:07:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 17:01:35 +08:00
|
|
|
|
#[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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("usePreferredSidebarSnapshot"));
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("preferredSidebarSnapshot"));
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("liveSidebarTitle"));
|
2026-05-18 17:01:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn page_layout_sidebar_toggle_is_wired_to_shell_state() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function toggleWorkspaceSidebar"));
|
2026-05-25 22:53:15 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("[data-mnote-action=\"toggle-sidebar\"]"));
|
2026-05-18 17:01:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 23:20:25 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_workspace_source_switch_remembers_real_cloud_workspace_before_local_folder() {
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
!SIDEBAR_TREE_RUNTIME_JS.contains("rememberCloudWorkspaceId(resolveWorkspaceId(document.body))"),
|
2026-05-24 02:23:48 +08:00
|
|
|
|
"进入本地文件夹前不能用 document.body 推导 workspaceId;它会在无 workspaceId URL 时回退成 default"
|
2026-05-15 23:20:25 +08:00
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-26 01:31:28 +08:00
|
|
|
|
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("rememberCurrentCloudWorkspaceId()"),
|
2026-05-24 02:23:48 +08:00
|
|
|
|
"进入本地文件夹前应从 URL 或 DOM 子树读取真实 workspaceId"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-26 01:31:28 +08:00
|
|
|
|
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("normalized.indexOf('local-ws:') === 0"),
|
2026-05-24 02:23:48 +08:00
|
|
|
|
"本地文件夹 workspaceId 不能写入 lastCloudWorkspaceId"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-26 01:31:28 +08:00
|
|
|
|
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("stored.trim().indexOf('local-ws:') !== 0"),
|
2026-05-24 02:23:48 +08:00
|
|
|
|
"切回工作区时不能复用 local-ws:* 作为旧云 workspace"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
!SIDEBAR_TREE_RUNTIME_JS
|
2026-05-24 02:23:48 +08:00
|
|
|
|
.contains("targetUrl.searchParams.set('sourceKind', 'convex_workspace')"),
|
|
|
|
|
|
"默认来源菜单不能跳到已退出 dev 主链的 convex_workspace"
|
2026-05-15 23:20:25 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_polls_local_folder_without_browser_reload() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("startLocalFolderSidebarWatch"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
2026-05-26 01:31:28 +08:00
|
|
|
|
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentWorkspaceId()"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var rootUri = currentRootUri();"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/local-folder-watch"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-19 12:35:53 +08:00
|
|
|
|
.contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("renderFileProjection(filePayload.result || filePayload)"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-19 12:35:53 +08:00
|
|
|
|
.contains("fetch(window.location.href, { headers: { accept: 'text/html' } })"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-19 12:35:53 +08:00
|
|
|
|
.contains("replaceSidebarTreeFromDocument(nextDocument, 'sidebar-tree-root')"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("window.location.reload"));
|
2026-05-15 23:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 23:09:56 +08:00
|
|
|
|
#[test]
|
2026-05-19 08:07:17 +08:00
|
|
|
|
fn sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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"));
|
2026-05-23 23:38:42 +08:00
|
|
|
|
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"
|
|
|
|
|
|
);
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("localOfficeUrl"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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"));
|
2026-05-19 08:07:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 23:43:22 +08:00
|
|
|
|
#[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"));
|
2026-05-25 00:57:52 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function fetchWithTimeout"));
|
2026-05-25 01:13:08 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadLocalFolderAsset"));
|
2026-05-25 01:52:11 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadMediaAsset"));
|
2026-05-25 01:28:15 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function insertUploadedAssetIntoEditor"));
|
2026-05-25 02:24:48 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFileToMediaAsset"));
|
2026-05-25 02:00:34 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFilesWithResolvedTarget"));
|
2026-05-25 01:28:15 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-last-upload-inserted"));
|
2026-05-25 02:24:48 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFileToMediaAsset: uploadFileToMediaAsset"));
|
2026-05-24 23:43:22 +08:00
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget"));
|
|
|
|
|
|
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 00:22:08 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeRuntimeFunction"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime"));
|
2026-05-25 08:46:36 +08:00
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeRowDocumentId')")
|
2026-05-25 08:46:36 +08:00
|
|
|
|
);
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeRowAssetId')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeRowLocalRelativePath')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeAssetDownloadDetail')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_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_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeUploadTargetFallback')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 08:46:36 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 08:46:36 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeDropPreflightRows')"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
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')"));
|
2026-05-25 04:16:35 +08:00
|
|
|
|
assert!(
|
2026-05-26 01:51:07 +08:00
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('parentIdOf')")
|
2026-05-25 04:16:35 +08:00
|
|
|
|
);
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeRuntimeFunction('titleOf')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('fileWorkspaceRelativePath')"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('groupRowsByParent')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('replaceSidebarTreeFromDocument')"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')"));
|
|
|
|
|
|
let document_id_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "fileTreeRowDocumentId");
|
2026-05-25 00:22:08 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
document_id_body.contains("data-document-id")
|
|
|
|
|
|
&& document_id_body.contains("data-doc-id"),
|
|
|
|
|
|
"inline fallback 必须保留旧 data attribute 解析"
|
|
|
|
|
|
);
|
2026-05-26 01:51:07 +08:00
|
|
|
|
let replace_body = js_function_body(
|
|
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
|
|
|
|
|
"replaceSidebarTreeFromDocument",
|
|
|
|
|
|
);
|
2026-05-25 04:16:35 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
replace_body.contains("current.innerHTML = next.innerHTML")
|
|
|
|
|
|
&& replace_body.contains("current.setAttribute(attr.name, attr.value)"),
|
|
|
|
|
|
"inline fallback 必须保留 DOM copy 行为"
|
|
|
|
|
|
);
|
2026-05-26 01:51:07 +08:00
|
|
|
|
let apply_body = js_function_body(
|
|
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
|
|
|
|
|
"applyLocalFolderSidebarSnapshot",
|
|
|
|
|
|
);
|
2026-05-25 06:47:54 +08:00
|
|
|
|
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 应用行为"
|
|
|
|
|
|
);
|
2026-05-26 01:51:07 +08:00
|
|
|
|
let watch_applied_body = js_function_body(
|
|
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
|
|
|
|
|
"markLocalFolderWatchApplied",
|
|
|
|
|
|
);
|
2026-05-25 06:55:35 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
watch_applied_body.contains("data-mnote-local-folder-watch-applied")
|
|
|
|
|
|
&& watch_applied_body.contains("String(value || 'projection')"),
|
|
|
|
|
|
"inline fallback 必须保留 local folder watch projection 标记"
|
|
|
|
|
|
);
|
2026-05-25 00:22:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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"));
|
2026-05-25 03:14:54 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeRow"));
|
|
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function revealFileTreeAssetRow"));
|
|
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function appendUploadedAssetRow"));
|
2026-05-25 02:55:45 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeUploadTargetFallback"));
|
2026-05-25 08:46:36 +08:00
|
|
|
|
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"));
|
2026-05-25 09:28:50 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
FILETREE_RUNTIME_JS.contains("function fileTreeDocumentWorkspacesForUploadPreflight")
|
|
|
|
|
|
);
|
2026-05-25 09:38:50 +08:00
|
|
|
|
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"));
|
2026-05-25 04:02:36 +08:00
|
|
|
|
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"));
|
2026-05-25 04:16:35 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function replaceSidebarTreeFromDocument"));
|
2026-05-25 06:47:54 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function applyLocalFolderSidebarSnapshot"));
|
2026-05-25 06:55:35 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("function markLocalFolderWatchApplied"));
|
2026-05-25 03:14:54 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("appendUploadedAssetRow: appendUploadedAssetRow"));
|
2026-05-25 04:02:36 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains("groupRowsByParent: groupRowsByParent"));
|
2026-05-25 04:16:35 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS
|
|
|
|
|
|
.contains("replaceSidebarTreeFromDocument: replaceSidebarTreeFromDocument"));
|
2026-05-25 06:47:54 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS
|
|
|
|
|
|
.contains("applyLocalFolderSidebarSnapshot: applyLocalFolderSidebarSnapshot"));
|
2026-05-25 06:55:35 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS
|
|
|
|
|
|
.contains("markLocalFolderWatchApplied: markLocalFolderWatchApplied"));
|
2026-05-25 08:46:36 +08:00
|
|
|
|
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")
|
|
|
|
|
|
);
|
2026-05-25 09:28:50 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS.contains(
|
|
|
|
|
|
"fileTreeDocumentWorkspacesForUploadPreflight: fileTreeDocumentWorkspacesForUploadPreflight"
|
|
|
|
|
|
));
|
2026-05-25 09:38:50 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 09:28:50 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 09:38:50 +08:00
|
|
|
|
.contains("fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 09:28:50 +08:00
|
|
|
|
.contains("fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('shortMindmapFileName')"));
|
2026-05-25 09:38:50 +08:00
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('parseMindmapApiTarget')")
|
2026-05-25 09:38:50 +08:00
|
|
|
|
);
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('requestMethod')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeRuntimeFunction('requestBodyHasMindmapCreateOnly')"));
|
2026-05-25 02:55:45 +08:00
|
|
|
|
assert!(FILETREE_RUNTIME_JS
|
|
|
|
|
|
.contains("uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')"));
|
|
|
|
|
|
assert!(FILETREE_RUNTIME_JS
|
|
|
|
|
|
.contains("uploadIntent: String(detail.uploadIntent || 'editor.markdown.attach')"));
|
2026-05-25 00:22:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 00:37:47 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_selection_runtime_helpers_are_externalized_with_inline_fallback() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeSelectionRuntimeFunction"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("fileTreeSelectionRuntimeFunction('visibleFileTreeRows')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 00:37:47 +08:00
|
|
|
|
.contains("fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 00:37:47 +08:00
|
|
|
|
.contains("fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 00:37:47 +08:00
|
|
|
|
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-25 00:37:47 +08:00
|
|
|
|
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let select_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "selectSidebarFileTreeRow");
|
2026-05-25 00:37:47 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
select_body.contains("shiftKey && sidebarFileTreeSelection.anchorRowId")
|
|
|
|
|
|
&& select_body.contains("ctrlKey"),
|
|
|
|
|
|
"inline fallback 必须保留 shift range 与 ctrl/meta toggle"
|
|
|
|
|
|
);
|
2026-05-25 07:55:37 +08:00
|
|
|
|
let select_document_body =
|
2026-05-25 17:36:17 +08:00
|
|
|
|
js_function_body(SIDEBAR_TREE_RUNTIME_JS, "selectSidebarFileTreeDocument");
|
2026-05-25 07:55:37 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
select_document_body
|
|
|
|
|
|
.contains("selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false })"),
|
|
|
|
|
|
"document selection 应复用 filetree row selection runtime/fallback"
|
|
|
|
|
|
);
|
2026-05-25 00:37:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 14:39:38 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote.pendingLocalFolderRestoreFiletreeRowId"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-local-folder-restore-focused-row-id"));
|
2026-05-21 14:39:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 10:43:38 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_runtime_routes_local_mindmap_and_office_assets() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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();"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
2026-05-20 10:43:38 +08:00
|
|
|
|
.contains("data-mnote-last-mindmap-asset-open-mode', 'local-mindmap-object-shell'"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildLocalOnlyOfficeOpenUrl"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isLocalAsset ? onlyOfficeUrl"));
|
2026-05-20 10:43:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 23:38:42 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_local_folder_authorized_root_normalizes_plain_path_root_uri() {
|
2026-05-26 01:31:28 +08:00
|
|
|
|
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
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("var rootUri = normalizeGrantedLocalFolderRootUri(grant);"));
|
|
|
|
|
|
assert!(
|
2026-05-26 01:31:28 +08:00
|
|
|
|
SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function isDefaultWorkspaceAutoGrant(grant)")
|
2026-05-25 17:36:17 +08:00
|
|
|
|
);
|
2026-05-26 01:31:28 +08:00
|
|
|
|
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);"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("button.textContent = rootUri.replace(/^file:\\/\\//, '')"));
|
2026-05-23 23:38:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_runtime_supports_resizable_filetree_and_hover_titles() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function installWorkspaceSidebarResizer()"));
|
2026-05-25 22:53:15 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installWorkspaceSidebarResizer();"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(r#"" title="' + escapeHtml(title) + '""#)
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-23 23:38:42 +08:00
|
|
|
|
.contains(r#"class="tree-link-title" title="' + escapeHtml(title) + '""#));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-05-24 01:49:51 +08:00
|
|
|
|
fn sidebar_runtime_opens_local_pdf_assets_in_active_resource_tab_by_default() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("function shouldOpenLocalResourceInNewWindow(fileName)"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("return false;"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("var shouldOpenInNewWindow = forceNewWindow || shouldOpenLocalResourceInNewWindow(localFileName);"));
|
|
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
2026-05-23 23:38:42 +08:00
|
|
|
|
.contains("if (!shouldOpenInNewWindow && await openLocalResourceInActiveTab({"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
|
2026-05-23 23:38:42 +08:00
|
|
|
|
"openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab'"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 08:07:17 +08:00
|
|
|
|
#[test]
|
2026-05-17 23:09:56 +08:00
|
|
|
|
fn sidebar_tree_runtime_contains_dev_hot_reload_client() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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)"));
|
2026-05-17 23:09:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 06:58:17 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openTreeContextMenu"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote-tree-context-menu"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("复制访问链接"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("删除到垃圾桶"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function createFileTreeFolder"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("action: 'create_folder'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeMenuTargetParentId"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (action === 'new-folder')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeCopyPath(detail, trigger)"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-21 09:04:13 +08:00
|
|
|
|
.contains("if (detail.contextKind === 'filetree' && action === 'download')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("downloadSelectedFileTreeAssetRows(detail, trigger)")
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function selectedSidebarFileTreeRowsForDownload"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("function selectedSidebarFileTreeAssetRowsForDownload"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-local-relative-path"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("targetRelativePath"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
|
2026-05-21 09:04:13 +08:00
|
|
|
|
"recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary)"
|
|
|
|
|
|
));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-download-count"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_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_TREE_RUNTIME_JS.contains(
|
2026-05-21 05:40:06 +08:00
|
|
|
|
"var pageRow = closestAction(e.target, '.tree-row[data-shell-mode=\"page\"]');"
|
|
|
|
|
|
));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-21 05:40:06 +08:00
|
|
|
|
.contains("var action = btn ? btn.getAttribute('data-rust-action') : 'open';"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var openTrigger = btn || pageRow;"));
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("var workspaceId = resolveWorkspaceId(openTrigger);")
|
|
|
|
|
|
);
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
|
2026-05-06 21:44:20 +08:00
|
|
|
|
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
|
|
|
|
|
|
));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
|
2026-05-06 21:44:20 +08:00
|
|
|
|
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
|
|
|
|
|
|
));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-17 20:11:39 +08:00
|
|
|
|
.contains(r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains(r#".wolai-breadcrumb-current [data-page-title-current]"#));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
|
2026-05-06 21:44:20 +08:00
|
|
|
|
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
|
2026-04-30 06:58:17 +08:00
|
|
|
|
));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-06 21:44:20 +08:00
|
|
|
|
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("setTreeLiveApplyError"));
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("scheduleProjectionRefresh"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/sidebar"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
|
2026-04-30 06:58:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 23:20:25 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
|
2026-05-26 01:51:07 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("currentFileTreeActiveRowId"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-15 23:20:25 +08:00
|
|
|
|
.contains("window.location.pathname.match(/^\\/mindmap\\/([^\\/]+)\\/([^\\/]+)/)"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
|
2026-05-15 23:20:25 +08:00
|
|
|
|
"var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId"
|
|
|
|
|
|
));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-15 23:20:25 +08:00
|
|
|
|
.contains("renderFileRows('', groupRowsByParent(rows), activeId, activeRowId)"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
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
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)"));
|
2026-05-26 01:51:07 +08:00
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("title: 'index.md'"));
|
|
|
|
|
|
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("rowId === 'index:' + activeId"));
|
2026-05-15 23:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_delete_to_trash_dispatches_archive_not_purge() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let delete_trash_start = SIDEBAR_TREE_RUNTIME_JS
|
2026-05-20 12:17:44 +08:00
|
|
|
|
.find("if (action === 'delete-trash'")
|
|
|
|
|
|
.expect("delete-trash branch");
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let delete_trash_end = SIDEBAR_TREE_RUNTIME_JS[delete_trash_start..]
|
2026-05-20 12:17:44 +08:00
|
|
|
|
.find("function appendTreeContextMenuButton")
|
|
|
|
|
|
.map(|offset| delete_trash_start + offset)
|
|
|
|
|
|
.expect("delete-trash branch end");
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let delete_trash_branch = &SIDEBAR_TREE_RUNTIME_JS[delete_trash_start..delete_trash_end];
|
2026-05-20 12:17:44 +08:00
|
|
|
|
assert!(delete_trash_branch.contains("action: 'archive'"));
|
|
|
|
|
|
assert!(!delete_trash_branch.contains("action: 'purge'"));
|
2026-05-15 23:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function deleteSelectedSidebarFileTreeRows"));
|
2026-05-21 17:15:06 +08:00
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("event.key === 'Delete' || event.key === 'Backspace'")
|
2026-05-21 17:15:06 +08:00
|
|
|
|
);
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("selectedFileTreeRows.length > 1"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("deleteSelectedSidebarFileTreeRows(trigger || document.body)"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
|
2026-05-15 23:20:25 +08:00
|
|
|
|
"currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset'"
|
|
|
|
|
|
));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (currentSourceKind() === 'local_folder')"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("documentId: fileAssetIds[lf]"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/media/batch"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/mindmap/"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tables/"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("确认删除选中的 "));
|
2026-05-15 23:20:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:45:11 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_blocks_readonly_paste_and_drop_with_action_status() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function blockReadonlyFileTreeAction"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-readonly-blocked"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-readonly-message"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('drop'"));
|
2026-05-20 21:45:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 17:15:06 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_command_context_helper_functions_exist() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function buildSidebarFileTreeContext"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'workspace.sourceKind'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'workspace.readonly'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'tree.focusKind'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'tree.selectionCount'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'tree.selectionResourceKind'"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-workspace-readonly"));
|
2026-05-21 17:15:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 00:22:08 +08:00
|
|
|
|
#[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'"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 17:15:06 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_context_menu_items_have_when_for_readonly() {
|
|
|
|
|
|
// FileTree 分支的写操作必须带 when: '!workspace.readonly'。
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("when: '!workspace.readonly'"),
|
2026-05-21 17:15:06 +08:00
|
|
|
|
"上下文菜单写操作应携带 when: '!workspace.readonly'"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Verify at least one delete-trash item has the when expression
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let delete_trash_items = SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.match_indices("'delete-trash'")
|
|
|
|
|
|
.count();
|
2026-05-21 17:15:06 +08:00
|
|
|
|
assert!(delete_trash_items > 0, "应至少有一个 delete-trash 菜单项");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_context_evaluator_uses_readonly_and_selection_count_for_delete_key() {
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS
|
|
|
|
|
|
.contains("evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')"),
|
2026-05-21 17:15:06 +08:00
|
|
|
|
"Delete/Backspace 快捷键应至少评估 workspace.readonly 上下文"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 20:48:18 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_local_table_bulk_delete_uses_tree_command() {
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let table_loop_start = SIDEBAR_TREE_RUNTIME_JS
|
2026-05-20 20:48:18 +08:00
|
|
|
|
.find("for (var t = 0; t < plan.tableRows.length; t += 1)")
|
|
|
|
|
|
.expect("table bulk delete loop");
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let table_loop_end = SIDEBAR_TREE_RUNTIME_JS[table_loop_start..]
|
2026-05-20 20:48:18 +08:00
|
|
|
|
.find("sidebarFileTreeSelection.selectedRowIds")
|
|
|
|
|
|
.map(|offset| table_loop_start + offset)
|
|
|
|
|
|
.expect("table bulk delete loop end");
|
2026-05-25 17:36:17 +08:00
|
|
|
|
let table_loop = &SIDEBAR_TREE_RUNTIME_JS[table_loop_start..table_loop_end];
|
2026-05-20 20:48:18 +08:00
|
|
|
|
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/"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 23:38:42 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_filetree_asset_open_uses_owner_document_id() {
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains(
|
2026-05-23 23:38:42 +08:00
|
|
|
|
"var ownerDocumentId = fileRow.getAttribute('data-owner-document-id') || documentId;"
|
|
|
|
|
|
),
|
|
|
|
|
|
"打开资源行时必须优先使用资源 owner document,不能用当前页面 documentId"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-25 17:36:17 +08:00
|
|
|
|
SIDEBAR_TREE_RUNTIME_JS.contains("documentId: ownerDocumentId || null"),
|
2026-05-23 23:38:42 +08:00
|
|
|
|
"tree.asset.open detail 应携带资源 owner document"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-26 01:51:07 +08:00
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("data-owner-document-id=\"' + escapeHtml(ownerDocumentId)"),
|
2026-05-23 23:38:42 +08:00
|
|
|
|
"SSR filetree 行应输出 data-owner-document-id"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
2026-05-26 01:51:07 +08:00
|
|
|
|
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
2026-05-23 23:38:42 +08:00
|
|
|
|
.contains("return 'local-md:' + bundleName + '~2F' + bundleName + '.md';"),
|
|
|
|
|
|
"local_folder bundle 资源应从 local-file 路径推导 owner markdown document"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 22:43:16 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_keeps_pdf_and_code_assets_out_of_onlyoffice() {
|
2026-05-26 01:58:00 +08:00
|
|
|
|
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"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
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'"));
|
2026-05-13 22:43:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 11:39:04 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_opens_office_assets_through_resource_shell() {
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("function buildOnlyOfficeOpenPath"));
|
|
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains(
|
2026-05-19 11:39:04 +08:00
|
|
|
|
"return '/office/' + encodeURIComponent(input.documentId) + '/' + encodeURIComponent(input.assetId)"
|
|
|
|
|
|
));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("return '/onlyoffice?' + params.toString();")
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("target.searchParams.set('mode', input.mode || 'view');"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(
|
|
|
|
|
|
SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("params.set('mode', input.mode || 'view');")
|
|
|
|
|
|
);
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains(
|
2026-05-21 05:40:06 +08:00
|
|
|
|
"{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }"
|
|
|
|
|
|
));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-21 05:40:06 +08:00
|
|
|
|
.contains("{ action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS
|
2026-05-21 05:40:06 +08:00
|
|
|
|
.contains("{ action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function withOfficeEditModeGuard(callback)"));
|
|
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-last-office-edit-mode-requested"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
|
|
|
|
|
.contains("function openEditorAttachmentEditTab(detail)"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("void openEditorAttachmentEditTab(detail);"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
|
|
|
|
|
.contains("openLocalOfficeFileInActiveTab(detail, 'edit')"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openEditorAttachmentNewWindow(detail, 'edit')"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
|
|
|
|
|
.contains("url.searchParams.set('mode', requestedMode);"));
|
|
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("mode: requestedMode"));
|
2026-05-19 11:39:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 22:43:16 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() {
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openPdfEditorAttachment"));
|
|
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openCodeEditorAttachment"));
|
|
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function resolveEditorAttachmentUrl"));
|
|
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
2026-05-25 17:36:17 +08:00
|
|
|
|
.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
2026-05-20 12:58:29 +08:00
|
|
|
|
.contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("type: 'codeBlock'"));
|
|
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attrs: { language: language }"));
|
2026-05-25 17:36:17 +08:00
|
|
|
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage"));
|
2026-05-26 02:05:36 +08:00
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
|
|
|
|
|
.contains("if (isPdfAttachmentFileName(detail.fileName))"));
|
|
|
|
|
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
|
|
|
|
|
.contains("if (isCodeAttachmentFileName(detail.fileName))"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
2026-05-13 22:43:16 +08:00
|
|
|
|
.contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
|
2026-05-26 01:58:00 +08:00
|
|
|
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));
|
2026-05-13 22:43:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 05:46:36 +08:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
|
2026-05-18 17:01:35 +08:00
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-ws"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("new WebSocket(url.toString())"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("startWithSseFallback"));
|
2026-04-30 05:46:36 +08:00
|
|
|
|
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"));
|
2026-05-19 12:03:43 +08:00
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("liveDeltaNeedsResync(payload)"));
|
|
|
|
|
|
assert!(TREE_LIVE_CONTROLLER_JS.contains("op === 'resync_required'"));
|
2026-05-11 13:16:34 +08:00
|
|
|
|
assert!(!TREE_LIVE_CONTROLLER_JS.contains("resyncEndpoint"));
|
|
|
|
|
|
assert!(!TREE_LIVE_CONTROLLER_JS.contains("tree:resync-requested"));
|
2026-05-19 12:03:43 +08:00
|
|
|
|
assert!(!TREE_LIVE_CONTROLLER_JS.contains("Delta indicates something changed"));
|
2026-04-30 05:46:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|