Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion crates/engine/src/js_dom_bridge_tests/part18.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2156,7 +2156,8 @@ fn t8q_adopt_node_materializes_dom_parser_snapshot_element() {

// t8q 邻近变体:① 快照后代元素(querySelector 产物)adopt 同样物化;② 元素物化的
// 子树保真——根元素重建时内部 Text/Comment 经 _wcRebuildAsHandle 递归物化为对应
// 类型节点(顶层裸 Text/Comment 快照直接 adopt 的物化不在本修复域,见池记录);
// 类型节点(顶层裸 Text/Comment 快照直接 adopt 的物化已由 t8s1 扩入修复域——
// npd.314 播放器崩簇根因,见下方 t8s1 钉测);
// ③ 同文档真元素 adoptNode 保持 identity 返回(R192 语义零回归)。
#[test]
fn t8q_adopt_node_snapshot_variants_and_identity_regression() {
Expand Down Expand Up @@ -2211,6 +2212,129 @@ fn t8q_adopt_node_snapshot_variants_and_identity_regression() {
);
}

// t8s1(site-compat bilibili-20261002-r1):快照顶层裸 Text/Comment adopt 物化——
// 真站 bilibili 播放器模板引擎 T.d(模块 86806)用
// `while(body.lastChild) frag.insertBefore(adoptNode(body.lastChild), frag.firstChild||null)`
// 倒序排空 DOMParser 快照 body(renderFragment HTML 带尾随空白 → body 末子是文本):
// 首轮 adopt 的快照文本(t8q 物化域外 → 无 host handle)作 ref 时,host 桥按 ref
// handle 定位失败 → 第二轮的元素静默丢失 → `.bpx-video-info` 注册表查询全 miss →
// npd.314 四处 `template.info/online` null 崩(resize/hideDm/renderParts/classList)。
// spec https://dom.spec.whatwg.org/#concept-node-adopt(adopt 完整搬入本文档)。
#[test]
fn t8s1_adopted_snapshot_text_ref_does_not_silently_drop_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::<DomMutation>::new()));
let dom_html = Arc::new(Mutex::new("<html><body></body></html>".to_string()));
let page_url = Arc::new(Mutex::new("https://zero.test/t8s1-adopt-text".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 r = [];
// T.d 倒序排空真站同构形态:div(含嵌套 online)+ 尾空白 → body [1,3]。
var doc = new DOMParser().parseFromString('<div class="bpx-video-info"><div class="bpx-video-info-online">5</div></div>\n ', 'text/html');
var frag = document.createDocumentFragment();
var guard = 0;
while (doc.body.lastChild && guard++ < 20) {
frag.insertBefore(document.adoptNode(doc.body.lastChild), frag.firstChild || null);
}
// ① 排空完成且元素在 frag(修复前:ref=无 handle 快照文本 → 元素静默丢失)。
var types = [];
for (var i = 0; i < frag.childNodes.length; i++) types.push(frag.childNodes[i].nodeType);
r.push('fragTypes:' + types.join(','));
r.push('fragInfo:' + (frag.querySelector('.bpx-video-info') ? 'hit' : 'miss'));
// ② v.W 式消费:清空容器 + append fragment → 相对查询 + style 写(npd.314 四崩点消费面)。
var bar = document.createElement('div');
bar.appendChild(document.createElement('span'));
while (bar.lastChild) bar.removeChild(bar.lastChild);
bar.appendChild(frag);
var info = bar.querySelector('.bpx-video-info');
r.push('barInfo:' + (info ? 'hit' : 'null'));
var sw = 'nostyle';
try { if (info && info.style) { info.style.width = '100px'; sw = String(info.style.width); } } catch (e) { sw = 'throw:' + e.name; }
r.push('styleW:' + sw);
r.push('online:' + (bar.querySelector('.bpx-video-info-online') ? 'hit' : 'null'));
return r.join('|');
})()"#,
)
.unwrap()
.value;
assert_eq!(
out,
"fragTypes:1,3|fragInfo:hit|barInfo:hit|styleW:100px|online:hit",
"t8s1:快照文本 adopt 物化后 T.d 倒序排空元素不再静默丢失(frag 保元素 + 注册表查询/style 消费全通)"
);
}

