050901 非全量刷新

This commit is contained in:
lix-2026
2026-05-09 06:24:50 +08:00
parent 3d5e0c9d5a
commit 71146de5f0
4 changed files with 350 additions and 55 deletions
@@ -370,7 +370,7 @@
- [x] 确认本地 `.md` 读写 smoke 不影响 file tree / page tree / page aggregate 路由。 - [x] 确认本地 `.md` 读写 smoke 不影响 file tree / page tree / page aggregate 路由。
### 11.9 清理与迁移收尾 ### 11.9 清理与迁移收尾11
- [ ] 把手写 parser 标记为过渡实现。11 - [ ] 把手写 parser 标记为过渡实现。11
+247 -21
View File
@@ -76,20 +76,20 @@ pub async fn document_page_shell(
.await?; .await?;
let title = aggregate.head.title.as_str(); let title = aggregate.head.title.as_str();
let workspace_id = aggregate.identity.workspace_id.clone(); let workspace_id = aggregate.identity.workspace_id.clone();
let requested_secondary_document_id = normalize_optional_owned(query.secondary_document_id.as_deref()); let requested_secondary_document_id =
normalize_optional_owned(query.secondary_document_id.as_deref());
let secondary_source_kind = normalize_source_kind( let secondary_source_kind = normalize_source_kind(
query.secondary_source_kind query
.secondary_source_kind
.as_deref() .as_deref()
.or(primary_source_kind), .or(primary_source_kind),
); );
let secondary_root_uri = normalize_optional_query_value( let secondary_root_uri =
query.secondary_root_uri normalize_optional_query_value(query.secondary_root_uri.as_deref().or(primary_root_uri));
.as_deref()
.or(primary_root_uri),
);
let mut secondary_requested = false; let mut secondary_requested = false;
let mut secondary_invalid = false; let mut secondary_invalid = false;
let secondary_aggregate = if let Some(secondary_document_id) = requested_secondary_document_id.as_deref() { let secondary_aggregate =
if let Some(secondary_document_id) = requested_secondary_document_id.as_deref() {
secondary_requested = true; secondary_requested = true;
match build_page_aggregate_snapshot( match build_page_aggregate_snapshot(
&state, &state,
@@ -153,18 +153,14 @@ pub async fn document_page_shell(
let page_options_json = serde_json::to_string(&aggregate.layout.page_options) let page_options_json = serde_json::to_string(&aggregate.layout.page_options)
.unwrap_or_else(|_| "null".to_string()); .unwrap_or_else(|_| "null".to_string());
let snapshot_json = serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string()); let snapshot_json = serde_json::to_string(&aggregate).unwrap_or_else(|_| "null".to_string());
let bootstrap_json = build_editor_bootstrap_json( let bootstrap_json =
&aggregate, build_editor_bootstrap_json(&aggregate, &context, primary_source_kind, primary_root_uri);
&context, let secondary_page_subtree_json = secondary_aggregate.as_ref().map(|aggregate| {
primary_source_kind, serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string())
primary_root_uri, });
); let secondary_page_options_json = secondary_aggregate.as_ref().map(|aggregate| {
let secondary_page_subtree_json = secondary_aggregate serde_json::to_string(&aggregate.layout.page_options).unwrap_or_else(|_| "null".to_string())
.as_ref() });
.map(|aggregate| serde_json::to_string(&aggregate.tree.page_subtree).unwrap_or_else(|_| "null".to_string()));
let secondary_page_options_json = secondary_aggregate
.as_ref()
.map(|aggregate| serde_json::to_string(&aggregate.layout.page_options).unwrap_or_else(|_| "null".to_string()));
let secondary_snapshot_json = secondary_aggregate let secondary_snapshot_json = secondary_aggregate
.as_ref() .as_ref()
.map(|aggregate| serde_json::to_string(aggregate).unwrap_or_else(|_| "null".to_string())); .map(|aggregate| serde_json::to_string(aggregate).unwrap_or_else(|_| "null".to_string()));
@@ -565,6 +561,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const replaceUrlState = (url) => { const replaceUrlState = (url) => {
window.history.replaceState({}, '', url.pathname + url.search + url.hash); window.history.replaceState({}, '', url.pathname + url.search + url.hash);
}; };
const pushUrlState = (url) => {
window.history.pushState({}, '', url.pathname + url.search + url.hash);
};
const clearSecondaryParams = () => { const clearSecondaryParams = () => {
const url = currentUrl(); const url = currentUrl();
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name)); secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
@@ -584,6 +583,15 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
else url.searchParams.delete(paneRouteConfig.secondary.sourceKindParam); else url.searchParams.delete(paneRouteConfig.secondary.sourceKindParam);
if (primaryRootUri) url.searchParams.set(paneRouteConfig.secondary.rootUriParam, primaryRootUri); if (primaryRootUri) url.searchParams.set(paneRouteConfig.secondary.rootUriParam, primaryRootUri);
else url.searchParams.delete(paneRouteConfig.secondary.rootUriParam); else url.searchParams.delete(paneRouteConfig.secondary.rootUriParam);
if (typeof window.__mnoteDocumentPaneRuntime?.openSecondaryDocument === 'function') {
void window.__mnoteDocumentPaneRuntime.openSecondaryDocument({
documentId,
sourceKind: primarySourceKind || null,
rootUri: primaryRootUri || null,
url,
});
return;
}
window.location.assign(url.pathname + url.search + url.hash); window.location.assign(url.pathname + url.search + url.hash);
}); });
@@ -592,6 +600,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
event.preventDefault(); event.preventDefault();
const url = currentUrl(); const url = currentUrl();
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name)); secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
if (typeof window.__mnoteDocumentPaneRuntime?.closeSecondaryDocument === 'function') {
window.__mnoteDocumentPaneRuntime.closeSecondaryDocument({ url });
return;
}
window.location.assign(url.pathname + url.search + url.hash); window.location.assign(url.pathname + url.search + url.hash);
}); });
}); });
@@ -968,9 +980,29 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (bootstrap.rootUri) url.searchParams.set('rootUri', bootstrap.rootUri); if (bootstrap.rootUri) url.searchParams.set('rootUri', bootstrap.rootUri);
return url; return url;
}; };
const pageAggregateUrlFromDescriptor = (descriptor) => {
const url = new URL(`/api/page-aggregate/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
url.searchParams.set('sourceKind', descriptor.sourceKind || 'convex_workspace');
if (descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
return url;
};
const buildBootstrapFromAggregate = (aggregate, descriptor, paneRole) => ({
schema: 'mnote.editor_bootstrap.v1',
documentId: aggregate?.identity?.documentId || aggregate?.identity?.document_id || descriptor.documentId,
workspaceId: aggregate?.identity?.workspaceId || aggregate?.identity?.workspace_id || descriptor.workspaceId || '',
paneRole,
sourceKind: descriptor.sourceKind || 'convex_workspace',
rootUri: descriptor.rootUri || '',
pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
saveEndpoint: '/api/documents/save',
titleEndpoint: '/api/documents/title',
editorHostKind: 'leptos_tiptap_island',
});
const documentSessionRegistry = new Map(); const documentSessionRegistry = new Map();
const localFolderEventRegistry = new Map(); const localFolderEventRegistry = new Map();
const paneViewRegistry = new Map();
let nextViewId = 1; let nextViewId = 1;
const externalConflictMessage = '本地 Markdown 文件已在外部更新,请刷新或保存前先处理冲突'; const externalConflictMessage = '本地 Markdown 文件已在外部更新,请刷新或保存前先处理冲突';
const SESSION_RELEASE_DELAY_MS = 1200; const SESSION_RELEASE_DELAY_MS = 1200;
@@ -1452,6 +1484,174 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return session; return session;
}; };
const updatePaneChrome = (runtimeDescriptor) => {
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
if (!(pane instanceof HTMLElement)) return;
const aggregate = runtimeDescriptor.aggregate || {};
const bootstrap = runtimeDescriptor.bootstrap || {};
const title = aggregate?.head?.title || '无标题';
const documentId = bootstrap.documentId || aggregate?.identity?.documentId || '';
const workspaceId = bootstrap.workspaceId || aggregate?.identity?.workspaceId || '';
pane.setAttribute('data-pane-document-id', documentId);
pane.setAttribute('data-pane-workspace-id', workspaceId);
pane.setAttribute('data-pane-visible', 'true');
pane.hidden = false;
const shell = pane.querySelector('.document-shell');
if (shell instanceof HTMLElement) {
shell.setAttribute('data-document-id', documentId);
shell.setAttribute('data-workspace-id', workspaceId);
const options = aggregate?.layout?.pageOptions || {};
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
shell.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
}
pane.querySelectorAll('[data-page-title-input="true"]').forEach((node) => {
if (!(node instanceof HTMLTextAreaElement)) return;
node.value = title;
node.setAttribute('data-document-id', documentId);
node.setAttribute('data-workspace-id', workspaceId);
node.setAttribute('data-title-save-status', 'saved');
node.style.height = 'auto';
node.style.height = `${Math.max(48, node.scrollHeight)}px`;
});
pane.querySelectorAll('[data-page-title-current="true"]').forEach((node) => {
if (node instanceof HTMLElement) node.textContent = title;
});
if (runtimeDescriptor.paneRole === 'primary') {
document.body.dataset.documentId = documentId;
document.title = title;
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
if (row instanceof HTMLElement) {
row.setAttribute('data-active', 'false');
row.setAttribute('data-selected', 'false');
}
});
const escapedId = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape(documentId) : String(documentId).replace(/["\\]/g, '\\$&');
document.querySelectorAll(`.tree-row[data-node-id="${escapedId}"], .tree-row[data-document-id="${escapedId}"], .tree-row[data-doc-id="${escapedId}"]`).forEach((row) => {
if (!(row instanceof HTMLElement)) return;
if (row.getAttribute('data-shell-mode') === 'filetree') {
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === `index:${documentId}`));
} else {
row.setAttribute('data-active', 'true');
}
});
}
};
const fetchPageAggregateForPane = async (descriptor) => {
const response = await fetch(pageAggregateUrlFromDescriptor(descriptor).toString(), {
cache: 'no-store',
headers: { accept: 'application/json' },
});
if (!response.ok) throw new Error(`page_aggregate_failed_${response.status}`);
const payload = await response.json();
if (!payload?.result) throw new Error('page_aggregate_missing_result');
return payload.result;
};
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
const runtime = await loadRuntime();
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
if (!(root instanceof HTMLElement)) throw new Error(`pane_root_missing_${paneRole}`);
const previousView = paneViewRegistry.get(paneRole);
if (previousView) {
unmountEditorViewBinding(previousView);
paneViewRegistry.delete(paneRole);
}
const aggregate = options.aggregate || await fetchPageAggregateForPane(descriptor);
const bootstrap = buildBootstrapFromAggregate(aggregate, descriptor, paneRole);
const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap };
updatePaneChrome(runtimeDescriptor);
if (paneRole === 'secondary' && workspace instanceof HTMLElement) {
workspace.setAttribute('data-has-secondary-pane', 'true');
const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
if (resizerNode instanceof HTMLElement) resizerNode.hidden = false;
applyStoredSecondaryWidth();
}
const session = getOrCreateDocumentSession(runtimeDescriptor);
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
const mountOptions = {
documentId: session.documentId,
workspaceId: session.workspaceId,
title: session.title,
content: session.currentTiptapDocument,
revision: session.revision,
conflictDetectionKey: session.conflictDetectionKey,
readOnly: session.readOnly,
editable: !session.readOnly,
pageOptions: runtimeDescriptor.aggregate.layout?.pageOptions || {},
};
setStatus(runtimeDescriptor, 'loading-assets');
clearEmbeddedLocalDraft(runtimeDescriptor);
try {
const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
view.mountId = mountId;
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
setStatus(runtimeDescriptor, 'mounting-editor');
paneViewRegistry.set(paneRole, view);
return view;
} catch (error) {
unmountEditorViewBinding(view);
throw error;
}
};
const descriptorFromCurrentUrl = (paneRole, documentId, explicit = {}) => {
const url = currentUrl();
if (paneRole === 'secondary') {
return {
documentId,
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'convex_workspace',
rootUri: explicit.rootUri || url.searchParams.get('secondaryRootUri') || url.searchParams.get('rootUri') || '',
};
}
return {
documentId,
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'convex_workspace',
rootUri: explicit.rootUri || url.searchParams.get('rootUri') || '',
};
};
const updatePrimaryUrl = (descriptor, urlFromCaller) => {
const url = urlFromCaller instanceof URL
? urlFromCaller
: new URL(`/documents/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
if (!url.searchParams.get('workspaceId') && descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
if (descriptor.sourceKind && descriptor.sourceKind !== 'convex_workspace') url.searchParams.set('sourceKind', descriptor.sourceKind);
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
pushUrlState(url);
};
const closeSecondaryPane = (url) => {
const view = paneViewRegistry.get('secondary');
if (view) {
unmountEditorViewBinding(view);
paneViewRegistry.delete('secondary');
}
const pane = document.querySelector('[data-document-pane="true"][data-pane-role="secondary"]');
if (pane instanceof HTMLElement) {
pane.hidden = true;
pane.setAttribute('data-pane-visible', 'false');
}
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="secondary"]`);
if (root instanceof HTMLElement) root.replaceChildren();
if (workspace instanceof HTMLElement) {
workspace.setAttribute('data-has-secondary-pane', 'false');
workspace.style.removeProperty('grid-template-columns');
}
const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
if (resizerNode instanceof HTMLElement) resizerNode.hidden = true;
replaceUrlState(url);
};
const handleSessionChange = (session, view, event) => { const handleSessionChange = (session, view, event) => {
const payload = normalizeEnvelopePayload(event); const payload = normalizeEnvelopePayload(event);
if (!payload) return; if (!payload) return;
@@ -1629,12 +1829,36 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId)); runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island'); runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
setStatus(runtimeDescriptor, 'mounting-editor'); setStatus(runtimeDescriptor, 'mounting-editor');
paneViewRegistry.set(runtimeDescriptor.paneRole, view);
} catch (error) { } catch (error) {
unmountEditorViewBinding(view); unmountEditorViewBinding(view);
throw error; throw error;
} }
}; };
window.__mnoteDocumentPaneRuntime = {
openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
const id = typeof documentId === 'string' ? documentId.trim() : '';
if (!id) return false;
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
await replacePaneDocument('primary', descriptor);
updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
return true;
},
openSecondaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
const id = typeof documentId === 'string' ? documentId.trim() : '';
if (!id) return false;
const descriptor = descriptorFromCurrentUrl('secondary', id, { workspaceId, sourceKind, rootUri });
await replacePaneDocument('secondary', descriptor);
if (url instanceof URL) replaceUrlState(url);
return true;
},
closeSecondaryDocument: ({ url } = {}) => {
closeSecondaryPane(url instanceof URL ? url : currentUrl());
return true;
},
};
window.addEventListener('pagehide', () => { window.addEventListener('pagehide', () => {
Array.from(documentSessionRegistry.values()).forEach((session) => { Array.from(documentSessionRegistry.values()).forEach((session) => {
sessionViews(session).forEach((view) => { sessionViews(session).forEach((view) => {
@@ -2377,7 +2601,9 @@ mod tests {
assert!(html.contains("localFolderEventRegistry")); assert!(html.contains("localFolderEventRegistry"));
assert!(html.contains("command: 'replaceContent'")); assert!(html.contains("command: 'replaceContent'"));
assert!(html.contains("external-change-conflict")); assert!(html.contains("external-change-conflict"));
assert!(!html.contains("setInterval(() => {\n void pollLocalMarkdownExternalChange();\n }, 1200);")); assert!(!html.contains(
"setInterval(() => {\n void pollLocalMarkdownExternalChange();\n }, 1200);"
));
} }
#[tokio::test] #[tokio::test]
@@ -633,6 +633,28 @@ const SIDEBAR_TREE_JS: &str = r##"
copyWorkspaceSourceParams(targetUrl); copyWorkspaceSourceParams(targetUrl);
var url = targetUrl.pathname + targetUrl.search; var url = targetUrl.pathname + targetUrl.search;
if (mnoteNavigationInFlight === url) return; if (mnoteNavigationInFlight === url) return;
if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') {
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
documentId: nodeId,
workspaceId: workspaceId || '',
sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace',
rootUri: targetUrl.searchParams.get('rootUri') || '',
url: targetUrl,
}).then(function(){
if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
document.documentElement.removeAttribute('data-mnote-navigation-pending');
}).catch(function(error){
console.warn('mnote pane 内导航失败,将回退整页导航', error);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
});
return;
}
mnoteNavigationInFlight = url; mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true'); document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId); document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
+61 -14
View File
@@ -401,6 +401,25 @@ async function readDocumentSessionSnapshot(page) {
}); });
} }
async function readNoReloadProbe(page) {
return await page.evaluate(({ primaryRootSelector, secondaryRootSelector }) => ({
pagehideCount: Number(window.sessionStorage?.getItem("__mnoteSmokePagehideCount") || "0"),
primaryMountId: document.querySelector(primaryRootSelector)?.getAttribute("data-runtime-mount-id") || "",
secondaryMountId: document.querySelector(secondaryRootSelector)?.getAttribute("data-runtime-mount-id") || "",
}), {
primaryRootSelector: paneRootSelector("primary"),
secondaryRootSelector: paneRootSelector("secondary"),
});
}
async function waitForDocumentPath(page, documentId) {
await page.waitForFunction(
(expectedDocumentId) => decodeURIComponent(window.location.pathname).endsWith(`/documents/${expectedDocumentId}`),
documentId,
{ timeout: UI_TIMEOUT_MS },
);
}
function countRequests(requests, startIndex, predicate) { function countRequests(requests, startIndex, predicate) {
return requests.slice(startIndex).filter(predicate).length; return requests.slice(startIndex).filter(predicate).length;
} }
@@ -462,15 +481,24 @@ async function runFixturePhase(page, baseUrl, requests) {
assert.equal(fixtureSnapshot.sessions[0]?.viewCount, 2, "同文档双开时单 session 应挂两个 view"); assert.equal(fixtureSnapshot.sessions[0]?.viewCount, 2, "同文档双开时单 session 应挂两个 view");
const navigationIndex = requests.length; const navigationIndex = requests.length;
const beforeFixtureNavigation = await readNoReloadProbe(page);
assert(beforeFixtureNavigation.secondaryMountId, "fixture 导航前应已挂载 secondary editor");
await page.getByTestId("wolai-sidebar-row").filter({ hasText: "Fixture Other" }).first().click({ timeout: UI_TIMEOUT_MS }); await page.getByTestId("wolai-sidebar-row").filter({ hasText: "Fixture Other" }).first().click({ timeout: UI_TIMEOUT_MS });
await page.waitForURL((nextUrl) => decodeURIComponent(nextUrl.pathname).endsWith("/documents/doc_other"), { await waitForDocumentPath(page, "doc_other");
timeout: UI_TIMEOUT_MS,
waitUntil: "domcontentloaded",
});
await waitForDualPaneReady(page); await waitForDualPaneReady(page);
await waitForRequestCount(requests, navigationIndex, isTreeEventRequest, 1, "sidebar 导航后的 tree EventSource 建连");
await page.waitForTimeout(800); await page.waitForTimeout(800);
assert.equal(countRequests(requests, navigationIndex, isTreeEventRequest), 1, "sidebar 导航后当前页面仍应只建立一条 tree EventSource"); const afterFixtureNavigation = await readNoReloadProbe(page);
assert.equal(
afterFixtureNavigation.pagehideCount,
beforeFixtureNavigation.pagehideCount,
"fixture sidebar 导航不应触发整页 pagehide",
);
assert.equal(
afterFixtureNavigation.secondaryMountId,
beforeFixtureNavigation.secondaryMountId,
"fixture sidebar 导航不应重挂 secondary editor",
);
assert.equal(countRequests(requests, navigationIndex, isTreeEventRequest), 0, "fixture sidebar 导航不应重建 tree EventSource");
const navigatedFixtureUrl = new URL(page.url()); const navigatedFixtureUrl = new URL(page.url());
assert.equal( assert.equal(
navigatedFixtureUrl.searchParams.get("secondaryDocumentId"), navigatedFixtureUrl.searchParams.get("secondaryDocumentId"),
@@ -496,10 +524,7 @@ async function runLocalFolderPhase(page, baseUrl, requests, fixture) {
"不同文档双开时 session 应分别归属到两个 documentId", "不同文档双开时 session 应分别归属到两个 documentId",
); );
await page.getByTestId("wolai-sidebar-row").filter({ hasText: "Local Third" }).first().click({ timeout: UI_TIMEOUT_MS }); await page.getByTestId("wolai-sidebar-row").filter({ hasText: "Local Third" }).first().click({ timeout: UI_TIMEOUT_MS });
await page.waitForURL((nextUrl) => decodeURIComponent(nextUrl.pathname).endsWith(`/documents/${fixture.thirdDocumentId}`), { await waitForDocumentPath(page, fixture.thirdDocumentId);
timeout: UI_TIMEOUT_MS,
waitUntil: "domcontentloaded",
});
await waitForDualPaneReady(page); await waitForDualPaneReady(page);
const navigatedUrl = new URL(page.url()); const navigatedUrl = new URL(page.url());
assert.equal( assert.equal(
@@ -581,12 +606,12 @@ async function runLocalFolderPhase(page, baseUrl, requests, fixture) {
const savedMarkdown = fs.readFileSync(fixture.readmePath, "utf8"); const savedMarkdown = fs.readFileSync(fixture.readmePath, "utf8");
assert(savedMarkdown.includes(externalText), "外部写盘未落到本地 Markdown 文件"); assert(savedMarkdown.includes(externalText), "外部写盘未落到本地 Markdown 文件");
const closeBefore = await readNoReloadProbe(page);
await page.locator('[data-mnote-pane-close="secondary"]').first().click({ timeout: UI_TIMEOUT_MS }); await page.locator('[data-mnote-pane-close="secondary"]').first().click({ timeout: UI_TIMEOUT_MS });
await page.waitForURL((nextUrl) => !nextUrl.searchParams.has("secondaryDocumentId"), { await page.waitForFunction(() => !new URL(window.location.href).searchParams.has("secondaryDocumentId"), {}, { timeout: UI_TIMEOUT_MS });
timeout: UI_TIMEOUT_MS,
waitUntil: "commit",
});
await waitForSinglePaneReady(page); await waitForSinglePaneReady(page);
const closeAfter = await readNoReloadProbe(page);
assert.equal(closeAfter.pagehideCount, closeBefore.pagehideCount, "关闭 secondary 不应触发整页 pagehide");
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await waitForSinglePaneReady(page); await waitForSinglePaneReady(page);
const finalUrl = new URL(page.url()); const finalUrl = new URL(page.url());
@@ -631,6 +656,21 @@ async function runSinglePaneTitlePhase(page, baseUrl, fixture) {
assert.equal(afterClick.inputValue.trim(), expectedTitle, "点击标题后输入框标题不应闪成“无标题”"); assert.equal(afterClick.inputValue.trim(), expectedTitle, "点击标题后输入框标题不应闪成“无标题”");
assert.equal(afterClick.metaTitle.trim(), expectedTitle, "点击标题后页头标题不应闪成“无标题”"); assert.equal(afterClick.metaTitle.trim(), expectedTitle, "点击标题后页头标题不应闪成“无标题”");
assert.equal(afterClick.breadcrumbTitle.trim(), expectedTitle, "点击标题后 breadcrumb 标题不应闪成“无标题”"); assert.equal(afterClick.breadcrumbTitle.trim(), expectedTitle, "点击标题后 breadcrumb 标题不应闪成“无标题”");
const beforeOpenRight = await readNoReloadProbe(page);
assert(beforeOpenRight.primaryMountId, "打开 secondary 前应已挂载 primary editor");
await page.evaluate((documentId) => {
window.dispatchEvent(new CustomEvent("tree.page.open-right", { detail: { documentId } }));
}, fixture.sideDocumentId);
await page.waitForFunction(() => new URL(window.location.href).searchParams.has("secondaryDocumentId"), {}, { timeout: UI_TIMEOUT_MS });
await waitForDualPaneReady(page);
const afterOpenRight = await readNoReloadProbe(page);
assert.equal(afterOpenRight.pagehideCount, beforeOpenRight.pagehideCount, "打开 secondary 不应触发整页 pagehide");
assert.equal(afterOpenRight.primaryMountId, beforeOpenRight.primaryMountId, "打开 secondary 不应重挂 primary editor");
const openRightUrl = new URL(page.url());
assert.equal(openRightUrl.searchParams.get("secondaryDocumentId"), fixture.sideDocumentId, "打开 secondary 后 URL 应写入 secondaryDocumentId");
assert.equal(openRightUrl.searchParams.get("secondarySourceKind"), "local_folder", "打开 secondary 后 URL 应保留 secondarySourceKind");
assert.equal(openRightUrl.searchParams.get("secondaryRootUri"), fixture.rootUri, "打开 secondary 后 URL 应保留 secondaryRootUri");
} }
async function main() { async function main() {
@@ -655,6 +695,13 @@ async function main() {
const browser = await chromium.launch({ headless: true }); const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
await context.addInitScript(() => {
const key = "__mnoteSmokePagehideCount";
window.addEventListener("pagehide", () => {
const current = Number(window.sessionStorage?.getItem(key) || "0");
window.sessionStorage?.setItem(key, String(current + 1));
});
});
const page = await context.newPage(); const page = await context.newPage();
page.on("request", (request) => { page.on("request", (request) => {