update
This commit is contained in:
125
tests/unit/heading-observer-persist.test.js
Normal file
125
tests/unit/heading-observer-persist.test.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user