// t8s1 邻近变体:① adopted Comment 作 ref 同域;② 快照文本物化数据保真 + 原父摘除
//(spec concept-node-adopt 第 1 步,t8q 返修同款);③ 快照文本不 adopt 的 identity
// 消费零回归(data/textContent 读照旧);④ handle 真文本 adopt 保持 identity(R192)。
#[test]
fn t8s1_adopted_snapshot_comment_ref_and_identity_regressions() {
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::<DomMutation>::new()));
let dom_html = Arc::new(Mutex::new("<html><body></body></html>".to_string()));
let page_url = Arc::new(Mutex::new("https://zero.test/t8s1-adopt-comment".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 r = [];
// ① adopted Comment 作 ref:后续 insert 元素不丢;物化 Comment data 逐字保真。
var d1 = new DOMParser().parseFromString('<i>px</i><!--anchor-->', 'text/html');
var cf = document.createDocumentFragment();
var ac = document.adoptNode(d1.body.lastChild);
r.push('cData:' + (ac.nodeType === 8 && ac.data === 'anchor' ? 'ok' : 'bad:' + ac.nodeType + '/' + JSON.stringify(ac.data)));
cf.insertBefore(ac, cf.firstChild || null);
while (d1.body.lastChild) cf.insertBefore(document.adoptNode(d1.body.lastChild), cf.firstChild || null);
r.push('cFrag:' + (cf.querySelector('i') ? 'hit' : 'miss') + '/' + (cf.childNodes.length >= 2 ? 'both' : 'part:' + cf.childNodes.length));
// ② 快照文本物化保真:data 内容逐字保留 + 原父摘除。
var d2 = new DOMParser().parseFromString('<b>x</b>\n tail ', 'text/html');
var at = document.adoptNode(d2.body.lastChild);
r.push('matType:' + at.nodeType + '|data:' + (at.data === '\n tail ' ? 'exact' : 'bad:' + JSON.stringify(at.data)));
r.push('cut:' + (d2.body.lastChild && d2.body.lastChild.nodeType === 1 ? 'ok' : 'bad'));
// ③ 快照文本不 adopt 的 identity 消费零回归。
var d3 = new DOMParser().parseFromString('<u>plain</u>\nraw', 'text/html');
var raw = d3.body.lastChild;
r.push('rawRead:' + (raw && raw.nodeType === 3 && raw.data === '\nraw' ? 'ok' : 'bad'));
// ④ handle 真文本 adopt identity(R192 语义);物化产物(带 handle)二次 adopt 同样 identity。
var lt = document.createTextNode('live');
r.push('liveIdent:' + (document.adoptNode(lt) === lt ? 'ok' : 'bad'));
var d4 = new DOMParser().parseFromString('<s>q</s>\nagain', 'text/html');
var mat = document.adoptNode(d4.body.lastChild);
r.push('readopt:' + (document.adoptNode(mat) === mat ? 'ok' : 'bad'));
return r.join('|');
})()"#,
)
.unwrap()
.value;
assert_eq!(
out,
"cData:ok|cFrag:hit/both|matType:3|data:exact|cut:ok|rawRead:ok|liveIdent:ok|readopt:ok",
"t8s1:adopted Comment ref 同域 + 物化数据保真/原父摘除 + 快照与 handle/物化产物 identity 零回归"
);
}

