From fef3d6260d1dd327b74397dcbc0bef8bb503f554 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 31 Aug 2026 11:07:27 +0800 Subject: [PATCH] fix(webidl): honor LegacyFactoryFunction NewTarget --- .../constructors/elements.rs | 21 ++++++ .../src/script_vm/tests/canvas_webgl.rs | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs b/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs index a97b94f4ab..6a8da9bfab 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs @@ -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>, diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs index 1405227eed..af19501763 100644 --- a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs +++ b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs @@ -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");