diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig
index 2e344e7b8..6eac3eda0 100644
--- a/src/browser/js/Snapshot.zig
+++ b/src/browser/js/Snapshot.zig
@@ -326,22 +326,18 @@ fn createSnapshotContext(
const template_index = comptime bridge.JsApiLookup.getId(JsApi);
const func = v8.v8__FunctionTemplate__GetFunction(templates[template_index], context);
if (@hasDecl(JsApi.Meta, "name")) {
+ const name = JsApi.Meta.name;
+ const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
+ var maybe_result: v8.MaybeBool = undefined;
+ // Web IDL: interface objects on the global are non-enumerable.
+ v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, v8.DontEnum, &maybe_result);
+
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
const alias = JsApi.Meta.constructor_alias;
- const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
- var maybe_result: v8.MaybeBool = undefined;
- v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
-
- const name = JsApi.Meta.name;
- const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
- var maybe_result2: v8.MaybeBool = undefined;
- v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
- } else {
- const name = JsApi.Meta.name;
- const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
- var maybe_result: v8.MaybeBool = undefined;
- // Web IDL: interface objects on the global are non-enumerable.
- v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, v8.DontEnum, &maybe_result);
+ const alias_func = generateLegacyFactoryFunction(JsApi, isolate, templates[template_index], func.?, context.?);
+ const v8_alias_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
+ var maybe_alias_result: v8.MaybeBool = undefined;
+ v8.v8__Object__DefineOwnProperty(global_obj, context, v8_alias_name, @ptrCast(alias_func), v8.DontEnum, &maybe_alias_result);
}
}
@@ -721,7 +717,9 @@ fn protoIndexLookup(comptime JsApi: type) ?u16 {
// Generate a constructor template for a JsApi type (public for reuse)
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback, const arity = comptime blk: {
- if (@hasDecl(JsApi, "constructor")) {
+ // The constructor belongs to the legacy factory function (`Image`),
+ // see generateLegacyFactoryFunction.
+ if (@hasDecl(JsApi, "constructor") and !@hasDecl(JsApi.Meta, "constructor_alias")) {
break :blk .{ JsApi.constructor.func, JsApi.constructor.arity };
}
if (inheritsFromHtmlElement(JsApi)) {
@@ -749,6 +747,38 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.Fun
return template;
}
+// https://webidl.spec.whatwg.org/#legacy-factory-functions
+// `Image`, `Audio`, `Option`: a function distinct from the interface object
+// (so `new HTMLImageElement()` stays illegal and `Image.name` is "Image"),
+// whose `prototype` is the interface's prototype object.
+fn generateLegacyFactoryFunction(comptime JsApi: type, isolate: *v8.Isolate, interface_template: *const v8.FunctionTemplate, interface_func: *const v8.Function, context: *const v8.Context) *const v8.Function {
+ const alias = JsApi.Meta.constructor_alias;
+ const template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
+ .length = JsApi.constructor.arity,
+ .callback = JsApi.constructor.func,
+ .behavior = v8.kConstructorBehavior_Allow,
+ }).?;
+ // Inherit so that the objects we construct pass the interface's
+ // accessor and method signature checks.
+ v8.v8__FunctionTemplate__Inherit(template, interface_template);
+ {
+ const internal_field_count = comptime countInternalFields(JsApi);
+ if (internal_field_count > 0) {
+ const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
+ v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
+ }
+ }
+ const class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
+ v8.v8__FunctionTemplate__SetClassName(template, class_name);
+
+ const func = v8.v8__FunctionTemplate__GetFunction(template, context).?;
+ const prototype_key = v8.v8__String__NewFromUtf8(isolate, "prototype", v8.kNormal, 9);
+ const interface_prototype = v8.v8__Object__Get(@ptrCast(interface_func), context, prototype_key).?;
+ var maybe_result: v8.MaybeBool = undefined;
+ v8.v8__Object__DefineOwnProperty(@ptrCast(func), context, prototype_key, interface_prototype, v8.ReadOnly + v8.DontEnum + v8.DontDelete, &maybe_result);
+ return func;
+}
+
// hard-coded special case for HtmlElement which can be extended but not
// instantiated.
fn inheritsFromHtmlElement(comptime JsApi: type) bool {
diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html
index fcb9455e4..a778285cb 100644
--- a/src/browser/tests/element/html/image.html
+++ b/src/browser/tests/element/html/image.html
@@ -179,3 +179,23 @@
img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACw=';
testing.expectEqual(undefined, await img.decode());
+
+
diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html
index a0e23f437..3d248e075 100644
--- a/src/browser/tests/element/html/media.html
+++ b/src/browser/tests/element/html/media.html
@@ -308,3 +308,23 @@
testing.expectEqual(true, video.paused);
}
+
+
diff --git a/src/browser/tests/element/html/option.html b/src/browser/tests/element/html/option.html
index e35cc306e..93bafa0b9 100644
--- a/src/browser/tests/element/html/option.html
+++ b/src/browser/tests/element/html/option.html
@@ -257,3 +257,23 @@
testing.expectEqual(1, select.selectedIndex)
}
+
+