// t8r(site-compat bilibili-20261002-r1,t8q-1 本地产物接口完整化):innerHTML 注入
// 本地视图产物物化。真站链:`area.innerHTML = tpl()` → `area.querySelector(".bar-normal")`
// → `bar.style.webkitTransform = …`(volume npd.586 @425567、progress npd.911 @72303 同型)——
Expand Down
12 changes: 11 additions & 1 deletion crates/engine/src/js_dom_shim/part03.js
Original file line number Diff line number Diff line change
Expand Up @@ -11941,9 +11941,19 @@
} catch (_eR202d) {}
}
function _zwMText(v, parent) { var t = String(v); var n = { nodeType: 3, nodeName: '#text', nodeValue: t, textContent: t, data: t, childNodes: [], children: [], hasChildNodes: function () { return false; }, contains: function (other) { return _zwNodeContains(n, other); }, compareDocumentPosition: function (other) { return _zwCompareDocumentPosition(n, other); }, parentNode: parent || null }; _zwMDefineSiblings(n); Object.defineProperty(n, 'firstChild', { get: function () { return null; }, configurable: true }); Object.defineProperty(n, 'lastChild', { get: function () { return null; }, configurable: true }); Object.defineProperty(n, 'parentElement', { get: function () { var p = n.parentNode; return p && p.nodeType === 1 ? p : null; }, configurable: true }); Object.defineProperty(n, 'length', { get: function () { return n.data.length; }, configurable: true }); Object.defineProperty(n, 'wholeText', { get: function () { var p = n.parentNode; if (!p || !p.childNodes) return n.data; var kids = p.childNodes, idx = -1, i2 = 0; for (i2 = 0; i2 < kids.length; i2++) { if (kids[i2] === n) { idx = i2; break; } } if (idx < 0) return n.data; var lo = idx, hi = idx; for (var l2 = idx - 1; l2 >= 0; l2--) { if (!kids[l2] || kids[l2].nodeType !== 3) break; lo = l2; } for (var h2 = idx + 1; h2 < kids.length; h2++) { if (!kids[h2] || kids[h2].nodeType !== 3) break; hi = h2; } var t2 = ''; for (var o2 = lo; o2 <= hi; o2++) { var c = kids[o2]; if (c && c.nodeType === 3) t2 += String(c.data != null ? c.data : ''); } return t2 || n.data; }, configurable: true }); /* R179:原型链 Text.prototype(spec Text : CharacterData : Node——`x instanceof Text` 断言;WPT node-creation-realm 的 innerHTML 解析子) */ try { Object.setPrototypeOf(n, (globalThis.Text && globalThis.Text.prototype) || (globalThis.Node ? globalThis.Node.prototype : Object.prototype)); } catch (_eR117t) {}
// t8s1(site-compat bilibili-20261002-r1):M 节点家族印章扩展到 Text——DOMParser
// 快照域顶层裸文本(_ensureMutTree 的 _zwMText 产物)无本印时 adoptNode 落地分支
// 不识别 → identity 返回无 host handle 的快照文本;其作 insertBefore ref 时 host
// 桥按 ref handle 定位失败静默丢节点(bilibili npd.314 播放器崩簇根因——模板
// renderFragment HTML 尾随空白经 T.d `while(body.lastChild)` 倒序排空末子为文本)。
// spec https://dom.spec.whatwg.org/#concept-node-adopt「adopt 完整搬入本文档」。
// 已知偏差面(PR #132 审查 B):盖章面含 new Text()/cloneNode 等全部 M 家族产物——
// 其同文档 adopt 由 R192 identity 变为物化副本(spec adopt-node 要求返回自身);
// 当前无内部 adoptNode 调用方、无测试/站点消费面受害;如需 identity 保真再拆独立印章。
try { Object.defineProperty(n, '__zwIsMEl', { value: true, enumerable: false }); } catch (_eMElTagT) {}
_zwAttachCharacterDataMethods(n);
return n; }
function _zwMComment(v, parent) { var t = String(v); var n = { nodeType: 8, nodeName: '#comment', nodeValue: t, textContent: t, data: t, childNodes: [], children: [], hasChildNodes: function () { return false; }, contains: function (other) { return _zwNodeContains(n, other); }, compareDocumentPosition: function (other) { return _zwCompareDocumentPosition(n, other); }, parentNode: parent || null }; _zwMDefineSiblings(n); Object.defineProperty(n, 'firstChild', { get: function () { return null; }, configurable: true }); Object.defineProperty(n, 'lastChild', { get: function () { return null; }, configurable: true }); Object.defineProperty(n, 'parentElement', { get: function () { var p = n.parentNode; return p && p.nodeType === 1 ? p : null; }, configurable: true }); Object.defineProperty(n, 'length', { get: function () { return n.data.length; }, configurable: true }); /* R179:原型链 Comment.prototype(spec Comment : CharacterData : Node——`x instanceof Comment` 断言;WPT node-creation-realm 的 cloneNode/importNode) */ try { if (globalThis.Comment && globalThis.Comment.prototype) Object.setPrototypeOf(n, globalThis.Comment.prototype); } catch (_e179n) {} _zwAttachCharacterDataMethods(n); return n; }
function _zwMComment(v, parent) { var t = String(v); var n = { nodeType: 8, nodeName: '#comment', nodeValue: t, textContent: t, data: t, childNodes: [], children: [], hasChildNodes: function () { return false; }, contains: function (other) { return _zwNodeContains(n, other); }, compareDocumentPosition: function (other) { return _zwCompareDocumentPosition(n, other); }, parentNode: parent || null }; _zwMDefineSiblings(n); Object.defineProperty(n, 'firstChild', { get: function () { return null; }, configurable: true }); Object.defineProperty(n, 'lastChild', { get: function () { return null; }, configurable: true }); Object.defineProperty(n, 'parentElement', { get: function () { var p = n.parentNode; return p && p.nodeType === 1 ? p : null; }, configurable: true }); Object.defineProperty(n, 'length', { get: function () { return n.data.length; }, configurable: true }); /* R179:原型链 Comment.prototype(spec Comment : CharacterData : Node——`x instanceof Comment` 断言;WPT node-creation-realm 的 cloneNode/importNode) */ try { if (globalThis.Comment && globalThis.Comment.prototype) Object.setPrototypeOf(n, globalThis.Comment.prototype); } catch (_e179n) {} /* t8s1:M 家族印章同 _zwMText——快照顶层裸 Comment adopt 物化同域(concept-node-adopt)。 */ try { Object.defineProperty(n, '__zwIsMEl', { value: true, enumerable: false }); } catch (_eMElTagC) {} _zwAttachCharacterDataMethods(n); return n; }
// 递归建子树:entry = {k:'E',s:sel}/{k:'T',v}/{k:'C',v}(__zw_parse_html_child_nodes)。元素取快照 + 递归子。
// R123:`<?...?>` 的 bogus comment(data '?…?'——tokenizer 在首个 '>' 结束并保留 '?' 前缀)
// 转换为 PI 视图节点(nodeType 7)——Chrome「Parse processing instructions in HTML」后
Expand Down
15 changes: 13 additions & 2 deletions crates/engine/src/js_dom_shim/part06.js
Original file line number Diff line number Diff line change
Expand Up @@ -3364,7 +3364,17 @@
throw new (globalThis.DOMException || Error)(
'Cannot adopt a Document node.', 'NotSupportedError');
}
if (node.nodeType === 1 && !node.__zwHandle && !node.__zwSelector
// t8s1(site-compat bilibili-20261002-r1):物化域扩展到快照顶层裸 Text/Comment
// ——t8q 只覆盖元素(nodeType 1);真站 bilibili 播放器模板引擎 T.d 用
// `while(body.lastChild) frag.insertBefore(adoptNode(body.lastChild), frag.firstChild||null)`
// 倒序排空(renderFragment HTML 尾随空白 → body 末子是文本):快照文本 adopt 后
// 无 host handle,插入 fragment 成功但其作 ref 时 host 桥按 ref handle 定位
// 失败 → 后续元素静默丢失 → `.bpx-video-info` 注册表查询全 miss → npd.314
// 四处 `template.info/online` null 崩。spec concept-node-adopt 要求完整搬入
// 本文档——快照 Text/Comment(_zwMText/_zwMComment 印章识别)同走物化。
// https://dom.spec.whatwg.org/#concept-node-adopt
if ((node.nodeType === 1 || node.nodeType === 3 || node.nodeType === 8)
&& !node.__zwHandle && !node.__zwSelector
&& ((typeof _zwParseEl === 'function' && node instanceof _zwParseEl)
|| node.__zwIsMEl === true)) {
try {
Expand All @@ -3374,7 +3384,8 @@
? node._ensureMutTree() : node;
if (_t8qTree) {
var _t8qMat = _wcRebuildAsHandle(_t8qTree);
if (_t8qMat && _t8qMat !== node && _t8qMat.nodeType === 1) {
if (_t8qMat && _t8qMat !== node
&& (_t8qMat.nodeType === 1 || _t8qMat.nodeType === 3 || _t8qMat.nodeType === 8)) {
// t8q 返修(PR #129 双审查②#1/#2):spec concept-node-adopt 第 1 步
// ——adopt 先从原父摘除。物化返回的是重建副本,原节点若留在原树,
// adopt 退化为复制(站点双引用消费时内容重复;快照文档
Expand Down
Loading