Files
Notes/tests/unit/heading-observer-persist.test.js
2026-10-16 16:34:15 +08:00

126 lines
4.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// P2-16(audit):heading IntersectionObserver 实例持久化。
//
// 旧实现:_rebuildHeadingObserver 每次 new IntersectionObserver(cb) ——
// loadContentLive 在 split 模式每个 debounced 击键都跑一次重建,observer
// 对象 + 闭包反复分配。
//
// 新契约:
// - observer 实例只建一次(多次 _rebuildHeadingObserver 不重复 new)
// - 重建仍 disconnect + 全量 re-observe(M1 历史教训:innerHTML 替换
// detach 旧节点,增量 observe 会让追踪冻死,不能省)
// - dispose 仍 disconnect + 置 null(Round 12 P3 契约不变)
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MarkdownViewer } from '../../src/markdown-viewer.js';
describe('heading observer 持久化(P2-16)', () => {
let body;
beforeEach(() => {
document.body.innerHTML = '';
body = document.createElement('div');
body.id = 'markdown-body';
document.body.appendChild(body);
});
function makeViewer() {
return new MarkdownViewer({
bodyElement: body,
emptyElement: document.createElement('div'),
statusPath: document.createElement('div'),
statusMeta: document.createElement('div'),
});
}
function addHeadings(...tags) {
for (const t of tags) {
const h = document.createElement(t);
h.id = `${t}-${Math.random().toString(36).slice(2, 6)}`;
h.textContent = 'title';
body.appendChild(h);
}
}
it('多次 _rebuildHeadingObserver 只 new 一次 IntersectionObserver', () => {
const constructCalls = [];
const fakeObserver = {
observe: vi.fn(),
disconnect: vi.fn(),
unobserve: vi.fn(),
};
const RealIO = window.IntersectionObserver;
window.IntersectionObserver = vi.fn(function () {
constructCalls.push(true);
return fakeObserver;
});
try {
const viewer = makeViewer();
addHeadings('h1', 'h2');
viewer._rebuildHeadingObserver();
viewer._rebuildHeadingObserver();
viewer._rebuildHeadingObserver();
expect(constructCalls.length).toBe(1);
// 每次重建都 disconnect + 全量 re-observe(detach 节点不能增量);
// 第一次重建时 observer 尚不存在、无可 disconnect,故 3 次重建 = 2 次 disconnect
expect(fakeObserver.disconnect.mock.calls.length).toBe(2);
expect(fakeObserver.observe.mock.calls.length).toBe(6); // 2 headings × 3 次
viewer.dispose();
} finally {
window.IntersectionObserver = RealIO;
}
});
it('重建后观察的是新 DOM 节点,不是 detach 的旧节点', () => {
const observed = [];
const fakeObserver = {
observe: vi.fn(function (node) { observed.push(node); }),
disconnect: vi.fn(),
unobserve: vi.fn(),
};
const RealIO = window.IntersectionObserver;
window.IntersectionObserver = vi.fn(function () { return fakeObserver; });
try {
const viewer = makeViewer();
addHeadings('h1');
viewer._rebuildHeadingObserver();
const firstNode = observed[0];
// 模拟 innerHTML 全量替换:旧节点 detach,新节点进 DOM
body.innerHTML = '';
observed.length = 0;
addHeadings('h1');
viewer._rebuildHeadingObserver();
const secondNode = observed[0];
expect(secondNode).not.toBe(firstNode);
expect(secondNode.isConnected).toBe(true);
expect(firstNode.isConnected).toBe(false);
viewer.dispose();
} finally {
window.IntersectionObserver = RealIO;
}
});
it('dispose 仍 disconnect 一次并置 null(Round 12 P3 契约不变)', () => {
const fakeObserver = {
observe: vi.fn(),
disconnect: vi.fn(),
unobserve: vi.fn(),
};
const RealIO = window.IntersectionObserver;
window.IntersectionObserver = vi.fn(function () { return fakeObserver; });
try {
const viewer = makeViewer();
addHeadings('h1');
viewer._rebuildHeadingObserver();
viewer.dispose();
expect(fakeObserver.disconnect.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(viewer._headingObserver).toBe(null);
} finally {
window.IntersectionObserver = RealIO;
}
});
});