From e5a566253e69314595aea9e55bc1cdeb8dff0995 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 7 Oct 2026 06:50:29 +0800 Subject: [PATCH] fix(svg): reflect filter enumerations with native receiver validation Expose GaussianBlur edgeMode with its none default and shared edge-mode keywords/constants. Preserve arithmetic as composite operator 6 and keep the existing lighter extension at 7. Use derived SVGAnimatedEnumeration receiver and argument validation, canonicalize registered native proxies, and reflect mutations back to native owner attributes. Cover live/windowless documents, invalid keywords, cross-realm bindings, conversion order, and registered/author proxies. --- .../context_bootstrap/svg_runtime/bindings.rs | 32 +++- .../svg_runtime/callbacks.rs | 46 ++--- .../src/context_bootstrap/svg_runtime/mod.rs | 19 ++- .../live_document/geometry_and_svg.rs | 6 +- moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../tests/svg_filter_enumerations.js | 160 ++++++++++++++++++ .../tests/svg_filter_enumerations.rs | 112 ++++++++++++ 7 files changed, 338 insertions(+), 38 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.js create mode 100644 moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.rs diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs index dce89a37b3..0ceff452f5 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -718,6 +718,17 @@ define_svg_enumeration_constants!( } ); +define_svg_enumeration_constants!( + SvgFeGaussianBlurElementTemplateConstantsDeclaration, + "SVGFEGaussianBlurElement", + { + unknown => "SVG_EDGEMODE_UNKNOWN" = SVG_EDGE_MODE_UNKNOWN, + duplicate => "SVG_EDGEMODE_DUPLICATE" = SVG_EDGE_MODE_DUPLICATE, + wrap => "SVG_EDGEMODE_WRAP" = SVG_EDGE_MODE_WRAP, + none => "SVG_EDGEMODE_NONE" = SVG_EDGE_MODE_NONE, + } +); + define_svg_enumeration_constants!( SvgFeDisplacementMapElementTemplateConstantsDeclaration, "SVGFEDisplacementMapElement", @@ -1250,7 +1261,7 @@ struct SvgAnimatedNumberListTemplateAccessorsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::SVGAnimatedEnumeration, enumerable)] +#[webapi(interface = web_api_interfaces::SVGAnimatedEnumeration, enumerable, receiver)] struct SvgAnimatedEnumerationTemplateAccessorsDeclaration { #[webapi( accessor_property = "baseVal", @@ -1896,6 +1907,17 @@ struct SvgFeCompositeElementPrototypeAccessorsDeclaration { operator: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::SVGFEGaussianBlurElement, enumerable, receiver)] +struct SvgFeGaussianBlurElementPrototypeAccessorsDeclaration { + #[webapi( + accessor_property = "edgeMode", + getter = svg_element_animated_enumeration_getter, + data = callback_data_index_value(scope, 25) + )] + edge_mode: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::SVGFEConvolveMatrixElement, enumerable, receiver)] struct SvgFeConvolveMatrixElementPrototypeAccessorsDeclaration { @@ -2427,6 +2449,9 @@ pub(super) fn install_svg_enumeration_constant_bindings<'s>( "SVGFEConvolveMatrixElement" => { install_constants!(SvgFeConvolveMatrixElementTemplateConstantsDeclaration); } + "SVGFEGaussianBlurElement" => { + install_constants!(SvgFeGaussianBlurElementTemplateConstantsDeclaration); + } "SVGFEDisplacementMapElement" => { install_constants!(SvgFeDisplacementMapElementTemplateConstantsDeclaration); } @@ -2682,6 +2707,11 @@ pub(super) fn install_svg_element_accessor_bindings<'s>( scope, prototype, ); } + "SVGFEGaussianBlurElement" => { + SvgFeGaussianBlurElementPrototypeAccessorsDeclaration::initialize_prototype_template( + scope, prototype, + ); + } "SVGFEConvolveMatrixElement" => { SvgFeConvolveMatrixElementPrototypeAccessorsDeclaration::initialize_prototype_template( scope, prototype, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs index c6a9b106e9..1e66f7470a 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -1932,14 +1932,8 @@ pub(super) fn svg_animated_enumeration_getter<'s>( rv.set_undefined(); return; }; - if !require_svg_receiver( - scope, - args.this(), - "SVGAnimatedEnumeration", - &format!("{name} getter"), - ) { - return; - } + let receiver = moli_webapi_declare::web_api_object_target(scope, args.this()) + .expect("generated receiver check validates native SVGAnimatedEnumeration identity"); let slot = match name { "baseVal" => SVG_ANIMATED_ENUMERATION_BASE_VAL_SLOT, "animVal" => SVG_ANIMATED_ENUMERATION_ANIM_VAL_SLOT, @@ -1948,11 +1942,11 @@ pub(super) fn svg_animated_enumeration_getter<'s>( return; } }; - sync_svg_animated_enumeration_from_owner_attribute(scope, args.this()); - let initial_value = svg_animated_enumeration_property(scope, args.this()) + sync_svg_animated_enumeration_from_owner_attribute(scope, receiver); + let initial_value = svg_animated_enumeration_property(scope, receiver) .map(|property| property.initial_value) .unwrap_or(SVG_LENGTH_ADJUST_SPACING); - let value = svg_length_number_slot(scope, args.this(), slot).unwrap_or(initial_value as f64); + let value = svg_length_number_slot(scope, receiver, slot).unwrap_or(initial_value as f64); rv.set(v8::Integer::new_from_unsigned(scope, value as u32).into()); } @@ -1969,28 +1963,16 @@ pub(super) fn svg_animated_enumeration_setter<'s>( ) else { return; }; - if !require_svg_receiver( - scope, - args.this(), - "SVGAnimatedEnumeration", - &format!("{name} setter"), - ) || name != "baseVal" - { + if name != "baseVal" { return; } - let value = match webidl::convert::( - scope, - args.get(0), - webidl::Context::member("SVGAnimatedEnumeration", "baseVal"), - ) { - Ok(value) => value.0, - Err(error) => { - webidl::throw_error(scope, &error); - return; - } + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; }; - let value = u32::from(value); - let Some(property) = svg_animated_enumeration_property(scope, args.this()) else { + let receiver = moli_webapi_declare::web_api_object_target(scope, args.this()) + .expect("generated receiver check validates native SVGAnimatedEnumeration identity"); + let value = u32::from(parsed.value); + let Some(property) = svg_animated_enumeration_property(scope, receiver) else { webidl::throw_type_error( scope, "SVGAnimatedEnumeration has no enumeration definition.", @@ -2001,8 +1983,8 @@ pub(super) fn svg_animated_enumeration_setter<'s>( webidl::throw_type_error(scope, "Invalid SVGAnimatedEnumeration value."); return; } - set_svg_animated_enumeration_values(scope, args.this(), value); - reflect_svg_animated_enumeration_to_owner_attribute(scope, args.this()); + set_svg_animated_enumeration_values(scope, receiver, value); + reflect_svg_animated_enumeration_to_owner_attribute(scope, receiver); } pub(super) fn svg_animated_number_setter<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs index df65de8aa5..df82ea5ed2 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -139,6 +139,7 @@ const SVG_FE_BLEND_MODE_SLOT: &str = "__moliSvgFeBlendMode"; const SVG_FE_COLOR_MATRIX_TYPE_SLOT: &str = "__moliSvgFeColorMatrixType"; const SVG_FE_COMPOSITE_OPERATOR_SLOT: &str = "__moliSvgFeCompositeOperator"; const SVG_FE_CONVOLVE_MATRIX_EDGE_MODE_SLOT: &str = "__moliSvgFeConvolveMatrixEdgeMode"; +const SVG_FE_GAUSSIAN_BLUR_EDGE_MODE_SLOT: &str = "__moliSvgFeGaussianBlurEdgeMode"; const SVG_FE_CONVOLVE_MATRIX_ORDER_X_SLOT: &str = "__moliSvgFeConvolveMatrixOrderX"; const SVG_FE_CONVOLVE_MATRIX_ORDER_Y_SLOT: &str = "__moliSvgFeConvolveMatrixOrderY"; const SVG_FE_CONVOLVE_MATRIX_TARGET_X_SLOT: &str = "__moliSvgFeConvolveMatrixTargetX"; @@ -436,6 +437,13 @@ struct SvgAngleConvertToSpecifiedUnitsArgs { unit_type: u16, } +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "SVGAnimatedEnumeration.baseVal")] +struct SvgAnimatedEnumerationBaseValArgs { + #[webidl(required, converter = "unsigned_short")] + value: u16, +} + #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "SVGGeometryElement.getPointAtLength")] struct SvgGeometryPointAtLengthArgs { @@ -567,8 +575,8 @@ const SVG_FE_COMPOSITE_OPERATOR_IN: u32 = 2; const SVG_FE_COMPOSITE_OPERATOR_OUT: u32 = 3; const SVG_FE_COMPOSITE_OPERATOR_ATOP: u32 = 4; const SVG_FE_COMPOSITE_OPERATOR_XOR: u32 = 5; -const SVG_FE_COMPOSITE_OPERATOR_LIGHTER: u32 = 6; -const SVG_FE_COMPOSITE_OPERATOR_ARITHMETIC: u32 = 7; +const SVG_FE_COMPOSITE_OPERATOR_ARITHMETIC: u32 = 6; +const SVG_FE_COMPOSITE_OPERATOR_LIGHTER: u32 = 7; const SVG_EDGE_MODE_UNKNOWN: u32 = 0; const SVG_EDGE_MODE_DUPLICATE: u32 = 1; @@ -877,6 +885,13 @@ const SVG_ANIMATED_ENUMERATION_PROPERTIES: &[SvgAnimatedEnumerationProperty] = & initial_value: SVG_TEXT_PATH_SIDE_TYPE_LEFT, kind: SvgAnimatedEnumerationKind::Keywords(SVG_TEXT_PATH_SIDE_TYPE_VALUES), }, + SvgAnimatedEnumerationProperty { + index: 25, + attribute: "edgeMode", + cache_slot: SVG_FE_GAUSSIAN_BLUR_EDGE_MODE_SLOT, + initial_value: SVG_EDGE_MODE_NONE, + kind: SvgAnimatedEnumerationKind::Keywords(SVG_EDGE_MODE_VALUES), + }, ]; const fn svg_animated_number_property( diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs index 756eaeffcf..6d39d359b9 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document/geometry_and_svg.rs @@ -961,8 +961,8 @@ fn svg_filter_and_text_path_enumerations_reflect_typed_content_attributes() { ["SVG_FECOMPOSITE_OPERATOR_OUT", 3], ["SVG_FECOMPOSITE_OPERATOR_ATOP", 4], ["SVG_FECOMPOSITE_OPERATOR_XOR", 5], - ["SVG_FECOMPOSITE_OPERATOR_LIGHTER", 6], - ["SVG_FECOMPOSITE_OPERATOR_ARITHMETIC", 7], + ["SVG_FECOMPOSITE_OPERATOR_LIGHTER", 7], + ["SVG_FECOMPOSITE_OPERATOR_ARITHMETIC", 6], ]], [SVGFEConvolveMatrixElement, [ ["SVG_EDGEMODE_UNKNOWN", 0], @@ -1029,7 +1029,7 @@ fn svg_filter_and_text_path_enumerations_reflect_typed_content_attributes() { ["feFuncR", SVGComponentTransferFunctionElement, "type", 1, 5, "gamma", 6], ["feBlend", SVGFEBlendElement, "mode", 1, 16, "luminosity", 17], ["feColorMatrix", SVGFEColorMatrixElement, "type", 1, 4, "luminanceToAlpha", 5], - ["feComposite", SVGFECompositeElement, "operator", 1, 7, "arithmetic", 8], + ["feComposite", SVGFECompositeElement, "operator", 1, 6, "arithmetic", 8], ["feConvolveMatrix", SVGFEConvolveMatrixElement, "edgeMode", 1, 3, "none", 4], ["feDisplacementMap", SVGFEDisplacementMapElement, "xChannelSelector", 4, 1, "R", 5], ["feDisplacementMap", SVGFEDisplacementMapElement, "yChannelSelector", 4, 3, "B", 5], diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 09e9a487fc..e3ed3e156d 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2329,6 +2329,7 @@ mod streams; mod string_timers; mod svg_animated_number; mod svg_element_receivers; +mod svg_filter_enumerations; mod svg_filter_interfaces; mod svg_geometry_methods; mod svg_geometry_receivers; diff --git a/moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.js b/moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.js new file mode 100644 index 0000000000..c9804c3395 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.js @@ -0,0 +1,160 @@ +(() => { + const checks = []; + const check = (name, fn) => { + try { checks.push({name, passed: fn() === true}); } + catch (error) { checks.push({name, passed: false, error: String(error)}); } + }; + const ns = 'http://www.w3.org/2000/svg'; + const realms = [window, document.querySelector('iframe').contentWindow]; + const edgeConstants = [['SVG_EDGEMODE_UNKNOWN', 0], ['SVG_EDGEMODE_DUPLICATE', 1], + ['SVG_EDGEMODE_WRAP', 2], ['SVG_EDGEMODE_NONE', 3]]; + const cases = [ + ['feComposite', 'SVGFECompositeElement', 'operator', 1, + [['over', 1], ['in', 2], ['out', 3], ['atop', 4], ['xor', 5], ['arithmetic', 6], ['lighter', 7]], + [['SVG_FECOMPOSITE_OPERATOR_UNKNOWN', 0], ['SVG_FECOMPOSITE_OPERATOR_OVER', 1], + ['SVG_FECOMPOSITE_OPERATOR_IN', 2], ['SVG_FECOMPOSITE_OPERATOR_OUT', 3], + ['SVG_FECOMPOSITE_OPERATOR_ATOP', 4], ['SVG_FECOMPOSITE_OPERATOR_XOR', 5], + ['SVG_FECOMPOSITE_OPERATOR_ARITHMETIC', 6]]], + ['feGaussianBlur', 'SVGFEGaussianBlurElement', 'edgeMode', 3, + [['duplicate', 1], ['wrap', 2], ['none', 3]], edgeConstants], + ['feConvolveMatrix', 'SVGFEConvolveMatrixElement', 'edgeMode', 1, + [['duplicate', 1], ['wrap', 2], ['none', 3]], edgeConstants], + ['feMorphology', 'SVGFEMorphologyElement', 'operator', 1, + [['erode', 1], ['dilate', 2]], [['SVG_MORPHOLOGY_OPERATOR_UNKNOWN', 0], + ['SVG_MORPHOLOGY_OPERATOR_ERODE', 1], ['SVG_MORPHOLOGY_OPERATOR_DILATE', 2]]], + ['feDisplacementMap', 'SVGFEDisplacementMapElement', 'xChannelSelector', 4, + [['R', 1], ['G', 2], ['B', 3], ['A', 4]], [['SVG_CHANNEL_UNKNOWN', 0], + ['SVG_CHANNEL_R', 1], ['SVG_CHANNEL_G', 2], ['SVG_CHANNEL_B', 3], ['SVG_CHANNEL_A', 4]]], + ]; + const throwsTypeError = (realm, fn) => { + try { fn(); } catch (error) { return Object.getPrototypeOf(error) === realm.TypeError.prototype; } + return false; + }; + for (const [ownerIndex, owner] of realms.entries()) { + for (const [calleeIndex, realm] of realms.entries()) { + const documents = [owner.document, owner.document.implementation.createHTMLDocument(''), + owner.document.implementation.createDocument(ns, 'svg'), + new owner.DOMParser().parseFromString('', 'image/svg+xml')]; + for (const [documentIndex, doc] of documents.entries()) { + for (const [tag, iface, property, initial, keywords, constants] of cases) { + const prefix = `${ownerIndex}/${calleeIndex}/${documentIndex}/${tag}/${property}`; + const element = doc.createElementNS(ns, tag); + (doc.body || doc.documentElement).appendChild(element); + const ctor = realm[iface]; + const getter = Object.getOwnPropertyDescriptor(ctor.prototype, property)?.get; + const enumProto = realm.SVGAnimatedEnumeration.prototype; + const base = Object.getOwnPropertyDescriptor(enumProto, 'baseVal'); + const anim = Object.getOwnPropertyDescriptor(enumProto, 'animVal'); + const value = element[property]; + check(prefix + '/attribute-descriptor', () => { + const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, property); + return descriptor.enumerable && descriptor.configurable && descriptor.set === undefined && + descriptor.get.length === 0 && typeof descriptor.get === 'function'; + }); + check(prefix + '/enum-descriptors', () => base.enumerable && base.configurable && + base.get.length === 0 && base.set.length === 1 && anim.enumerable && anim.configurable && + anim.get.length === 0 && anim.set === undefined); + check(prefix + '/owner-realm-and-identity', () => + Object.getPrototypeOf(value) === owner.SVGAnimatedEnumeration.prototype && + getter.call(element) === value && element[property] === value); + check(prefix + '/missing-initial', () => value.baseVal === initial && value.animVal === initial && + !element.hasAttribute(property)); + for (const [name, expected] of constants) { + for (const [targetName, target] of [['constructor', ctor], ['prototype', ctor.prototype]]) { + check(prefix + '/constant/' + targetName + '/' + name, () => { + const descriptor = Object.getOwnPropertyDescriptor(target, name); + return descriptor.value === expected && descriptor.enumerable && + !descriptor.writable && !descriptor.configurable; + }); + } + } + for (const [keyword, expected] of keywords) { + check(prefix + '/attribute-to-enum/' + keyword, () => { + element.setAttribute(property, keyword); + return base.get.call(value) === expected && anim.get.call(value) === expected && + getter.call(element) === value; + }); + check(prefix + '/enum-to-attribute/' + keyword, () => { + base.set.call(value, expected); + return element.getAttribute(property) === keyword && value.baseVal === expected && + value.animVal === expected; + }); + } + for (const invalid of ['', 'invalid', 'NONE', ' duplicate ', '\u0000']) { + check(prefix + '/invalid-content-initial/' + JSON.stringify(invalid), () => { + element.setAttribute(property, invalid); + return value.baseVal === initial && value.animVal === initial && + element.getAttribute(property) === invalid; + }); + } + check(prefix + '/remove-initial-and-identity', () => { + element.removeAttribute(property); + return value.baseVal === initial && value.animVal === initial && getter.call(element) === value; + }); + const [keyword, expected] = keywords[0]; + for (const [name, argument] of [['wrapped', 65536 + expected], ['negative-wrapped', expected - 65536], + ['fractional', expected + 0.75], ['string', String(expected)]]) { + check(prefix + '/unsigned-short/' + name, () => { + base.set.call(value, argument); + return value.baseVal === expected && element.getAttribute(property) === keyword; + }); + } + for (const [name, argument] of [['zero', 0], ['outside-domain', 42], ['nan', NaN], + ['infinite', Infinity], ['undefined', undefined], ['null', null], ['negative', -1], + ['symbol', Symbol('enum')], ['bigint', 1n]]) { + check(prefix + '/invalid-setter/' + name, () => { + element.setAttribute(property, keyword); + return throwsTypeError(realm, () => base.set.call(value, argument)) && + element.getAttribute(property) === keyword && value.baseVal === expected; + }); + } + check(prefix + '/conversion-once-and-live-mutation', () => { + let conversions = 0; + base.set.call(value, {valueOf() { conversions++; element.setAttribute(property, 'invalid'); return expected; }}); + return conversions === 1 && element.getAttribute(property) === keyword && value.baseVal === expected; + }); + check(prefix + '/original-conversion-exception', () => { + const sentinel = {}; let conversions = 0; + try { base.set.call(value, {valueOf() { conversions++; throw sentinel; }}); } + catch (error) { return error === sentinel && conversions === 1 && element.getAttribute(property) === keyword; } + return false; + }); + check(prefix + '/clone-independent-identity', () => { + const clone = element.cloneNode(); + const cloned = getter.call(clone); + clone.setAttribute(property, keywords[1][0]); + return cloned !== value && cloned.baseVal === keywords[1][1] && + value.baseVal === expected && Object.getPrototypeOf(cloned) === owner.SVGAnimatedEnumeration.prototype; + }); + check(prefix + '/retained-value-after-detach', () => { + element.remove(); element.setAttribute(property, keywords[1][0]); + return getter.call(element) === value && value.baseVal === keywords[1][1]; + }); + const wrongElement = doc.createElementNS(ns, tag === 'feMorphology' ? 'feConvolveMatrix' : 'feMorphology'); + for (const [index, receiver] of [wrongElement, doc.createElement('div'), {}, Object.create(element)].entries()) { + check(prefix + '/element-receiver/' + index, () => throwsTypeError(realm, () => getter.call(receiver))); + } + let traps = 0; + const authorProxy = new Proxy(value || {}, {get() {traps++; throw 42;}, getPrototypeOf() {traps++; throw 42;}}); + const revoked = Proxy.revocable(value || {}, {}); revoked.revoke(); + const invalidReceivers = [{}, Object.create(enumProto), Object.create(value || enumProto), + authorProxy, revoked.proxy, element, doc, doc.createElementNS(ns, 'circle')]; + for (const [index, receiver] of invalidReceivers.entries()) { + for (const [name, fn] of [['base', base.get], ['anim', anim.get]]) { + check(prefix + '/enum-receiver/' + index + '/' + name, () => + throwsTypeError(realm, () => fn.call(receiver)) && traps === 0); + } + check(prefix + '/enum-receiver/' + index + '/set-before-conversion', () => { + let conversions = 0; + return throwsTypeError(realm, () => base.set.call(receiver, {valueOf() {conversions++; return expected;}})) && + conversions === 0 && traps === 0; + }); + } + } + } + } + } + globalThis.__uiEventResults = {complete: true, total: checks.length, + passed: checks.filter(row => row.passed).length, checks}; + return true; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.rs b/moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.rs new file mode 100644 index 0000000000..3fd4f67f74 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/svg_filter_enumerations.rs @@ -0,0 +1,112 @@ +use super::*; + +#[test] +fn svg_filter_enumerations_reflect_native_attributes_with_webidl_receiver_order() { + let mut vm = new_storage_page_task_executor_test_vm("https://svg-filter-enums.test/"); + vm.eval("document.body.innerHTML = ''") + .unwrap(); + vm.eval(include_str!("svg_filter_enumerations.js")).unwrap(); + assert_eq!(vm.eval("__uiEventResults.complete").unwrap(), "true"); + assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "5744"); + assert_eq!( + vm.eval("JSON.stringify(__uiEventResults.checks.filter(row => !row.passed))") + .unwrap(), + "[]" + ); +} + +#[test] +fn svg_filter_enumerations_accept_registered_native_proxies_for_values_and_elements() { + let mut vm = new_storage_page_task_executor_test_vm("https://svg-filter-enum-proxies.test/"); + vm.eval( + r#" + document.body.innerHTML = ''; + globalThis.childWindow = document.querySelector('iframe').contentWindow; + const doc = childWindow.document.implementation.createHTMLDocument(''); + globalThis.definitions = [ + ['feComposite', 'SVGFECompositeElement', 'operator', 'arithmetic', 6], + ['feGaussianBlur', 'SVGFEGaussianBlurElement', 'edgeMode', 'wrap', 2], + ['feConvolveMatrix', 'SVGFEConvolveMatrixElement', 'edgeMode', 'none', 3], + ['feMorphology', 'SVGFEMorphologyElement', 'operator', 'dilate', 2], + ['feDisplacementMap', 'SVGFEDisplacementMapElement', 'xChannelSelector', 'A', 4], + ]; + for (const [tag, , property, keyword] of definitions) { + const element = doc.createElementNS('http://www.w3.org/2000/svg', tag); + element.setAttribute(property, keyword); + globalThis[tag] = element; + globalThis[tag + 'Value'] = element[property]; + } + "#, + ) + .unwrap(); + let context_ptr = &vm.page_default_context as *const _; + vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| { + let global = scope.get_current_context().global(scope); + for tag in [ + "feComposite", + "feGaussianBlur", + "feConvolveMatrix", + "feMorphology", + "feDisplacementMap", + ] { + for suffix in ["", "Value"] { + let name = format!("{tag}{suffix}"); + let key = crate::util::v8_string(scope, &name).unwrap(); + let object = global.get(scope, key.into()).unwrap(); + let object = v8::Local::::try_from(object).unwrap(); + let handler = crate::util::new_null_prototype_object(scope); + let proxy = v8::Proxy::new(scope, object, handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap(); + let key = crate::util::v8_string(scope, &format!("{name}Proxy")).unwrap(); + assert_eq!( + global.create_data_property(scope, key.into(), proxy.into()), + Some(true) + ); + } + } + Ok(()) + }) + .unwrap(); + assert_eq!( + vm.eval( + r#"(() => { + for (const realm of [window, childWindow]) { + const prototype = realm.SVGAnimatedEnumeration.prototype; + const base = Object.getOwnPropertyDescriptor(prototype, 'baseVal'); + const anim = Object.getOwnPropertyDescriptor(prototype, 'animVal'); + for (const [tag, iface, property, keyword, expected] of definitions) { + const element = globalThis[tag]; + const value = globalThis[tag + 'Value']; + const nativeValue = globalThis[tag + 'ValueProxy']; + const getter = Object.getOwnPropertyDescriptor(realm[iface].prototype, property).get; + if (getter.call(globalThis[tag + 'Proxy']) !== value || + base.get.call(nativeValue) !== expected || anim.get.call(nativeValue) !== expected) { + throw Error('registered proxy lost identity or reflected value'); + } + let conversions = 0; + base.set.call(nativeValue, {valueOf() {conversions++; return expected + 65536;}}); + if (conversions !== 1 || element.getAttribute(property) !== keyword || + value.baseVal !== expected || value.animVal !== expected) throw Error('native proxy writeback'); + let traps = 0; + const author = new Proxy(nativeValue, { + get() {traps++; throw 42;}, getPrototypeOf() {traps++; throw 42;} + }); + const revoked = Proxy.revocable(nativeValue, {}); revoked.revoke(); + for (const receiver of [author, revoked.proxy, Object.create(nativeValue)]) { + for (const fn of [base.get, anim.get, base.set]) { + let error; + try {fn.call(receiver, {valueOf() {conversions++; return expected;}});} + catch (caught) {error = caught;} + if (!error || Object.getPrototypeOf(error) !== realm.TypeError.prototype || + conversions !== 1 || traps !== 0) throw Error('invalid proxy receiver'); + } + } + } + } + return true; + })()"#, + ) + .unwrap(), + "true" + ); +}