fix local markdown resource missing tabs and title setting

This commit is contained in:
lix-2026
2026-05-24 03:01:34 +08:00
parent 6904588055
commit 6df74fb4e0
10 changed files with 132 additions and 17 deletions
@@ -299,7 +299,7 @@ PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium node scripts/task479-local-folde
已修复 / 已验证: 已修复 / 已验证:
- 2.1 主编辑区上传附件落盘到当前 Markdown 页面资源目录:`task479` 通过。 - 2.1 主编辑区上传附件落盘到当前 Markdown 页面资源目录:`task479` 通过。
- 2.2 删除真实附件后刷新,正文 broken link 保留:`task479` 通过。 - 2.2 删除真实附件后刷新,正文 broken link 保留;点击缺失附件进入主编辑区 resource tab error state,不弹新窗口`task479` 通过。
- 2.5 刚进入 local folder、无 active Markdown document 时,点击 PDF 附件进入主编辑区 resource tab`task479` 通过,`popupCount=0` - 2.5 刚进入 local folder、无 active Markdown document 时,点击 PDF 附件进入主编辑区 resource tab`task479` 通过,`popupCount=0`
- 2.7 本地 Markdown 附件 / PDF 默认 active tab`task459``task479` 通过。 - 2.7 本地 Markdown 附件 / PDF 默认 active tab`task459``task479` 通过。
- 2.8 本地 Markdown 标题来源:File Tree / 页头标题来自文件名,正文 H1 保留在正文;`local_markdown` 单测与 `task479` 通过。 - 2.8 本地 Markdown 标题来源:File Tree / 页头标题来自文件名,正文 H1 保留在正文;`local_markdown` 单测与 `task479` 通过。
@@ -322,4 +322,5 @@ PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium node scripts/task479-local-folde
- 2.3 Backspace / Delete 删除相邻附件已补浏览器级 smoke:`task479` Check 7 通过。Backspace 与手柄删除都只删除 Markdown 链接,不删除真实附件文件,相邻附件刷新后仍保持可点击附件块。 - 2.3 Backspace / Delete 删除相邻附件已补浏览器级 smoke:`task479` Check 7 通过。Backspace 与手柄删除都只删除 Markdown 链接,不删除真实附件文件,相邻附件刷新后仍保持可点击附件块。
- 2.4 切回“我的空间”已按当前 local-first MVP 语义修复并复测:旧“云空间”入口不再进入 `convex_workspace`,改为“我的空间”,通过 `mnoteHome=1` 回默认 `my-space`,避免最近普通本地目录和旧 Convex compat 接管;`task441-local-folder-cloud-switch-smoke.js` 通过。 - 2.4 切回“我的空间”已按当前 local-first MVP 语义修复并复测:旧“云空间”入口不再进入 `convex_workspace`,改为“我的空间”,通过 `mnoteHome=1` 回默认 `my-space`,避免最近普通本地目录和旧 Convex compat 接管;`task441-local-folder-cloud-switch-smoke.js` 通过。
- 2.5 / 2.7 resource tab 打开目标已补 active/selected row 验证:`task479` Check 4 断言刚进入 asset-only local folder 点击 `report-a.pdf` 后,主编辑区 resource tab 打开且 File Tree 唯一 selected/active row 保持在该附件 row。 - 2.5 / 2.7 resource tab 打开目标已补 active/selected row 验证:`task479` Check 4 断言刚进入 asset-only local folder 点击 `report-a.pdf` 后,主编辑区 resource tab 打开且 File Tree 唯一 selected/active row 保持在该附件 row。
- 页面设置中“隐藏本地 Markdown 文件标题”的可切换 UI / 持久化尚未实现;当前只落地默认隐藏 - 2.2 missing tab 点击根因已收口:本地 Markdown 附件缺失时,`openTiptapResourceTab()` 读取 `/api/local-folder/resource/read` 失败,`openResourceInActiveTab()` 已创建 resource tab 并调用 `markResourceTabError()`,但此前仍返回 `false`;外层 `openEditorAttachmentDetail()``opened=false` 执行 `window.open()` fallback,导致弹出新窗口。修复为错误 tab 已渲染时返回 `true`,表示主编辑区已接管打开意图
- 2.8 页面设置中“隐藏本地 Markdown 文件标题”的可切换 UI / 持久化已实现:新增 `hideTitleHeader` page option,本地 Markdown 默认开启;关闭后页头立即显示,刷新后从 `.mnote/page-options.json` 恢复。`task479` Check 6 通过。
@@ -119,9 +119,9 @@ Markdown 正文链接只是引用,不是所有权声明。
- [x] 主编辑区附件删除 command 只删除 Markdown 链接并保存。 - [x] 主编辑区附件删除 command 只删除 Markdown 链接并保存。
- [x] 手柄删除复用同一 command,不做纯 DOM 隐藏。 - [x] 手柄删除复用同一 command,不做纯 DOM 隐藏。
- [ ] Parser / serializer 保留 broken link。 - [x] Parser / serializer 保留 broken link。
- [ ] watcher 收到真实文件删除后刷新存在性状态,不改正文。 - [ ] watcher 收到真实文件删除后刷新存在性状态,不改正文。
- [ ] 缺失资源点击进入 missing tab。 - [x] 缺失资源点击进入 missing / error tab。
### 5.4 Resource tab host ### 5.4 Resource tab host
@@ -137,8 +137,8 @@ Markdown 正文链接只是引用,不是所有权声明。
- [x] File Tree / Page Tree 标题默认来自文件名。 - [x] File Tree / Page Tree 标题默认来自文件名。
- [x] 正文 H1 保留在正文,不参与标题推导。 - [x] 正文 H1 保留在正文,不参与标题推导。
- [x] 本地 Markdown 页头标题默认隐藏。 - [x] 本地 Markdown 页头标题默认隐藏。
- [ ] 页面设置新增“隐藏本地 Markdown 文件标题”的可切换 UI。 - [x] 页面设置新增“隐藏本地 Markdown 文件标题”的可切换 UI。
- [ ] 设置刷新后持久生效。 - [x] 设置刷新后持久生效。
- [ ] 重命名标题时走文件重命名和 bundle 目录同步。 - [ ] 重命名标题时走文件重命名和 bundle 目录同步。
### 5.6 滚动边界 ### 5.6 滚动边界
@@ -146,7 +146,7 @@ Markdown 正文链接只是引用,不是所有权声明。
- [ ] Workbench 根容器固定视口高度。 - [ ] Workbench 根容器固定视口高度。
- [ ] Sidebar / File Tree 是唯一纵向滚动容器。 - [ ] Sidebar / File Tree 是唯一纵向滚动容器。
- [ ] resize handle wheel 不穿透到 body。 - [ ] resize handle wheel 不穿透到 body。
- [ ] smoke 断言 `document.scrollingElement.scrollTop` 不变化。 - [x] smoke 断言 `document.scrollingElement.scrollTop` 不变化。
## 6. 建议切片 ## 6. 建议切片
@@ -188,3 +188,5 @@ codegraph sync .
- 2026-05-24 追加:来源菜单的旧“云空间”入口按当前 local-first MVP 语义改为“我的空间”,默认回到受管 `my-space` 本地根;新增 `mnoteHome=1` 防止被最近普通本地目录自动重定向接管。`task441-local-folder-cloud-switch-smoke.js` 已按当前语义通过。 - 2026-05-24 追加:来源菜单的旧“云空间”入口按当前 local-first MVP 语义改为“我的空间”,默认回到受管 `my-space` 本地根;新增 `mnoteHome=1` 防止被最近普通本地目录自动重定向接管。`task441-local-folder-cloud-switch-smoke.js` 已按当前语义通过。
- 2026-05-24 追加:上传链路已改为显式 `uploadIntent` 合同。主编辑区上传发送并返回 `editor.markdown.attach`,写入 page resource directoryFile Tree folder drop 发送并返回 `filetree.folder.drop`,写入 `targetRelativePath` 指向目录。`task479` 已断言响应中的 `uploadIntent``rootRelativePath``markdownRelativePath``ownerDocumentId` - 2026-05-24 追加:上传链路已改为显式 `uploadIntent` 合同。主编辑区上传发送并返回 `editor.markdown.attach`,写入 page resource directoryFile Tree folder drop 发送并返回 `filetree.folder.drop`,写入 `targetRelativePath` 指向目录。`task479` 已断言响应中的 `uploadIntent``rootRelativePath``markdownRelativePath``ownerDocumentId`
- 2026-05-24 追加:`task479` Check 4 已补充断言 resource tab 打开后 File Tree 的 `data-selected` / `data-active` 唯一保持在被打开附件 row,避免资源打开后焦点跳到其他 row。 - 2026-05-24 追加:`task479` Check 4 已补充断言 resource tab 打开后 File Tree 的 `data-selected` / `data-active` 唯一保持在被打开附件 row,避免资源打开后焦点跳到其他 row。
- 2026-05-24 追加:`task479` Check 3 已补齐缺失真实附件后的点击行为断言。根因是 resource tab 已创建并标记 error 后仍返回 `false`,外层附件点击逻辑因此执行 `window.open()` 兜底;修复后错误 tab 也视为主编辑区已接管,broken link 刷新保留、点击不弹新窗口,并显示 `resource-note.md` 的 resource tab error state。
- 2026-05-24 追加:页面设置新增 `hideTitleHeader`,本地 Markdown 默认开启隐藏页头;用户可在页面设置中关闭,写入 `.mnote/page-options.json`,刷新后由 Page Aggregate 恢复。`task479` Check 6 已断言默认勾选、切换后页头立即显示、刷新后保持显示。
+3
View File
@@ -5766,6 +5766,9 @@ fn build_page_aggregate_projection_result(
show_block_ref_count: bool_field(meta, "show_block_ref_count") show_block_ref_count: bool_field(meta, "show_block_ref_count")
.or_else(|| bool_field(meta, "showBlockRefCount")) .or_else(|| bool_field(meta, "showBlockRefCount"))
.unwrap_or(false), .unwrap_or(false),
hide_title_header: bool_field(meta, "hide_title_header")
.or_else(|| bool_field(meta, "hideTitleHeader"))
.unwrap_or(false),
embed_default_block_id: meta embed_default_block_id: meta
.get("embed_default_block_id") .get("embed_default_block_id")
.or_else(|| meta.get("embedDefaultBlockId")) .or_else(|| meta.get("embedDefaultBlockId"))
@@ -93,6 +93,7 @@ pub struct PageOptions {
pub page_font: String, pub page_font: String,
pub hide_child_pages: bool, pub hide_child_pages: bool,
pub show_block_ref_count: bool, pub show_block_ref_count: bool,
pub hide_title_header: bool,
pub embed_default_block_id: Value, pub embed_default_block_id: Value,
} }
@@ -111,6 +112,7 @@ impl Default for PageOptions {
page_font: "default".into(), page_font: "default".into(),
hide_child_pages: false, hide_child_pages: false,
show_block_ref_count: false, show_block_ref_count: false,
hide_title_header: false,
embed_default_block_id: Value::Null, embed_default_block_id: Value::Null,
} }
} }
@@ -46,6 +46,7 @@ pub struct PageAggregateBuilder {
page_font: String, page_font: String,
hide_child_pages: bool, hide_child_pages: bool,
show_block_ref_count: bool, show_block_ref_count: bool,
hide_title_header: bool,
embed_default_block_id: Value, embed_default_block_id: Value,
content: Value, content: Value,
revision: Value, revision: Value,
@@ -86,6 +87,7 @@ impl PageAggregateBuilder {
page_font: "default".to_string(), page_font: "default".to_string(),
hide_child_pages: false, hide_child_pages: false,
show_block_ref_count: false, show_block_ref_count: false,
hide_title_header: false,
embed_default_block_id: Value::Null, embed_default_block_id: Value::Null,
content: Value::Null, content: Value::Null,
revision: Value::Null, revision: Value::Null,
@@ -305,6 +307,7 @@ impl PageAggregateBuilder {
page_font: self.page_font, page_font: self.page_font,
hide_child_pages: self.hide_child_pages, hide_child_pages: self.hide_child_pages,
show_block_ref_count: self.show_block_ref_count, show_block_ref_count: self.show_block_ref_count,
hide_title_header: self.hide_title_header,
embed_default_block_id: self.embed_default_block_id, embed_default_block_id: self.embed_default_block_id,
}; };
let layout_options = to_value(&page_options).expect("PageOptions 序列化不应失败"); let layout_options = to_value(&page_options).expect("PageOptions 序列化不应失败");
@@ -1597,7 +1597,8 @@ mod tests {
"rootUri": root_uri, "rootUri": root_uri,
"options": { "options": {
"wideLayout": true, "wideLayout": true,
"showToc": false "showToc": false,
"hideTitleHeader": false
} }
}) })
.to_string(), .to_string(),
@@ -1625,6 +1626,10 @@ mod tests {
options_json["pages"][&renamed_document_id]["showToc"], options_json["pages"][&renamed_document_id]["showToc"],
false false
); );
assert_eq!(
options_json["pages"][&renamed_document_id]["hideTitleHeader"],
false
);
let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&root);
} }
@@ -2769,12 +2769,19 @@ pub fn resolve_local_markdown_page_aggregate(
let block_count = content.as_array().map(|blocks| blocks.len()).unwrap_or(0) as u64; let block_count = content.as_array().map(|blocks| blocks.len()).unwrap_or(0) as u64;
let page_subtree = markdown_page_subtree(document_id, &title, &content); let page_subtree = markdown_page_subtree(document_id, &title, &content);
let workspace_id = local_workspace_id(&canonical_root); let workspace_id = local_workspace_id(&canonical_root);
let page_options = metadata let page_options_value = metadata
.page_options .page_options
.get(document_id) .get(document_id)
.or_else(|| metadata.page_options.get(&markdown_file.relative_path)) .or_else(|| metadata.page_options.get(&markdown_file.relative_path));
let mut page_options = page_options_value
.map(page_options_from_metadata) .map(page_options_from_metadata)
.unwrap_or_default(); .unwrap_or_default();
if page_options_value
.and_then(page_options_hide_title_header_value)
.is_none()
{
page_options.hide_title_header = true;
}
let character_count = parsed.body.chars().count() as u64; let character_count = parsed.body.chars().count() as u64;
let word_count = parsed let word_count = parsed
.body .body
@@ -6696,9 +6703,19 @@ fn page_options_from_metadata(value: &Value) -> PageOptions {
{ {
options.protect_editing = protect_editing; options.protect_editing = protect_editing;
} }
if let Some(hide_title_header) = page_options_hide_title_header_value(value) {
options.hide_title_header = hide_title_header;
}
options options
} }
fn page_options_hide_title_header_value(value: &Value) -> Option<bool> {
value
.get("hideTitleHeader")
.or_else(|| value.get("hide_title_header"))
.and_then(Value::as_bool)
}
pub(crate) fn local_workspace_id(root: &Path) -> String { pub(crate) fn local_workspace_id(root: &Path) -> String {
if let Ok(manifest) = load_local_workspace_manifest(root) { if let Ok(manifest) = load_local_workspace_manifest(root) {
if !manifest.workspace_id.trim().is_empty() { if !manifest.workspace_id.trim().is_empty() {
@@ -8294,7 +8311,7 @@ mod tests {
std::fs::write(root.join("README.md"), "# Local Options\n").expect("write md"); std::fs::write(root.join("README.md"), "# Local Options\n").expect("write md");
std::fs::write( std::fs::write(
root.join(".mnote").join("page-options.json"), root.join(".mnote").join("page-options.json"),
r#"{"version":1,"pages":{"local-md:README.md":{"wideLayout":true,"showToc":true,"showHeadingNumbers":true}}}"#, r#"{"version":1,"pages":{"local-md:README.md":{"wideLayout":true,"showToc":true,"showHeadingNumbers":true,"hideTitleHeader":false}}}"#,
) )
.expect("write options"); .expect("write options");
let root_uri = format!("file://{}", root.display()); let root_uri = format!("file://{}", root.display());
@@ -8304,6 +8321,7 @@ mod tests {
assert!(aggregate.layout.page_options.wide_layout); assert!(aggregate.layout.page_options.wide_layout);
assert!(aggregate.layout.page_options.show_toc); assert!(aggregate.layout.page_options.show_toc);
assert!(aggregate.layout.page_options.show_heading_numbers); assert!(aggregate.layout.page_options.show_heading_numbers);
assert!(!aggregate.layout.page_options.hide_title_header);
let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&root);
} }
@@ -10236,6 +10254,7 @@ fn main() {}
let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:File~20Name.md") let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:File~20Name.md")
.expect("aggregate"); .expect("aggregate");
assert_eq!(aggregate.title, "File Name"); assert_eq!(aggregate.title, "File Name");
assert!(aggregate.layout.page_options.hide_title_header);
let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&root);
} }
@@ -222,8 +222,8 @@ pub async fn document_page_shell(
secondary_workspace_id={secondary_aggregate.as_ref().map(|aggregate| aggregate.identity.workspace_id.clone()).unwrap_or_default()} secondary_workspace_id={secondary_aggregate.as_ref().map(|aggregate| aggregate.identity.workspace_id.clone()).unwrap_or_default()}
secondary_page_subtree_json={secondary_page_subtree_json.unwrap_or_default()} secondary_page_subtree_json={secondary_page_subtree_json.unwrap_or_default()}
secondary_page_options_json={secondary_page_options_json.unwrap_or_default()} secondary_page_options_json={secondary_page_options_json.unwrap_or_default()}
primary_hide_title_header={is_local_folder} primary_hide_title_header={aggregate.layout.page_options.hide_title_header}
secondary_hide_title_header={secondary_source_kind == Some("local_folder")} secondary_hide_title_header={secondary_aggregate.as_ref().map(|aggregate| aggregate.layout.page_options.hide_title_header).unwrap_or(secondary_source_kind == Some("local_folder"))}
show_admin_access_policy={is_local_access_policy_admin_context(&context)} show_admin_access_policy={is_local_access_policy_admin_context(&context)}
/> />
}); });
@@ -4185,7 +4185,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
} catch (error) { } catch (error) {
console.warn('mnote resource tab 打开失败', error); console.warn('mnote resource tab 打开失败', error);
markResourceTabError(entry); markResourceTabError(entry);
return false; return true;
} }
}; };
+13 -1
View File
@@ -501,6 +501,7 @@ const SIDEBAR_TREE_JS: &str = r##"
pageFont: 'default', pageFont: 'default',
hideChildPages: false, hideChildPages: false,
showBlockRefCount: false, showBlockRefCount: false,
hideTitleHeader: false,
embedDefaultBlockId: null embedDefaultBlockId: null
}; };
} }
@@ -545,12 +546,14 @@ const SIDEBAR_TREE_JS: &str = r##"
|| name === 'smallText' || name === 'smallText'
|| name === 'layoutDensity' || name === 'layoutDensity'
|| name === 'pageFont' || name === 'pageFont'
|| name === 'showHeadingNumbers'; || name === 'showHeadingNumbers'
|| (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
} }
function pageOptionDescription(name) { function pageOptionDescription(name) {
if (name === 'wideLayout') return ''; if (name === 'wideLayout') return '';
if (name === 'smallText') return ''; if (name === 'smallText') return '';
if (name === 'hideTitleHeader') return ' Markdown ';
if (name === 'showToc') return ''; if (name === 'showToc') return '';
if (name === 'showHeadingNumbers') return ''; if (name === 'showHeadingNumbers') return '';
if (name === 'protectEditing') return ''; if (name === 'protectEditing') return '';
@@ -564,6 +567,7 @@ const SIDEBAR_TREE_JS: &str = r##"
if (name === 'wideLayout') return ''; if (name === 'wideLayout') return '';
if (name === 'smallText') return ''; if (name === 'smallText') return '';
if (name === 'showHeadingNumbers') return ''; if (name === 'showHeadingNumbers') return '';
if (name === 'hideTitleHeader') return ' Markdown ';
if (name === 'layoutDensity') return ''; if (name === 'layoutDensity') return '';
if (name === 'pageFont') return ''; if (name === 'pageFont') return '';
if (name === 'showToc') return '线 Rust '; if (name === 'showToc') return '线 Rust ';
@@ -663,6 +667,13 @@ const SIDEBAR_TREE_JS: &str = r##"
document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default')); document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers)); document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers));
document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers)); document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
if (titleHeader instanceof HTMLElement) {
var hidden = Boolean(options.hideTitleHeader);
titleHeader.hidden = hidden;
titleHeader.setAttribute('data-page-title-hidden', String(hidden));
}
} }
function normalizeSidebarTreeMode(value) { function normalizeSidebarTreeMode(value) {
@@ -8358,6 +8369,7 @@ const SIDEBAR_TREE_JS: &str = r##"
createPageOptionRow('smallText', 'checkbox') + createPageOptionRow('smallText', 'checkbox') +
createPageOptionRow('showToc', 'checkbox') + createPageOptionRow('showToc', 'checkbox') +
createPageOptionRow('protectEditing', 'checkbox') + createPageOptionRow('protectEditing', 'checkbox') +
createPageOptionRow('hideTitleHeader', 'checkbox') +
'</div>' + '</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' + '<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
createPageFontRow() + createPageFontRow() +
@@ -477,16 +477,41 @@ async function main() {
// 预期行为:链接应保留 // 预期行为:链接应保留
const linkPreserved = linkTextAfter.includes("resource-note.md"); const linkPreserved = linkTextAfter.includes("resource-note.md");
const popupCountBefore = popups.length;
const brokenLink = page
.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a', {
hasText: "resource-note.md",
})
.first();
await brokenLink.click({ timeout: UI_TIMEOUT_MS });
await page.waitForTimeout(1500);
const popupCountAfter = popups.length;
const resourceTabErrorVisible = await page
.locator('[data-testid="mnote-resource-tab-host"]:not([hidden]) .mnote-resource-tab-error, [data-mnote-resource-panel][data-resource-kind="error"]')
.first()
.isVisible()
.catch(() => false);
const resourceTabTitle = await page
.locator('[data-mnote-main-tab][data-pane-role="primary"] .mnote-main-tab-title', { hasText: "resource-note.md" })
.first()
.textContent({ timeout: UI_TIMEOUT_MS })
.catch(() => "");
assert.equal(popupCountAfter, popupCountBefore, "点击缺失附件不应打开新浏览器窗口");
assert(resourceTabErrorVisible, "点击缺失附件应进入 resource tab missing/error state");
assert((resourceTabTitle || "").includes("resource-note.md"), `缺失资源 tab 标题应包含 resource-note.md,实际: ${resourceTabTitle}`);
checks[checkId] = { checks[checkId] = {
ok: linkPreserved, ok: linkPreserved,
message: linkPreserved message: linkPreserved
? "删除真实附件后刷新,正文链接仍保留" ? "删除真实附件后刷新,正文链接仍保留;点击缺失链接进入 resource tab missing/error state"
: "删除真实附件后刷新,正文链接消失(当前实现未保留 broken link", : "删除真实附件后刷新,正文链接消失(当前实现未保留 broken link",
assetPath, assetPath,
deleted: true, deleted: true,
linkPreserved, linkPreserved,
editorText: linkTextAfter.slice(0, 500), editorText: linkTextAfter.slice(0, 500),
popupCount: popupCountAfter - popupCountBefore,
resourceTabErrorVisible,
resourceTabTitle,
}; };
if (!linkPreserved) { if (!linkPreserved) {
@@ -731,14 +756,57 @@ async function main() {
// 预期:文件树显示 MyNotes.md(文件名),正文显示 Body HeadingH1 内容) // 预期:文件树显示 MyNotes.md(文件名),正文显示 Body HeadingH1 内容)
const treeShowsFileName = treeTitle && treeTitle.includes("MyNotes.md"); const treeShowsFileName = treeTitle && treeTitle.includes("MyNotes.md");
const bodyShowsHeading = bodyHeading && bodyHeading.includes("Body Heading"); const bodyShowsHeading = bodyHeading && bodyHeading.includes("Body Heading");
const titleHeaderInitiallyHidden = await page.evaluate(() => {
const header = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header');
return header instanceof HTMLElement ? header.hidden || header.getAttribute("data-page-title-hidden") === "true" : false;
});
await page.locator('[data-testid="wolai-page-settings-trigger"]').click({ timeout: UI_TIMEOUT_MS });
const hideTitleCheckbox = page.locator('[data-page-option-checkbox="hideTitleHeader"]').first();
await hideTitleCheckbox.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const hideTitleCheckedBefore = await hideTitleCheckbox.isChecked();
assert.equal(hideTitleCheckedBefore, true, "本地 Markdown 应默认开启隐藏文件标题");
await hideTitleCheckbox.setChecked(false, { timeout: UI_TIMEOUT_MS });
const optionsPath = path.join(root, ".mnote", "page-options.json");
const optionsSaved = await waitForFileContent(
optionsPath,
(content) => {
try {
const payload = JSON.parse(content);
return payload?.pages?.["local-md:MyNotes.md"]?.hideTitleHeader === false;
} catch {
return false;
}
},
UI_TIMEOUT_MS,
);
assert(optionsSaved.ok, `页面设置应持久化 hideTitleHeader=false,实际: ${optionsSaved.content}`);
await page.waitForTimeout(500);
const titleHeaderVisibleAfterToggle = await page.evaluate(() => {
const header = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header');
return header instanceof HTMLElement ? !header.hidden && header.getAttribute("data-page-title-hidden") === "false" : false;
});
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
const titleHeaderVisibleAfterReload = await page.evaluate(() => {
const header = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header');
return header instanceof HTMLElement ? !header.hidden && header.getAttribute("data-page-title-hidden") === "false" : false;
});
checks[checkId] = { checks[checkId] = {
ok: !!(treeShowsFileName && bodyShowsHeading), ok: !!(treeShowsFileName && bodyShowsHeading && titleHeaderInitiallyHidden && titleHeaderVisibleAfterToggle && titleHeaderVisibleAfterReload),
message: `treeTitle="${treeTitle}" bodyHeading="${bodyHeading}"`, message: `treeTitle="${treeTitle}" bodyHeading="${bodyHeading}"`,
treeTitle, treeTitle,
bodyHeading, bodyHeading,
treeShowsFileName, treeShowsFileName,
bodyShowsHeading, bodyShowsHeading,
titleHeaderInitiallyHidden,
hideTitleCheckedBefore,
titleHeaderVisibleAfterToggle,
titleHeaderVisibleAfterReload,
}; };
} catch (err) { } catch (err) {
overallOk = false; overallOk = false;