fix(webidl): honor LegacyFactoryFunction NewTarget

This commit is contained in:
ldm0
2026-09-08 02:00:32 +08:00
parent 39a6ecbf8f
commit fef3d6260d
2 changed files with 87 additions and 0 deletions
@@ -84,6 +84,7 @@ pub(in crate::context_bootstrap) fn image_constructor_callback<'s>(
let value = v8::Integer::new_from_unsigned(scope, height);
let _ = image.set(scope, v8str(scope, "height").into(), value.into());
}
apply_legacy_factory_new_target_prototype(scope, &args, image, "HTMLImageElement");
rv.set(image.into());
}
@@ -153,6 +154,7 @@ pub(in crate::context_bootstrap) fn audio_constructor_callback<'s>(
);
}
}
apply_legacy_factory_new_target_prototype(scope, &args, audio, "HTMLAudioElement");
rv.set(audio.into());
}
@@ -243,9 +245,28 @@ pub(in crate::context_bootstrap) fn option_constructor_callback<'s>(
false,
);
}
apply_legacy_factory_new_target_prototype(scope, &args, option, "HTMLOptionElement");
rv.set(option.into());
}
fn apply_legacy_factory_new_target_prototype<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
result: v8::Local<'s, v8::Object>,
default_constructor_name: &str,
) {
let receiver = args.this();
crate::util::apply_webidl_constructor_prototype_fallback(
scope,
receiver,
args.new_target(),
default_constructor_name,
);
if let Some(prototype) = receiver.get_prototype(scope) {
let _ = result.set_prototype(scope, prototype);
}
}
pub(crate) fn html_element_constructor_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -2438,6 +2438,72 @@ fn html_legacy_factory_constructors_use_element_interface_prototypes() {
);
}
#[test]
fn html_legacy_factory_constructors_honor_new_target_prototypes() {
let mut vm = new_storage_test_vm("https://legacy-factory-new-target.test/");
let result = vm
.eval(
r#"
(() => {
function check(ctor, iface, localName, args) {
const Derived = class extends ctor {};
const instance = Reflect.construct(ctor, args, Derived);
return [
Object.getPrototypeOf(instance) === Derived.prototype,
instance instanceof Derived,
instance instanceof iface,
instance.localName === localName
].join(':');
}
function fallback(ctor, iface) {
function BadNewTarget() {}
BadNewTarget.prototype = 1;
const instance = Reflect.construct(ctor, [], BadNewTarget);
return [
Object.getPrototypeOf(instance) === iface.prototype,
instance instanceof iface
].join(':');
}
let prototypeGets = 0;
const proxyPrototype = Object.create(HTMLImageElement.prototype);
const ProxyNewTarget = new Proxy(function() {}, {
get(target, property, receiver) {
if (property === 'prototype') {
prototypeGets++;
return proxyPrototype;
}
return Reflect.get(target, property, receiver);
}
});
const proxyImage = Reflect.construct(Image, [], ProxyNewTarget);
return [
check(Audio, HTMLAudioElement, 'audio', ['clip.mp3']),
check(Image, HTMLImageElement, 'img', [4, 5]),
check(Option, HTMLOptionElement, 'option', ['label', 'value']),
fallback(Audio, HTMLAudioElement),
fallback(Image, HTMLImageElement),
fallback(Option, HTMLOptionElement),
prototypeGets,
Object.getPrototypeOf(proxyImage) === proxyPrototype
].join('|');
})()
"#,
)
.expect("legacy factory NewTarget prototype probe should evaluate");
assert_eq!(
result,
concat!(
"true:true:true:true|true:true:true:true|true:true:true:true|",
"true:true|true:true|true:true|1|true"
)
);
}
#[tokio::test]
async fn html_image_load_runs_on_its_dom_task_not_window_load_dispatch() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");