From f29e6aeb8771f9897fdc6482fb322ea8a433aaa4 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 14 Jul 2026 06:09:37 +0800 Subject: [PATCH] fix(renderer): complete tabular element reflection --- moli-core/tests/web_apis.rs | 2 +- moli-renderer-v8/src/native_bridge/element.rs | 71 ++++-- .../element/global_attributes.rs | 105 ++++++--- .../element/html_elements/table.rs | 210 ++++++++++++++---- .../src/native_bridge/element/reflection.rs | 50 +++++ .../script_vm/tests/dom_elements/detached.rs | 109 ++++++++- 6 files changed, 455 insertions(+), 92 deletions(-) diff --git a/moli-core/tests/web_apis.rs b/moli-core/tests/web_apis.rs index f67629c7d9..223dfa1788 100644 --- a/moli-core/tests/web_apis.rs +++ b/moli-core/tests/web_apis.rs @@ -353,7 +353,7 @@ async fn html_element_reflected_accessors_cover_simple_tag_specific_surface() -> page.serialize_html_async() .await .unwrap() - .contains("data-table-cell=\"1:1:1000:1000:0:0:65534:65534\"") + .contains("data-table-cell=\"1:1:1000:2000:0:0:65534:70000\"") ); assert!( page.serialize_html_async() diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 00dae3cf6d..ded9abb98c 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -453,6 +453,7 @@ pub(super) use global_attributes::{ node_tab_index_setter_function, node_title_getter_function, node_title_setter_function, node_translate_getter_function, node_translate_setter_function, node_writing_suggestions_getter_function, node_writing_suggestions_setter_function, + null_to_empty_dom_string_reflection_getter_function, null_to_empty_dom_string_reflection_setter_function, object_archive_getter_function, object_code_base_getter_function, object_code_getter_function, object_code_type_getter_function, object_data_getter_function, object_declare_getter_function, @@ -462,8 +463,8 @@ pub(super) use global_attributes::{ table_cell_axis_getter_function, table_cell_headers_getter_function, table_cell_no_wrap_getter_function, table_cell_no_wrap_setter_function, table_cell_scope_getter_function, table_ch_getter_function, table_ch_off_getter_function, - table_col_span_getter_function, table_col_span_setter_function, table_v_align_getter_function, - unsigned_long_reflection_setter_function, usv_string_reflection_setter_function, + table_v_align_getter_function, unsigned_long_reflection_setter_function, + usv_string_reflection_setter_function, }; pub(in crate::native_bridge) const BODY_LEGACY_PROTOTYPE_ACCESSORS: &[&str] = &[ "onload", @@ -487,19 +488,19 @@ use html_elements::{ table_caption_getter_function, table_caption_setter_function, table_cell_col_span_getter_function, table_cell_col_span_setter_function, table_cell_index_getter_function, table_cell_row_span_getter_function, - table_cell_row_span_setter_function, table_create_caption_callback, - table_create_t_body_callback, table_create_t_foot_callback, table_create_t_head_callback, - table_delete_caption_callback, table_delete_row_callback, table_delete_t_foot_callback, - table_delete_t_head_callback, table_insert_row_callback, table_row_cells_getter_function, - table_row_delete_cell_callback, table_row_index_getter_function, - table_row_insert_cell_callback, table_rows_getter_function, table_section_delete_row_callback, - table_section_insert_row_callback, table_section_row_index_getter_function, - table_section_rows_getter_function, table_t_bodies_getter_function, - table_t_foot_getter_function, table_t_foot_setter_function, table_t_head_getter_function, - table_t_head_setter_function, track_default_getter_function, track_default_setter_function, - track_kind_getter_function, track_kind_setter_function, track_ready_state_getter_function, - track_src_getter_function, track_src_setter_function, track_srclang_getter_function, - track_srclang_setter_function, + table_cell_row_span_setter_function, table_col_span_getter_function, + table_col_span_setter_function, table_create_caption_callback, table_create_t_body_callback, + table_create_t_foot_callback, table_create_t_head_callback, table_delete_caption_callback, + table_delete_row_callback, table_delete_t_foot_callback, table_delete_t_head_callback, + table_insert_row_callback, table_row_cells_getter_function, table_row_delete_cell_callback, + table_row_index_getter_function, table_row_insert_cell_callback, table_rows_getter_function, + table_section_delete_row_callback, table_section_insert_row_callback, + table_section_row_index_getter_function, table_section_rows_getter_function, + table_t_bodies_getter_function, table_t_foot_getter_function, table_t_foot_setter_function, + table_t_head_getter_function, table_t_head_setter_function, track_default_getter_function, + track_default_setter_function, track_kind_getter_function, track_kind_setter_function, + track_ready_state_getter_function, track_src_getter_function, track_src_setter_function, + track_srclang_getter_function, track_srclang_setter_function, }; use html_elements::{ marquee_loop_getter_function, marquee_loop_setter_function, @@ -1940,6 +1941,46 @@ struct HtmlTableElementPrototypeDeclaration { setter_data = DomStringReflection::TableBorder )] border: (), + #[webapi( + accessor_property, + enumerable, + getter = dom_string_reflection_getter_function, + setter = dom_string_reflection_setter_function, + data = DomStringReflection::TableFrame + )] + frame: (), + #[webapi( + accessor_property, + enumerable, + getter = dom_string_reflection_getter_function, + setter = dom_string_reflection_setter_function, + data = DomStringReflection::TableRules + )] + rules: (), + #[webapi( + accessor_property, + enumerable, + getter = dom_string_reflection_getter_function, + setter = dom_string_reflection_setter_function, + data = DomStringReflection::TableSummary + )] + summary: (), + #[webapi( + accessor_property = "cellPadding", + enumerable, + getter = null_to_empty_dom_string_reflection_getter_function, + setter = null_to_empty_dom_string_reflection_setter_function, + data = NullToEmptyDomStringReflection::TableCellPadding + )] + cell_padding: (), + #[webapi( + accessor_property = "cellSpacing", + enumerable, + getter = null_to_empty_dom_string_reflection_getter_function, + setter = null_to_empty_dom_string_reflection_setter_function, + data = NullToEmptyDomStringReflection::TableCellSpacing + )] + cell_spacing: (), #[webapi( accessor_property, enumerable, diff --git a/moli-renderer-v8/src/native_bridge/element/global_attributes.rs b/moli-renderer-v8/src/native_bridge/element/global_attributes.rs index 0b57d00bfc..9a95cf4825 100644 --- a/moli-renderer-v8/src/native_bridge/element/global_attributes.rs +++ b/moli-renderer-v8/src/native_bridge/element/global_attributes.rs @@ -687,32 +687,6 @@ fn set_html_unsigned_long_attribute_for_receiver<'s>( set_reflected_attribute(scope, runtime_ptr, handle, content_attr, &value.to_string()); } -pub(in crate::native_bridge) fn table_col_span_getter_function<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - rv: v8::ReturnValue<'s, v8::Value>, -) { - html_unsigned_long_attribute_getter_for_receiver(scope, args.this(), rv, "span", 1, |value| { - if value == 0 { 1 } else { value.min(1000) } - }); -} - -pub(in crate::native_bridge) fn table_col_span_setter_function<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - set_html_unsigned_long_attribute_for_receiver( - scope, - args.this(), - "span", - args.get(0), - "HTMLTableColElement", - "span", - ); - rv.set_undefined(); -} - pub(in crate::native_bridge) fn html_download_getter_function<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -1110,6 +1084,31 @@ fn set_dom_string_treat_null_as_empty_on_object<'s>( set_reflected_attribute(scope, runtime_ptr, handle, attribute, &value); } +pub(in crate::native_bridge) fn null_to_empty_dom_string_reflection_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some(descriptor) = + NullToEmptyDomStringReflection::descriptor_from_callback_data(scope, args.data()) + else { + return; + }; + if let Some(local_name) = descriptor.local_name + && html_element_getter_receiver( + scope, + args.this(), + descriptor.interface, + descriptor.member, + local_name, + ) + .is_none() + { + return; + } + attribute_property_getter_from_object_or_detached(scope, args.this(), descriptor.attribute, rv); +} + pub(in crate::native_bridge) fn null_to_empty_dom_string_reflection_setter_function<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -1118,6 +1117,18 @@ pub(in crate::native_bridge) fn null_to_empty_dom_string_reflection_setter_funct if let Some(descriptor) = NullToEmptyDomStringReflection::descriptor_from_callback_data(scope, args.data()) { + if let Some(local_name) = descriptor.local_name + && html_element_setter_receiver( + scope, + args.this(), + descriptor.interface, + descriptor.member, + local_name, + ) + .is_none() + { + return; + } set_dom_string_treat_null_as_empty_on_object( scope, args.this(), @@ -1521,20 +1532,27 @@ fn canonical_scope_value(raw: &str) -> &'static str { // ---------- Unsigned-long content attribute reflections ---------- // // Each parses the content attribute via the HTML "non-negative integer -// parsing rules" (leading whitespace skipped, leading digits consumed) and -// returns the parsed value (clamped to [0, u32::MAX] then narrowed to i32 for -// the IDL `unsigned long`). When the attribute is missing or unparseable the -// attribute-specific default applies. +// parsing rules" (leading whitespace skipped, leading digits consumed). +// Attribute-specific reflection code decides whether to apply the signed +// 31-bit boundary or a narrower getter clamp. -fn parse_non_negative_integer(value: &str) -> Option { +pub(in crate::native_bridge::element) fn parse_non_negative_integer(value: &str) -> Option { // Per HTML "rules for parsing non-negative integers": skip leading ASCII // whitespace, then consume leading ASCII digits. Out-of-range results // saturate at u32::MAX rather than falling back to the attribute default // — the spec's reflection algorithm clamps to the unsigned-long range. let mut chars = value.chars().skip_while(|ch| ch.is_ascii_whitespace()); - if matches!(chars.clone().next(), Some('+')) { - chars.next(); - } + let negative = match chars.clone().next() { + Some('+') => { + chars.next(); + false + } + Some('-') => { + chars.next(); + true + } + _ => false, + }; let mut acc: u64 = 0; let mut had_digit = false; for ch in chars.by_ref() { @@ -1548,7 +1566,11 @@ fn parse_non_negative_integer(value: &str) -> Option { break; } } - if had_digit { Some(acc as u32) } else { None } + if !had_digit || (negative && acc != 0) { + None + } else { + Some(acc as u32) + } } fn unsigned_long_attribute_getter_from_object_or_detached<'s, F>( @@ -2553,7 +2575,18 @@ fn parse_tab_index_attribute(value: &str) -> Option { #[cfg(test)] mod tests { - use super::{canonical_dir_value, parse_tab_index_attribute}; + use super::{canonical_dir_value, parse_non_negative_integer, parse_tab_index_attribute}; + + #[test] + fn parses_html_non_negative_integer_including_minus_zero() { + assert_eq!(parse_non_negative_integer(""), None); + assert_eq!(parse_non_negative_integer("\u{b}7"), None); + assert_eq!(parse_non_negative_integer(" +7tail"), Some(7)); + assert_eq!(parse_non_negative_integer("-0"), Some(0)); + assert_eq!(parse_non_negative_integer("-00tail"), Some(0)); + assert_eq!(parse_non_negative_integer("-1"), None); + assert_eq!(parse_non_negative_integer("4294967296"), Some(u32::MAX)); + } #[test] fn parses_tab_index_attribute_like_html_signed_integer() { diff --git a/moli-renderer-v8/src/native_bridge/element/html_elements/table.rs b/moli-renderer-v8/src/native_bridge/element/html_elements/table.rs index 7530aadac8..f8026575a8 100644 --- a/moli-renderer-v8/src/native_bridge/element/html_elements/table.rs +++ b/moli-renderer-v8/src/native_bridge/element/html_elements/table.rs @@ -19,7 +19,8 @@ use super::super::super::{ throw_dom_exception, }; use super::super::set_reflected_attribute; -use super::{DomHandle, JsContextHost, parse_i32_attribute_or}; +use super::super::{element_attribute, global_attributes::parse_non_negative_integer}; +use super::{DomHandle, JsContextHost}; #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "HTMLTableElement.insertRow")] @@ -85,6 +86,7 @@ enum TableReceiverKind { Table, Section, Row, + Col, Cell, } @@ -97,6 +99,9 @@ fn table_receiver_matches( TableReceiverKind::Table => runtime.dom_host().is_html_element_named(handle, "table"), TableReceiverKind::Section => is_html_table_section(runtime, handle), TableReceiverKind::Row => runtime.dom_host().is_html_element_named(handle, "tr"), + TableReceiverKind::Col => ["col", "colgroup"] + .into_iter() + .any(|name| runtime.dom_host().is_html_element_named(handle, name)), TableReceiverKind::Cell => is_html_table_cell(runtime, handle), } } @@ -273,20 +278,83 @@ fn html_table_cell_setter_receiver<'s>( ) } -fn clamp_col_span(value: i32) -> i32 { - value.clamp(1, 1000) +fn html_table_col_getter_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + member: &'static str, +) -> Option<(*mut JsContextHost, DomHandle)> { + table_getter_receiver( + scope, + receiver, + "HTMLTableColElement", + member, + TableReceiverKind::Col, + ) } -fn parse_col_span(runtime: &JsContextHost, handle: DomHandle) -> i32 { - clamp_col_span(parse_i32_attribute_or(runtime, handle, "colspan", 1)) +fn html_table_col_setter_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + member: &'static str, +) -> Option<(*mut JsContextHost, DomHandle)> { + table_setter_receiver( + scope, + receiver, + "HTMLTableColElement", + member, + TableReceiverKind::Col, + ) } -fn clamp_row_span(value: i32) -> i32 { - if value == 0 { 0 } else { value.clamp(1, 65534) } +fn parse_clamped_table_span( + runtime: &JsContextHost, + handle: DomHandle, + attribute: &str, + default: u32, + min: u32, + max: u32, +) -> u32 { + let Some(raw) = element_attribute(runtime, handle, attribute) else { + return default; + }; + parse_non_negative_integer(&raw) + .map(|value| value.clamp(min, max)) + .unwrap_or(default) } -fn parse_row_span(runtime: &JsContextHost, handle: DomHandle) -> i32 { - clamp_row_span(parse_i32_attribute_or(runtime, handle, "rowspan", 1)) +fn set_table_unsigned_long_attribute<'s>( + scope: &mut v8::PinScope<'s, '_>, + runtime_ptr: *mut JsContextHost, + handle: DomHandle, + attribute: &str, + value: v8::Local<'s, v8::Value>, + default: u32, + interface: &'static str, + member: &'static str, +) { + let value = match webidl::convert::( + scope, + value, + webidl::Context::member(interface, member), + ) { + Ok(value) => value.0, + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + }; + let reflected = if value <= i32::MAX as u32 { + value + } else { + default + }; + set_reflected_attribute( + scope, + runtime_ptr, + handle, + attribute, + &reflected.to_string(), + ); } pub(in crate::native_bridge::element) fn table_cell_col_span_getter_function<'s>( @@ -297,18 +365,40 @@ pub(in crate::native_bridge::element) fn table_cell_col_span_getter_function<'s> let Some((runtime_ptr, handle)) = html_table_cell_getter_receiver(scope, args.this(), "colSpan") else { - rv.set_int32(1); + rv.set_uint32(1); return; }; - rv.set_int32(parse_col_span(unsafe { &*runtime_ptr }, handle)); + rv.set_uint32(parse_clamped_table_span( + unsafe { &*runtime_ptr }, + handle, + "colspan", + 1, + 1, + 1000, + )); } pub(in crate::native_bridge::element) fn table_cell_col_span_setter_function<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, - rv: v8::ReturnValue<'s, v8::Value>, + mut rv: v8::ReturnValue<'s, v8::Value>, ) { - table_cell_i32_attribute_setter(scope, args.this(), "colspan", args.get(0), 1, "colSpan", rv); + let Some((runtime_ptr, handle)) = + html_table_cell_setter_receiver(scope, args.this(), "colSpan") + else { + return; + }; + set_table_unsigned_long_attribute( + scope, + runtime_ptr, + handle, + "colspan", + args.get(0), + 1, + "HTMLTableCellElement", + "colSpan", + ); + rv.set_undefined(); } pub(in crate::native_bridge::element) fn table_cell_row_span_getter_function<'s>( @@ -319,18 +409,82 @@ pub(in crate::native_bridge::element) fn table_cell_row_span_getter_function<'s> let Some((runtime_ptr, handle)) = html_table_cell_getter_receiver(scope, args.this(), "rowSpan") else { - rv.set_int32(1); + rv.set_uint32(1); return; }; - rv.set_int32(parse_row_span(unsafe { &*runtime_ptr }, handle)); + rv.set_uint32(parse_clamped_table_span( + unsafe { &*runtime_ptr }, + handle, + "rowspan", + 1, + 0, + 65534, + )); } pub(in crate::native_bridge::element) fn table_cell_row_span_setter_function<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, - rv: v8::ReturnValue<'s, v8::Value>, + mut rv: v8::ReturnValue<'s, v8::Value>, ) { - table_cell_i32_attribute_setter(scope, args.this(), "rowspan", args.get(0), 1, "rowSpan", rv); + let Some((runtime_ptr, handle)) = + html_table_cell_setter_receiver(scope, args.this(), "rowSpan") + else { + return; + }; + set_table_unsigned_long_attribute( + scope, + runtime_ptr, + handle, + "rowspan", + args.get(0), + 1, + "HTMLTableCellElement", + "rowSpan", + ); + rv.set_undefined(); +} + +pub(in crate::native_bridge::element) fn table_col_span_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some((runtime_ptr, handle)) = html_table_col_getter_receiver(scope, args.this(), "span") + else { + rv.set_uint32(1); + return; + }; + rv.set_uint32(parse_clamped_table_span( + unsafe { &*runtime_ptr }, + handle, + "span", + 1, + 1, + 1000, + )); +} + +pub(in crate::native_bridge::element) fn table_col_span_setter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Some((runtime_ptr, handle)) = html_table_col_setter_receiver(scope, args.this(), "span") + else { + return; + }; + set_table_unsigned_long_attribute( + scope, + runtime_ptr, + handle, + "span", + args.get(0), + 1, + "HTMLTableColElement", + "span", + ); + rv.set_undefined(); } pub(in crate::native_bridge::element) fn table_caption_getter_function<'s>( @@ -875,28 +1029,6 @@ enum TableSlotPlacement { LastChild, } -fn table_cell_i32_attribute_setter<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - attribute: &str, - value: v8::Local<'s, v8::Value>, - default: i32, - member: &'static str, - mut rv: v8::ReturnValue<'s, v8::Value>, -) { - let Some((runtime_ptr, handle)) = html_table_cell_setter_receiver(scope, object, member) else { - rv.set_undefined(); - return; - }; - let number = match attribute { - "colspan" => clamp_col_span(value.int32_value(scope).unwrap_or(default)), - "rowspan" => clamp_row_span(value.int32_value(scope).unwrap_or(default)), - _ => value.int32_value(scope).unwrap_or(default), - }; - set_reflected_attribute(scope, runtime_ptr, handle, attribute, &number.to_string()); - rv.set_undefined(); -} - fn set_table_slot_for_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/native_bridge/element/reflection.rs b/moli-renderer-v8/src/native_bridge/element/reflection.rs index cf4244b28f..e1f85225a2 100644 --- a/moli-renderer-v8/src/native_bridge/element/reflection.rs +++ b/moli-renderer-v8/src/native_bridge/element/reflection.rs @@ -241,6 +241,9 @@ pub(super) enum DomStringReflection { StyleMedia, StyleType, TableBorder, + TableFrame, + TableRules, + TableSummary, TableCellAbbr, TableCellAxis, TableCellCh, @@ -692,6 +695,33 @@ const DOM_STRING_REFLECTION_DESCRIPTORS: &[(DomStringReflection, DomStringReflec DomStringReflection::TableBorder, DomStringReflectionDescriptor::new("HTMLTableElement", "border", "border"), ), + ( + DomStringReflection::TableFrame, + DomStringReflectionDescriptor::new_html_element( + "HTMLTableElement", + "table", + "frame", + "frame", + ), + ), + ( + DomStringReflection::TableRules, + DomStringReflectionDescriptor::new_html_element( + "HTMLTableElement", + "table", + "rules", + "rules", + ), + ), + ( + DomStringReflection::TableSummary, + DomStringReflectionDescriptor::new_html_element( + "HTMLTableElement", + "table", + "summary", + "summary", + ), + ), ( DomStringReflection::TableCellAbbr, DomStringReflectionDescriptor::new("HTMLTableCellElement", "abbr", "abbr"), @@ -887,6 +917,8 @@ pub(super) enum NullToEmptyDomStringReflection { ImageBorder, ObjectBorder, TableBgColor, + TableCellPadding, + TableCellSpacing, TableCellBgColor, TableRowBgColor, Count, @@ -932,6 +964,24 @@ const NULL_TO_EMPTY_DOM_STRING_REFLECTION_DESCRIPTORS: &[( NullToEmptyDomStringReflection::TableBgColor, ReflectedAttributeDescriptor::new("HTMLTableElement", "bgcolor", "bgColor"), ), + ( + NullToEmptyDomStringReflection::TableCellPadding, + ReflectedAttributeDescriptor::new_html_element( + "HTMLTableElement", + "table", + "cellpadding", + "cellPadding", + ), + ), + ( + NullToEmptyDomStringReflection::TableCellSpacing, + ReflectedAttributeDescriptor::new_html_element( + "HTMLTableElement", + "table", + "cellspacing", + "cellSpacing", + ), + ), ( NullToEmptyDomStringReflection::TableCellBgColor, ReflectedAttributeDescriptor::new("HTMLTableCellElement", "bgcolor", "bgColor"), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs index 44a302f887..c66f022aec 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs @@ -1797,6 +1797,75 @@ fn table_cell_legacy_accessors_use_owner_prototype() { assert_eq!(result, "ok"); } +#[test] +fn table_legacy_dom_string_reflectors_use_owner_prototype() { + let mut vm = new_storage_test_vm("https://table-legacy-dom-string-reflectors.test/"); + + let result = vm + .eval( + r#" +(() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const own = (object, name) => Object.prototype.hasOwnProperty.call(object, name); + const throwsTypeError = callback => { + try { + callback(); + return false; + } catch (error) { + return error instanceof TypeError; + } + }; + const cases = [ + ["frame", "frame", false], + ["rules", "rules", false], + ["summary", "summary", false], + ["cellPadding", "cellpadding", true], + ["cellSpacing", "cellspacing", true] + ]; + const detachedDocument = document.implementation.createHTMLDocument(""); + + for (const [name, attribute, nullAsEmpty] of cases) { + const descriptor = Object.getOwnPropertyDescriptor(HTMLTableElement.prototype, name); + assert(!!descriptor, `${name} descriptor missing`); + assert(typeof descriptor.get === "function", `${name} getter`); + assert(typeof descriptor.set === "function", `${name} setter`); + assert(descriptor.enumerable === true, `${name} enumerable`); + assert(descriptor.configurable === true, `${name} configurable`); + assert(!own(HTMLElement.prototype, name), `${name} should not be on HTMLElement.prototype`); + + for (const [doc, label] of [[document, "live"], [detachedDocument, "detached"]]) { + const table = doc.createElement("table"); + assert(!own(table, name), `${label}.${name} should not be own before set`); + assert(table[name] === "", `${label}.${name} missing-value default`); + table[name] = { toString: () => `${name}-value` }; + assert(table[name] === `${name}-value`, `${label}.${name} getter`); + assert(table.getAttribute(attribute) === `${name}-value`, `${label}.${name} attribute`); + table[name] = null; + const expectedNull = nullAsEmpty ? "" : "null"; + assert(table[name] === expectedNull, `${label}.${name} null getter`); + assert(table.getAttribute(attribute) === expectedNull, `${label}.${name} null attribute`); + assert(!own(table, name), `${label}.${name} should stay inherited after set`); + assert(delete table[name], `${label}.${name} delete`); + assert(!own(table, name), `${label}.${name} should stay inherited after delete`); + assert(table[name] === expectedNull, `${label}.${name} after delete`); + } + + for (const receiver of [document.createElement("div"), {}]) { + assert(throwsTypeError(() => descriptor.get.call(receiver)), `${name} getter receiver`); + assert(throwsTypeError(() => descriptor.set.call(receiver, "wrong")), `${name} setter receiver`); + } + } + return "ok"; +})() +"#, + ) + .expect("table legacy DOMString reflectors should evaluate"); + + assert_eq!(result, "ok"); +} + #[test] fn detached_table_structural_accessors_use_owner_prototypes() { let mut vm = new_storage_test_vm("https://detached-table-structural-prototypes.test/"); @@ -1872,9 +1941,28 @@ fn detached_table_structural_accessors_use_owner_prototypes() { secondCell.rowSpan = -5; assert(firstCell.colSpan === 7 && firstCell.getAttribute("colspan") === "7", `${label}.colSpan`); assert(firstCell.rowSpan === 0 && firstCell.getAttribute("rowspan") === "0", `${label}.rowSpan zero`); - assert(secondCell.colSpan === 1000 && secondCell.getAttribute("colspan") === "1000", `${label}.colSpan clamp`); + assert(secondCell.colSpan === 1000 && secondCell.getAttribute("colspan") === "2000", `${label}.colSpan clamp`); assert(secondCell.rowSpan === 1 && secondCell.getAttribute("rowspan") === "1", `${label}.rowSpan clamp`); + firstCell.setAttribute("colspan", "4294967296"); + firstCell.setAttribute("rowspan", "2147483648"); + assert(firstCell.colSpan === 1000, `${label}.colSpan large content clamp`); + assert(firstCell.rowSpan === 65534, `${label}.rowSpan large content clamp`); + firstCell.setAttribute("rowspan", "-0"); + assert(firstCell.rowSpan === 0, `${label}.rowSpan minus-zero content`); + firstCell.colSpan = "-0"; + assert(firstCell.getAttribute("colspan") === "0" && firstCell.colSpan === 1, `${label}.colSpan zero setter`); + firstCell.colSpan = 1001; + assert(firstCell.getAttribute("colspan") === "1001" && firstCell.colSpan === 1000, `${label}.colSpan setter clamp`); + firstCell.rowSpan = 65535; + assert(firstCell.getAttribute("rowspan") === "65535" && firstCell.rowSpan === 65534, `${label}.rowSpan setter clamp`); + firstCell.colSpan = 2147483648; + firstCell.rowSpan = 4294967295; + assert(firstCell.getAttribute("colspan") === "1" && firstCell.colSpan === 1, `${label}.colSpan setter default`); + assert(firstCell.getAttribute("rowspan") === "1" && firstCell.rowSpan === 1, `${label}.rowSpan setter default`); + firstCell.colSpan = 7; + firstCell.rowSpan = 0; + for (const [element, names, elementLabel] of [ [tbody, ["rows"], "tbody"], [firstRow, ["rowIndex", "sectionRowIndex", "cells"], "firstRow"], @@ -2043,6 +2131,14 @@ fn table_legacy_alignment_accessors_use_owner_prototypes() { if (!condition) throw new Error(message); }; const own = (object, name) => Object.prototype.hasOwnProperty.call(object, name); + const throwsTypeError = callback => { + try { + callback(); + return false; + } catch (error) { + return error instanceof TypeError; + } + }; const accessor = (prototype, name) => { const descriptor = Object.getOwnPropertyDescriptor(prototype, name); assert(!!descriptor, `${name} descriptor missing`); @@ -2138,11 +2234,22 @@ fn table_legacy_alignment_accessors_use_owner_prototypes() { element.span = 1002; assert(element.getAttribute("span") === "1002", `${label}.span large attr`); assert(element.span === 1000, `${label}.span large canonical`); + element.setAttribute("span", "4294967296"); + assert(element.span === 1000, `${label}.span large content clamp`); + element.span = 2147483648; + assert(element.getAttribute("span") === "1", `${label}.span setter default attr`); + assert(element.span === 1, `${label}.span setter default`); element.setAttribute("span", "invalid"); assert(element.span === 1, `${label}.span invalid canonical`); assert(delete element.span, `${label}.span delete`); assert(!own(element, "span"), `${label}.span should stay inherited`); } + + const spanDescriptor = Object.getOwnPropertyDescriptor(HTMLTableColElement.prototype, "span"); + for (const receiver of [document.createElement("div"), document.createElement("td"), {}]) { + assert(throwsTypeError(() => spanDescriptor.get.call(receiver)), "span getter receiver"); + assert(throwsTypeError(() => spanDescriptor.set.call(receiver, 2)), "span setter receiver"); + } return "ok"; })() "#,