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
196 changes: 196 additions & 0 deletions crates/engine/src/js_dom_bridge_tests/part18.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,3 +2210,199 @@ fn t8q_adopt_node_snapshot_variants_and_identity_regression() {
"t8q:子树文本/注释保真 + 快照后代物化 + 真元素 identity 与 Document 抛错零回归"
);
}

// 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 同型)——
// innerHTML setter 的 handle 路径(detached createElement 容器)把 _zwFragmentAdded 的
// _zwMEl 解析代理存入 _handleChildren,相对查询返回 truthy 节点但无 .style 接口 → TypeError
// 脚本中断。spec https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-innerhtml
// ——innerHTML 产物应与 createElement 产物同体系(完整元素接口)。
#[test]
fn t8r_inner_html_local_view_materializes_query_artifacts() {
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><div id='host'></div></body></html>".to_string()));
let page_url = Arc::new(Mutex::new("https://zero.test/t8r-innerhtml".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 area = document.createElement('div');
area.innerHTML = '<div class="bar-normal"><div class="bar-fill"></div></div><span class="tip"></span>';
var bar = area.querySelector('.bar-normal');
if (!bar) return 'setup:no-bar';
var r = [];
// ① 元素接口:查询产物带 .style(真站 TypeError 根面)。
r.push('style:' + typeof bar.style);
r.push('styleSet:' + typeof (bar.style && bar.style.setProperty));
var sw = 'ok';
try { bar.style.setProperty('opacity', '0.5'); } catch (e) { sw = 'throw:' + e.name; }
r.push('styleWrite:' + sw);
r.push('styleRead:' + (bar.style ? bar.style.getPropertyValue('opacity') : 'nostyle'));
// ② 结构与属性:tagName/className(物化重建保真)。
r.push('tag:' + bar.tagName);
r.push('class:' + (bar.className === 'bar-normal' ? 'ok' : 'bad'));
// ③ 树内深查:物化子树的相对查询产物同为完整元素(递归物化)。
var fill = bar.querySelector('.bar-fill');
r.push('nested:' + (fill && typeof fill.style === 'object' ? 'ok' : 'bad'));
// ④ 多顶层子:第二个顶层子同样物化。
var tip = area.querySelector('.tip');
r.push('tip:' + (tip && typeof tip.style === 'object' ? 'ok' : 'bad'));
// ⑤ parentNode 反链(append 家族同款):查询产物上行到宿主容器。
r.push('parent:' + (bar.parentNode === area ? 'ok' : 'bad'));
return r.join('|');
})()"#,
)
.unwrap()
.value;
assert_eq!(
out,
"style:object|styleSet:function|styleWrite:ok|styleRead:0.5|tag:DIV|class:ok|nested:ok|tip:ok|parent:ok",
"t8r:innerHTML 本地视图产物物化(style 接口 + 写读回 + 深查 + 多顶层子 + parentNode 反链)"
);
}

