diff --git a/crates/engine/src/js_dom_bridge_tests/part18.rs b/crates/engine/src/js_dom_bridge_tests/part18.rs index ca73959f50..302d21747d 100644 --- a/crates/engine/src/js_dom_bridge_tests/part18.rs +++ b/crates/engine/src/js_dom_bridge_tests/part18.rs @@ -2095,3 +2095,118 @@ fn test_synthetic_click_pre_activation_r108() { "R108 合成 click pre-activation 核心三面(全六语义由 WPT Event-dispatch-click 31P 守)" ); } + +// t8q(site-compat bilibili-20261002-r1 P7):DOMParser 解析快照 adoptNode 落地。 +// 真站链:tr.t = `document.adoptNode(new DOMParser().parseFromString(tpl, 'text/html').body.firstChild)` +// ——快照产物(_zwParseEl)此前 adopt 恒 identity no-op:无 .style(widget 样式操作 +// `nodes.X.style.foo` 全崩)、append 后序列化为空 `
`、不进文档查询。 +// spec https://dom.spec.whatwg.org/#concept-node-adopt —— adopt 把节点完整搬入本文档。 +#[test] +fn t8q_adopt_node_materializes_dom_parser_snapshot_element() { + use std::sync::{Arc, Mutex}; + use zero_script_sandbox::{Sandbox, V8Sandbox}; + + let mut sandbox = V8Sandbox::with_config(zero_script_sandbox::SandboxConfig { + persistent_context: true, + ..Default::default() + }) + .unwrap(); + sandbox.execute(generate_js_dom_shim()).unwrap(); + let mutations = Arc::new(Mutex::new(Vec::::new())); + let dom_html = Arc::new(Mutex::new("
".to_string())); + let page_url = Arc::new(Mutex::new("https://zero.test/t8q-adopt".to_string())); + let canvas_registry = Arc::new(Mutex::new(crate::js_dom_bridge::CanvasRegistry::new())); + register_dom_callbacks(&mut sandbox, &mutations, &dom_html, &page_url, &canvas_registry, None); + + let out = sandbox + .execute( + r#"(function(){ + var tpl = '
'; + var doc = new DOMParser().parseFromString(tpl, 'text/html'); + var snap = doc.body.firstChild; + if (!snap || snap.nodeType !== 1) return 'setup:no-first-child'; + var adopted = document.adoptNode(snap); + var r = []; + // ① 物化:adopt 产物带 .style 接口(快照对象无 style——真站 7 处 TypeError 根面)。 + r.push('style:' + typeof adopted.style); + r.push('styleSet:' + typeof (adopted.style && adopted.style.setProperty)); + // ② 原父摘除(spec concept-node-adopt 第 1 步):物化返回副本后,原节点已从 + // 快照文档树摘除——body.firstChild 不再返回它(PR #129 审查②:不摘则 adopt + // 退化为复制,双引用消费时内容重复)。 + r.push('snapGone:' + (doc.body.firstChild ? 'bad' : 'ok')); + // ③ 结构保真:tagName/子树查询(真站 nodes 注册表同款相对查询)。 + r.push('tag:' + adopted.tagName); + r.push('relQ:' + (adopted.querySelector('.bpx-player-video-wrap') ? 'hit' : 'miss')); + // ④ append 进文档:父相对查询 + 序列化可见(快照 append 后为空
)。 + var host = document.getElementById('host'); + host.appendChild(adopted); + r.push('parentQ:' + (host.querySelector('.bpx-player-video-wrap') ? 'hit' : 'miss')); + r.push('serial:' + (host.innerHTML.indexOf('bpx-player-video-wrap') >= 0 ? 'hit' : 'miss')); + return r.join('|'); +})()"#, + ) + .unwrap() + .value; + assert_eq!( + out, + "style:object|styleSet:function|snapGone:ok|tag:DIV|relQ:hit|parentQ:hit|serial:hit", + "t8q:DOMParser 快照 adoptNode 物化落地(style 接口 + 原父摘除 + 相对查询 + append 后查询/序列化)" + ); +} + +// t8q 邻近变体:① 快照后代元素(querySelector 产物)adopt 同样物化;② 元素物化的 +// 子树保真——根元素重建时内部 Text/Comment 经 _wcRebuildAsHandle 递归物化为对应 +// 类型节点(顶层裸 Text/Comment 快照直接 adopt 的物化不在本修复域,见池记录); +// ③ 同文档真元素 adoptNode 保持 identity 返回(R192 语义零回归)。 +#[test] +fn t8q_adopt_node_snapshot_variants_and_identity_regression() { + use std::sync::{Arc, Mutex}; + use zero_script_sandbox::{Sandbox, V8Sandbox}; + + let mut sandbox = V8Sandbox::with_config(zero_script_sandbox::SandboxConfig { + persistent_context: true, + ..Default::default() + }) + .unwrap(); + sandbox.execute(generate_js_dom_shim()).unwrap(); + let mutations = Arc::new(Mutex::new(Vec::::new())); + let dom_html = Arc::new(Mutex::new("".to_string())); + let page_url = Arc::new(Mutex::new("https://zero.test/t8q-adopt-var".to_string())); + let canvas_registry = Arc::new(Mutex::new(crate::js_dom_bridge::CanvasRegistry::new())); + register_dom_callbacks(&mut sandbox, &mutations, &dom_html, &page_url, &canvas_registry, None); + + let out = sandbox + .execute( + r#"(function(){ + var doc = new DOMParser().parseFromString('', 'text/html'); + var root = doc.body.firstChild; + var r = []; + // ① 元素物化的子树保真:li 内文本 + ul 内注释随根物化(createTextNode/createComment)。 + // (先 adopt 根——adopt 有摘除副作用,后代臂用独立文档,避免序列互相污染。) + var ul = document.adoptNode(root); + var liText = ul.firstChild && ul.firstChild.firstChild; + var ulCmt = ul.childNodes[1]; + r.push('text:' + (liText && liText.nodeType === 3 && liText.data === 't' ? 'ok' : 'bad')); + r.push('comment:' + (ulCmt && ulCmt.nodeType === 8 && ulCmt.data === ' lc ' ? 'ok' : 'bad')); + // ② 快照后代(querySelector 产物)adopt 物化 + 属性保真(独立文档)。 + var doc2 = new DOMParser().parseFromString('', 'text/html'); + var li = document.adoptNode(doc2.body.firstChild.querySelector('li')); + r.push('li:' + (li && li.tagName === 'LI' && typeof li.style === 'object' && li.className === 'i' ? 'ok' : 'bad')); + // ③ 同文档真元素 adopt 仍 identity(R192)。 + var real = document.createElement('div'); + r.push('identity:' + (document.adoptNode(real) === real ? 'ok' : 'bad')); + // ④ Document adopt 仍抛 NotSupportedError(R192;审查①:断言异常类型防退化)。 + var threw = ''; + try { document.adoptNode(new DOMParser().parseFromString('', 'text/html')); } catch (e) { threw = e.name; } + r.push('docThrow:' + (threw === 'NotSupportedError' ? 'ok' : 'bad:' + threw)); + return r.join('|'); +})()"#, + ) + .unwrap() + .value; + assert_eq!( + out, + "text:ok|comment:ok|li:ok|identity:ok|docThrow:ok", + "t8q:子树文本/注释保真 + 快照后代物化 + 真元素 identity 与 Document 抛错零回归" + ); +} diff --git a/crates/engine/src/js_dom_shim/part03.js b/crates/engine/src/js_dom_shim/part03.js index 7cf8bc00ef..3e13f6cd1c 100644 --- a/crates/engine/src/js_dom_shim/part03.js +++ b/crates/engine/src/js_dom_shim/part03.js @@ -9286,6 +9286,11 @@ childNodes: [], parentNode: parent || null }; + // t8q(site-compat):本地产物标记(non-enumerable 防 for-in/序列化扩散)—— + // _zwMEl 是工厂返回的 plain 对象(无共享 prototype 可 instanceof);DOMParser + // 文档 body.firstChild 经 _ensureMutTree 返回的就是本形态节点,adoptNode + // 落地分支(part06)以 `__zwIsMEl === true` 识别物化对象。 + try { Object.defineProperty(node, '__zwIsMEl', { value: true, enumerable: false }); } catch (_eMElTag) {} // R167(js-dom M1 L2-d3b):ownerDocument accessor——查询产物归一到 mutTree 真实 // 节点后,消费面(WPT Element-matches 的 runSpecialMatchesTests // `element.ownerDocument.defaultView.TypeError`)经此读文档。归建树设 diff --git a/crates/engine/src/js_dom_shim/part06.js b/crates/engine/src/js_dom_shim/part06.js index 14c1f11f7b..d45c5335d5 100644 --- a/crates/engine/src/js_dom_shim/part06.js +++ b/crates/engine/src/js_dom_shim/part06.js @@ -3324,12 +3324,53 @@ //(spec concept-node-adopt 前置——WPT "Explicitly adopting a DocumentType" 断言 // adopt 后 parentNode null);③ 子树 ownerDocument 重指本文档(handle 注册表 + // plain defineProperty,与 R191 appendChild adopt 同款);④ 返回节点自身。 + // t8q(site-compat bilibili-20261002-r1 P7):**DOMParser 快照元素落地**—— + // 快照产物(_zwParseEl,R2790 查询工厂)无 __zwHandle/__zwSelector 身份,旧 + // R192 第 3 分支只补 ownerDocument 后原样返回:append 进文档后 ① 无 `.style` + //(站点模板引擎 tr.t = parseFromString + adoptNode 链的样式操作全 TypeError + // ——bilibili nano 播放器 7 处崩溃实证);② 序列化为空壳 `
`; + // ③ 不进文档查询。spec https://dom.spec.whatwg.org/#concept-node-adopt 要求 + // adopt 把节点完整搬入本文档——此处经 R3019 lazy 可变子树桥(_ensureMutTree) + // + `_wcRebuildAsHandle`(WC-M2 既有重建器)物化为 handle 树(真 wrapper: + // 同回合完整接口,mutation 队列异步 apply 与页面 createElement 同时序)。 + // 识别 _zwParseEl 实例(R2790 查询工厂)与本地可变树产物(_zwMEl 印章 + // `__zwIsMEl`——工厂产物是 plain 对象无共享 prototype 可 instanceof,覆盖 + // R3019 懒桥树与 detached 归建树两类解析域产物;主文档 handle 元素带 + // `__zwHandle` 身份不进本分支)。物化只发生在 adopt 时刻,物化成功即从 + // 原父摘除(concept-node-adopt 第 1 步);真 wrapper 走下方既有分支零变化。 adoptNode: function(node) { if (!node || typeof node !== 'object') return node; if ((node.nodeType | 0) === 9) { throw new (globalThis.DOMException || Error)( 'Cannot adopt a Document node.', 'NotSupportedError'); } + if (node.nodeType === 1 && !node.__zwHandle && !node.__zwSelector + && ((typeof _zwParseEl === 'function' && node instanceof _zwParseEl) + || node.__zwIsMEl === true)) { + try { + // 查询产物(_zwParseEl)经 R3019 lazy 桥转 live 可变树(_zwMEl);本地 + // 可变树产物(_zwMEl,如 doc.body.firstChild)直接物化。 + var _t8qTree = (typeof node._ensureMutTree === 'function' && node instanceof _zwParseEl) + ? node._ensureMutTree() : node; + if (_t8qTree) { + var _t8qMat = _wcRebuildAsHandle(_t8qTree); + if (_t8qMat && _t8qMat !== node && _t8qMat.nodeType === 1) { + // t8q 返修(PR #129 双审查②#1/#2):spec concept-node-adopt 第 1 步 + // ——adopt 先从原父摘除。物化返回的是重建副本,原节点若留在原树, + // adopt 退化为复制(站点双引用消费时内容重复;快照文档 + // body.firstChild 仍返原节点)。经原父 removeChild 摘除(_zwParseEl + // R3019 桥与 _zwMEl 本地树同一语义面);失败不阻断物化(残留回旧 + // 状态——detached 弃置文档域,无实际消费面)。 + try { + var _t8qP = node.parentNode; + if (_t8qP && typeof _t8qP.removeChild === 'function') _t8qP.removeChild(node); + } catch (_eT8qCut) {} + return _t8qMat; + } + } + } catch (_eT8qMat) {} + // 物化不可行 → 落入下方既有分支(identity 返回兜底)。 + } try { if (node.parentNode && typeof node.parentNode.removeChild === 'function') { node.parentNode.removeChild(node);