mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
297 lines
10 KiB
Rust
297 lines
10 KiB
Rust
//! EventTarget 验收测试:addEventListener / removeEventListener / dispatchEvent
|
||
//! 三方法、原型链(Worker / MessagePort / BroadcastChannel 经 EventTarget.prototype)、
|
||
//! GC 剪枝。
|
||
//!
|
||
//! 集成测试走公共 API(`eval` 返回脚本完成值字符串),不访问私有字段。
|
||
//! 目标对象一律用 MessagePort(EventTarget 实例)——普通对象原型链上没有
|
||
//! EventTarget.prototype,DOM 语义下 addEventListener 也只存在于 EventTarget 实例。
|
||
|
||
use std::sync::Arc;
|
||
|
||
use oxide_compiler::compiler::Compiler;
|
||
use oxide_compiler::DefaultCompilerService;
|
||
use oxide_parser::Allocator;
|
||
use oxide_vm::vm::Vm;
|
||
|
||
/// 编译并运行脚本,返回完成值字符串(解析 / 编译 / 运行失败时返回错误描述)。
|
||
fn eval(source: &str) -> String {
|
||
let allocator = Allocator::default();
|
||
let program = match oxide_parser::parse(&allocator, source) {
|
||
Ok(p) => p,
|
||
Err(e) => return format!("parse error: {}", e[0].message),
|
||
};
|
||
let module = match Compiler::new().compile(&program) {
|
||
Ok(m) => m,
|
||
Err(e) => return format!("compile error: {e}"),
|
||
};
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
match vm.run(&Arc::new(module)) {
|
||
// 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。
|
||
Ok(result) => vm.lookup_str(result).unwrap_or_else(|| format!("{result}")),
|
||
Err(e) => format!("vm error: {e}"),
|
||
}
|
||
}
|
||
|
||
/// 建一个注入编译服务的 VM 并运行首段脚本(供 GC 测试中途收集)。
|
||
fn vm_with_script(source: &str) -> Vm {
|
||
let allocator = Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, source).expect("parse 应成功");
|
||
let module = Compiler::new().compile(&program).expect("compile 应成功");
|
||
let mut vm = Vm::new();
|
||
vm.set_compiler_service(Arc::new(DefaultCompilerService));
|
||
vm.run(&Arc::new(module)).expect("run 应成功");
|
||
vm
|
||
}
|
||
|
||
/// 在既有 VM 上运行一段脚本,返回完成值字符串。
|
||
fn vm_eval(vm: &mut Vm, source: &str) -> String {
|
||
let allocator = Allocator::default();
|
||
let program = oxide_parser::parse(&allocator, source).expect("parse 应成功");
|
||
let module = Compiler::new().compile(&program).expect("compile 应成功");
|
||
match vm.run(&Arc::new(module)) {
|
||
// 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。
|
||
Ok(result) => vm.lookup_str(result).unwrap_or_else(|| format!("{result}")),
|
||
Err(e) => format!("vm error: {e}"),
|
||
}
|
||
}
|
||
|
||
/// 建一个 MessagePort 目标的前缀(每个测试目标独立,避免跨测试串扰)。
|
||
const TARGET: &str = "var mc = new MessageChannel(); var t = mc.port1;";
|
||
|
||
/// dispatchEvent(new Event('x')) 返回 true(无 preventDefault)。
|
||
#[test]
|
||
fn dispatch_event_returns_true() {
|
||
assert_eq!(eval(&format!("{TARGET} t.dispatchEvent(new Event('x'));")), "true");
|
||
}
|
||
|
||
/// preventDefault() 后 dispatchEvent 返回 false(cancelable 事件)。
|
||
#[test]
|
||
fn dispatch_event_returns_false_after_prevent_default() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} t.addEventListener('x', function(e) {{ e.preventDefault(); }}); \
|
||
t.dispatchEvent(new Event('x', {{ cancelable: true }}));"
|
||
)),
|
||
"false"
|
||
);
|
||
}
|
||
|
||
/// 非 cancelable 事件 preventDefault 无效,dispatchEvent 仍返回 true。
|
||
#[test]
|
||
fn dispatch_event_non_cancelable_stays_true() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} t.addEventListener('x', function(e) {{ e.preventDefault(); }}); \
|
||
t.dispatchEvent(new Event('x'));"
|
||
)),
|
||
"true"
|
||
);
|
||
}
|
||
|
||
/// removeEventListener 生效:移除后派发不再调用监听器。
|
||
#[test]
|
||
fn remove_event_listener_works() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} var calls = 0; \
|
||
var fn = function(e) {{ calls++; }}; \
|
||
t.addEventListener('x', fn); \
|
||
t.removeEventListener('x', fn); \
|
||
t.dispatchEvent(new Event('x')); \
|
||
calls;"
|
||
)),
|
||
"0"
|
||
);
|
||
}
|
||
|
||
/// 重复登记(同 type + callback + capture)为 no-op:派发只调用一次。
|
||
#[test]
|
||
fn duplicate_add_event_listener_is_noop() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} var calls = 0; \
|
||
var fn = function(e) {{ calls++; }}; \
|
||
t.addEventListener('x', fn); \
|
||
t.addEventListener('x', fn); \
|
||
t.dispatchEvent(new Event('x')); \
|
||
calls;"
|
||
)),
|
||
"1"
|
||
);
|
||
}
|
||
|
||
/// once 选项:监听器调用一次后自动移除。
|
||
#[test]
|
||
fn once_listener_removed_after_call() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} var calls = 0; \
|
||
var fn = function(e) {{ calls++; }}; \
|
||
t.addEventListener('x', fn, {{ once: true }}); \
|
||
t.dispatchEvent(new Event('x')); \
|
||
t.dispatchEvent(new Event('x')); \
|
||
calls;"
|
||
)),
|
||
"1"
|
||
);
|
||
}
|
||
|
||
/// stopImmediatePropagation 截断后续监听器。
|
||
#[test]
|
||
fn stop_immediate_propagation_truncates() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} var calls = 0; \
|
||
t.addEventListener('x', function(e) {{ calls++; e.stopImmediatePropagation(); }}); \
|
||
t.addEventListener('x', function(e) {{ calls++; }}); \
|
||
t.dispatchEvent(new Event('x')); \
|
||
calls;"
|
||
)),
|
||
"1"
|
||
);
|
||
}
|
||
|
||
/// 监听器回调的 this 是目标对象、实参是事件(e.type 为类型串)。
|
||
#[test]
|
||
fn listener_receives_target_this_and_event() {
|
||
assert_eq!(
|
||
eval(&format!(
|
||
"{TARGET} var gotThis = null; var gotType = null; \
|
||
t.addEventListener('x', function(e) {{ gotThis = (this === t); gotType = e.type; }}); \
|
||
t.dispatchEvent(new Event('x')); \
|
||
gotThis + ':' + gotType;"
|
||
)),
|
||
"true:x"
|
||
);
|
||
}
|
||
|
||
/// MessagePort 原型链经 EventTarget.prototype:port.addEventListener 生效。
|
||
#[test]
|
||
fn port_add_event_listener_works() {
|
||
assert_eq!(
|
||
eval(
|
||
"var mc = new MessageChannel(); var calls = 0; \
|
||
mc.port1.addEventListener('message', function(e) { calls++; }); \
|
||
mc.port1.dispatchEvent(new Event('message')); \
|
||
calls;"
|
||
),
|
||
"1"
|
||
);
|
||
}
|
||
|
||
/// BroadcastChannel 原型链经 EventTarget.prototype:channel.addEventListener 生效。
|
||
#[test]
|
||
fn channel_add_event_listener_works() {
|
||
assert_eq!(
|
||
eval(
|
||
"var ch = new BroadcastChannel('t'); var calls = 0; \
|
||
ch.addEventListener('message', function(e) { calls++; }); \
|
||
ch.dispatchEvent(new Event('message')); \
|
||
calls;"
|
||
),
|
||
"1"
|
||
);
|
||
}
|
||
|
||
/// Worker.prototype 原型链经 EventTarget.prototype:worker.addEventListener 生效。
|
||
#[test]
|
||
fn worker_add_event_listener_works() {
|
||
let path = std::env::temp_dir().join(format!("oxide_et_{}.js", std::process::id()));
|
||
let _ = std::fs::write(&path, "1");
|
||
let script = format!(
|
||
"var w = new Worker('{}'); var calls = 0; \
|
||
w.addEventListener('message', function(e) {{ calls++; }}); \
|
||
w.dispatchEvent(new Event('message')); \
|
||
calls;",
|
||
path.display()
|
||
);
|
||
assert_eq!(eval(&script), "1");
|
||
let _ = std::fs::remove_file(&path);
|
||
}
|
||
|
||
/// 监听器对象经 GC 后仍有效(目标存活时回调随目标存活,无悬垂)。
|
||
#[test]
|
||
fn listener_survives_gc_while_target_live() {
|
||
let mut vm = vm_with_script(
|
||
"var mc = new MessageChannel(); globalThis.t = mc.port1; \
|
||
globalThis.t.addEventListener('x', function(e) { globalThis.calls = (globalThis.calls || 0) + 1; });",
|
||
);
|
||
// 强制完整收集:目标存活(globalThis.t),监听器回调应保活。
|
||
vm.collect_session_gc();
|
||
// 收集后派发:监听器仍应被调用(无悬垂)。
|
||
assert_eq!(vm_eval(&mut vm, "globalThis.t.dispatchEvent(new Event('x')); globalThis.calls;"), "1");
|
||
}
|
||
|
||
/// 目标对象被回收后注册表剪枝(GC 后无悬垂指针)。
|
||
#[test]
|
||
fn registry_pruned_after_target_collected() {
|
||
// t 是局部变量,脚本结束后无引用 → 目标死亡。
|
||
let mut vm =
|
||
vm_with_script("var mc = new MessageChannel(); var t = mc.port1; t.addEventListener('x', function() {});");
|
||
// 强制完整收集:注册表应按 mark 位剪枝死目标条目。
|
||
vm.collect_session_gc();
|
||
// 剪枝后无悬垂:新建同型目标派发不触发旧监听器(旧条目已剪),无崩溃。
|
||
assert_eq!(
|
||
vm_eval(&mut vm, "var mc2 = new MessageChannel(); mc2.port1.dispatchEvent(new Event('x')); 'ok';"),
|
||
"ok"
|
||
);
|
||
}
|
||
|
||
/// ErrorEvent 构造器:init 的 message 经原型 getter 可读。
|
||
#[test]
|
||
fn error_event_constructor_message() {
|
||
assert_eq!(
|
||
eval("new ErrorEvent('error', { message: 'boom' }).message;"),
|
||
"boom"
|
||
);
|
||
}
|
||
|
||
/// ErrorEvent 缺省字段:message 空串、filename 空串、lineno 与 colno 为 0、
|
||
/// error 为 null。
|
||
#[test]
|
||
fn error_event_defaults() {
|
||
assert_eq!(
|
||
eval(
|
||
"var e = new ErrorEvent('error'); \
|
||
e.message === '' && e.filename === '' && e.lineno === 0 && e.colno === 0 && e.error === null;"
|
||
),
|
||
"true"
|
||
);
|
||
}
|
||
|
||
/// ErrorEvent 原型链:instanceof ErrorEvent 与 instanceof Event 均真,
|
||
/// 原型双亲为 Event.prototype。
|
||
#[test]
|
||
fn error_event_proto_chain() {
|
||
assert_eq!(
|
||
eval(
|
||
"var e = new ErrorEvent('error'); \
|
||
e instanceof ErrorEvent && e instanceof Event && \
|
||
Object.getPrototypeOf(e) === ErrorEvent.prototype && \
|
||
Object.getPrototypeOf(ErrorEvent.prototype) === Event.prototype;"
|
||
),
|
||
"true"
|
||
);
|
||
}
|
||
|
||
/// ErrorEvent 非构造形态抛 TypeError。
|
||
#[test]
|
||
fn error_event_requires_new() {
|
||
assert_eq!(
|
||
eval("try { ErrorEvent('error'); 'no-throw'; } catch (e) { e instanceof TypeError; }"),
|
||
"true"
|
||
);
|
||
}
|
||
|
||
/// ErrorEvent 盒边 GC 保活:error 字段引用对象经完整收集后仍存活。
|
||
#[test]
|
||
fn error_event_box_edge_survives_gc() {
|
||
let mut vm = vm_with_script(
|
||
"globalThis.e = new ErrorEvent('error', { error: { tag: 1 } });",
|
||
);
|
||
// 强制完整收集:error 字段是盒内 GC 边,对象应保活。
|
||
vm.collect_session_gc();
|
||
assert_eq!(vm_eval(&mut vm, "globalThis.e.error.tag;"), "1");
|
||
}
|