// t8r 邻近变体:① 纯文本 innerHTML 分支零回归(R34xx 文本注册面);② 清空语义;
// ③ 二次 innerHTML 重物化(旧物化树被替换、新查询产物仍全接口);④ 混合容器——
// innerHTML 物化成员与 appendChild wrapper 成员共存(_handleChildren 混合类型契约);
// ⑤ 物化树 introspection 护栏(属性/结构保真——_zwMEl 代理时代已有的消费面不变坏);
// ⑥ 混合 markup+文本顶层注入(审查①#1:顶层文本子保持代理、元素子物化);⑦ 物化
// 成员跨查询 identity 稳定(审查①#2,对照 ④ 的 append 成员断言)。
#[test]
fn t8r_inner_html_local_view_variants_and_text_path_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::<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/t8r-innerhtml-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 r = [];
// ① 纯文本 innerHTML:firstChild 文本节点路径(不物化分支)零回归。
var t = document.createElement('div');
t.innerHTML = 'plain text';
r.push('text:' + (t.firstChild && t.firstChild.nodeType === 3 && t.firstChild.data === 'plain text' ? 'ok' : 'bad'));
// ② 清空:innerHTML='' 后无子。
t.innerHTML = '';
r.push('clear:' + (t.firstChild ? 'bad' : 'ok'));
// ③ 二次 innerHTML:重物化替换,新查询产物仍全接口。
var d2 = document.createElement('div');
d2.innerHTML = '<b class="a">x</b>';
var first = d2.querySelector('.a');
d2.innerHTML = '<i class="b">y</i>';
var second = d2.querySelector('.b');
r.push('reIh:' + (first && second && typeof second.style === 'object' && second.tagName === 'I' ? 'ok' : 'bad'));
// ④ 混合容器:物化成员 + appendChild wrapper 成员共存(结构计数 + append 成员接口)。
var d3 = document.createElement('div');
d3.innerHTML = '<em class="m">z</em>';
var app = document.createElement('strong');
d3.appendChild(app);
r.push('mix:' + (d3.querySelectorAll('*').length === 2 && d3.querySelector('strong') === app ? 'ok' : 'bad'));
// ⑤ introspection 护栏:物化树属性保真 + 结构计数(_zwMEl 代理时代已有消费面)。
var d4 = document.createElement('div');
d4.innerHTML = '<div data-k="v" class="w"><p class="p1">1</p><p class="p2">2</p></div>';
var w = d4.querySelector('.w');
r.push('deep:' + (w && w.querySelectorAll('p').length === 2 && w.getAttribute('data-k') === 'v' ? 'ok' : 'bad'));
// ⑥ 混合 markup+文本顶层注入:顶层元素子物化,尾文本子保持解析代理(.data 可读),
// 元素子接口完整(审查①#1 缺口护栏)。
var d5 = document.createElement('div');
d5.innerHTML = '<b class="h">x</b>tail';
var hb = d5.querySelector('.h');
var tl = d5.childNodes[1];
r.push('mixText:' + (hb && typeof hb.style === 'object' && hb.firstChild.data === 'x' &&
tl && tl.nodeType === 3 && tl.data === 'tail' && d5.childNodes.length === 2 ? 'ok' : 'bad'));
// ⑦ 物化成员跨查询 identity 稳定(对照 appendChild 成员 ④ 的 === app 断言;审查①#2)。
var q1 = d5.querySelector('.h');
var q2 = d5.querySelector('.h');
r.push('ident:' + (q1 && q1 === q2 && q1 === d5.firstElementChild ? 'ok' : 'bad'));
return r.join('|');
})()"#,
)
.unwrap()
.value;
assert_eq!(
out,
"text:ok|clear:ok|reIh:ok|mix:ok|deep:ok|mixText:ok|ident:ok",
"t8r:纯文本/清空分支零回归 + 重物化 + 混合容器共存 + introspection 保真 + 混合 markup/文本顶层注入 + 物化成员 identity"
);
}

