/** overlay 的最小 DOM adapter:只验证节点/样式契约,不模拟浏览器布局或动画。 */ export function markerTestDocument() { class Element { constructor(tag) { this.tagName = tag this.nodeType = 1 this.children = [] this.dataset = {} this.attributes = {} this.className = '' this.listeners = new Map() this.style = { setProperty(key, value) { this[key] = value } } this.classList = { toggle: (name, enabled) => { const names = new Set(this.className.split(' ').filter(Boolean)) if (enabled) names.add(name) else names.delete(name) this.className = [...names].join(' ') } } } get parentElement() { return this.parentNode } get isConnected() { return this.connected === true || Boolean(this.parentNode?.isConnected) } appendChild(child) { child.parentNode?.removeChild(child) child.parentNode = this this.children.push(child) return child } removeChild(child) { this.children = this.children.filter((item) => item !== child) child.parentNode = null return child } remove() { this.parentNode?.removeChild(this) } set innerHTML(value) { // overlay 的水滴 SVG 占位;不解析路径、不伪造像素证据。 this.html = value this.firstChild = new Element('svg') this.firstChild.inner = new Element('image') } setAttribute(key, value) { this.attributes[key] = value } querySelector(selector) { return selector === '[data-marker-overlay-inner]' ? this.inner : null } addEventListener(type, fn) { const listeners = this.listeners.get(type) || [] this.listeners.set(type, [...listeners, fn]) } removeEventListener(type, fn) { this.listeners.set(type, (this.listeners.get(type) || []).filter((item) => item !== fn)) } contains(node) { return node === this || this.children.some((child) => child.contains(node)) } closest(selector) { return this.className.split(' ').includes(selector.slice(1)) ? this : this.parentNode?.closest(selector) } getBoundingClientRect() { return { left: 0, top: 0, width: 1000, height: 800 } } nodes() { return [this, ...this.children.flatMap((child) => child.nodes())] } fire(type, target) { for (const fn of this.listeners.get(type) || []) fn({ target, clientX: 100, clientY: 200, stopPropagation() {} }) } } const head = new Element('head') const host = new Element('main') head.connected = host.connected = true return { head, host, createElement: (tag) => new Element(tag), getElementById: (id) => head.nodes().find((node) => node.id === id) || null, } }