// t8r 返修(审查② N1):innerHTML 物化与 `__zwCeAttachForAdded` 双轨——物化经
// createElement 已对 registry 命中的 CE 跑过 ctor,attach 再对原解析代理整表 upgrade
//(_ceUpgradeNode 无幂等门)→ ctor ×2、connectedCallback 落孤儿代理。修复后回退代理
// 才走 upgrade、成功 wrapper 只补连接态派发:ctor 恒 1 次,查询产物是真实升级实例。
#[test]
fn t8r_inner_html_materialized_custom_element_ctor_once() {
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/t8r-ce-once".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 made = 0;
class T8rCe extends HTMLElement {
constructor() { super(); made++; }
}
customElements.define('t8r-ce', T8rCe);
var c = document.createElement('div');
c.innerHTML = '<t8r-ce class="ce1"></t8r-ce>';
var q = c.querySelector('t8r-ce');
var r = [];
// ctor 恰 1 次(createElement 物化时构造;attach 不再对丢弃代理重复 upgrade)。
r.push('ctor:' + made);
// 查询产物仍是真实升级实例(接口完整 + instanceof 命中)。
r.push('iface:' + (q && typeof q.style === 'object' ? 'ok' : 'bad'));
r.push('isInst:' + (q instanceof T8rCe ? 'ok' : 'bad'));
// createElement 基线路径不受扰(ctor 恒 1)。
var made2 = 0;
class T8rCe2 extends HTMLElement {
constructor() { super(); made2++; }
}
customElements.define('t8r-ce2', T8rCe2);
var solo = document.createElement('t8r-ce2');
r.push('solo:' + (made2 === 1 && solo instanceof T8rCe2 ? 'ok' : 'bad'));
return r.join('|');
})()"#,
)
.unwrap()
.value;
assert_eq!(
out,
"ctor:1|iface:ok|isInst:ok|solo:ok",
"t8r N1:innerHTML 物化的自定义元素 ctor 恰 1 次,查询产物为真实升级实例,createElement 基线不受扰"
);
}
76 changes: 74 additions & 2 deletions crates/engine/src/js_dom_shim/part04.js
Original file line number Diff line number Diff line change
Expand Up @@ -3507,6 +3507,21 @@
} else {
r148Related = r148OldMEl;
}
} else if (oldProxy && oldProxy.__zwHandle) {
// t8r:旧焦点为 handle/proxy 域——innerHTML 产物物化后 shadow 子是真
// handle(解析节点臂不再命中),retarget 语义须同源:沿父反链上行判
// shadow 归属,命中 _shadowHandleMeta → 以 host 为 relatedTarget(spec
// UI Events shadow DOM retargeting,同上)。非 shadow 的 handle 焦点
// 上行 miss → r148Related 保持 null,rtForNew 回落 oldProxy(既有语义)。
// 焦点迁移是交互级频率(非热路径),guard 32 足够浅。
var _r148Up = oldProxy.__zwHandle;
var _r148Guard = 0;
while (_r148Up != null && _r148Guard++ < 32) {
var _r148Meta = (typeof _shadowHandleMeta !== 'undefined') ? _shadowHandleMeta[_r148Up] : null;
if (_r148Meta) { r148Related = _makeProxy(_r148Meta.hostSel, _r148Meta.hostHandle); break; }
var _r148Link = (typeof _zwNodeParent !== 'undefined' && _zwNodeParent) ? _zwNodeParent[_r148Up] : null;
_r148Up = (_r148Link && _r148Link.parentHandle != null) ? _r148Link.parentHandle : null;
}
}
// uievents-compat M3 尾簇 6b(2026-10-04):失焦相位**先于**获焦相位且
// blur 先于 focusout(WPT focus-events expected「blur@a → focusout@a →
Expand Down Expand Up @@ -9594,7 +9609,41 @@
// R100:纯文本 innerHTML 且 handle 容器——下方 _zwRegisterTextEl 已建本地文本
// 视图,此处不再同时入 registry(R81 textContent getter 融合两源会双计文本,
// e2e 'Hello WorldHello World' 实证)。含 markup 时保持 registry(结构子树)。
if (handle && !(_ihVal.indexOf('<') < 0)) _handleChildren[handle] = _ihAdded;
if (handle && !(_ihVal.indexOf('<') < 0)) {
// t8r:注入产物物化为真 handle 树——_zwMEl 解析代理无 .style/classList
// 等元素接口,相对查询(_handleChildren 扫描)返回代理后站点脚本读 .style
// 抛 TypeError(bilibili nano 播放器 volume/progress 两簇:area.innerHTML
// 注入 → elements.bar 相对查询产物无接口)。复用 t8q 重建设施
// [_wcRebuildAsHandle]:顶层元素子重建为 detached 真 handle 树(属性/
// 深层子递归),与 createElement+appendChild 路径成员类型同构(appendChild
// push wrapper、R380 存 wrapper——_handleChildren 混合容器两种皆合法)。
// 文本/注释子保持原样(cloneNode 复制分支读 .data 兼容)。物化失败回退
// 代理原样(_wcRebuildAsHandle catch 返原 node)。_zwNodeParent 反链的
// 承重面是焦点注册/retargeting 上行链(R148 handle 域臂沿它上行判 shadow
// 归属)与事件冒泡;parentNode 读另有 _parentNodeFor 的 R155
// _handleChildren 反查 fallback 兜底。R3031 MO addedNodes 仍持 setter 时
// 快照的代理原件(既有钉测纯 introspect;identity 双表示 documented)。
// 已知限制:物化 wrapper 是
// detached 新建 handle,与宿主树 innerHTML 产物 handle 不同源——对查询
// 产物的样式/属性写不穿透渲染(本切片消「.style 抛 TypeError 脚本中断」面)。
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-innerhtml
var _ihMat = [];
var _ihFallback = [];
for (var _ihMi = 0; _ihMi < _ihAdded.length; _ihMi++) {
var _ihMk = _ihAdded[_ihMi];
var _ihMw = (_ihMk && _ihMk.nodeType === 1) ? _wcRebuildAsHandle(_ihMk) : _ihMk;
if (_ihMw && _ihMw !== _ihMk && _ihMw.__zwHandle && _zwNodeParent) {
_zwNodeParent[_ihMw.__zwHandle] = { parentSel: null, parentHandle: handle, nextSibling: null };
}
// t8r 返修(N1):物化失败的顶层元素子(_wcRebuildAsHandle catch 返原
// node,_ihMw === _ihMk)未经 createElement 构造,仍需 CE upgrade——
// 下方 attach 分流时交给 __zwCeAttachForAdded;成功 wrapper 已由
// createElement ctor(registry 命中即构造),不得再 upgrade。
if (_ihMk && _ihMk.nodeType === 1 && _ihMw === _ihMk) _ihFallback.push(_ihMk);
_ihMat.push(_ihMw);
}
_handleChildren[handle] = _ihMat;
}
else if (handle && _ihVal.indexOf('<') < 0) _handleChildren[handle] = [];
// WC-M3 切片 8 第九增量:slotchange 队列钩子**下移**到本地文本视图重注册
// (下方 _zwRegisterTextEl/_zwUnregisterTextEl)之后——queue 时 diff 读的
Expand Down Expand Up @@ -9670,7 +9719,30 @@
// 'innerHTML on ...' inserting-markup 簇 + builtin-coverage 解析实例化面)。
if (typeof globalThis.__zwCeAttachForAdded === 'function') {
try {
globalThis.__zwCeAttachForAdded(_ihAdded, _ceParentConnected(sel, handle));
if (_ihMat) {
// t8r 返修(N1):物化生效时顶层元素子已经 createElement 构造
//(registry 命中的 CE ctor 在 createElement 内跑过一次),不能再对
// 原解析代理整表 upgrade——_ceUpgradeNode 无幂等门(_ceRunCtor 无条件
// 执行),重复 ctor 副作用翻倍,且 connectedCallback 会派发到被丢弃
// 的孤儿代理上。改为:物化失败回退的代理(未构造)仍走 upgrade;
// 成功 wrapper 只补连接态派发(同 __zwCeAttachForAdded 的 pc 双源
// 判定 + _ceApplyConn 递归派发,无 ctor)。
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-innerhtml
var _ihPc = _ceParentConnected(sel, handle);
if (_ihFallback.length) globalThis.__zwCeAttachForAdded(_ihFallback, _ihPc);
for (var _ihCi = 0; _ihCi < _ihMat.length; _ihCi++) {
var _ihCw = _ihMat[_ihCi];
if (_ihCw && _ihCw.__zwHandle) {
var _ihCpc = _ihPc === true;
if (!_ihCpc) { try { _ihCpc = _elConnected(_ihCw); } catch (_eIhCp) { _ihCpc = _ihCpc; } }
if (_ihCpc && typeof _ceApplyConn === 'function') {
try { _ceApplyConn(_ihCw, true); } catch (_eIhCc) {}
}
}
}
} else {
globalThis.__zwCeAttachForAdded(_ihAdded, _ceParentConnected(sel, handle));
}
} catch (_eCeIh) {}
}
// R304(js-dom M4):innerHTML 解析 wrapper 打挂父槽(sel 容器的同 turn
Expand Down
Loading