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 d93609b554..b2fcaba909 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -3951,7 +3951,7 @@ pub(super) fn svg_geometry_get_total_length_callback<'s>( let length = svg_geometry_segments(scope, receiver) .iter() .fold(0.0, |total, segment| total + segment.length()); - rv.set(v8::Number::new(scope, length).into()); + rv.set(v8::Number::new(scope, f64::from(length as f32)).into()); } pub(super) fn svg_geometry_get_point_at_length_callback<'s>( @@ -3965,7 +3965,16 @@ pub(super) fn svg_geometry_get_point_at_length_callback<'s>( return; }; let segments = svg_geometry_segments(scope, receiver); - let point = svg_geometry::point_at_length(&segments, parsed.distance); + if segments.is_empty() { + throw_dom_exception( + scope, + "InvalidStateError", + 11, + "The SVG geometry has no point on its path.", + ); + return; + } + let point = svg_geometry::point_at_length(&segments, f64::from(parsed.distance)); rv.set(build_dom_point_like(scope, point.x, point.y).into()); } 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 6e483e4565..94347b707f 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -368,8 +368,8 @@ struct SvgAngleConvertToSpecifiedUnitsArgs { #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "SVGGeometryElement.getPointAtLength")] struct SvgGeometryPointAtLengthArgs { - #[webidl(required, converter = "double")] - distance: f64, + #[webidl(required, converter = "float")] + distance: f32, } #[derive(webidl::WebIdlArgs)] diff --git a/moli-renderer-v8/src/script_vm/tests/svg_geometry_float.js b/moli-renderer-v8/src/script_vm/tests/svg_geometry_float.js new file mode 100644 index 0000000000..553017aa85 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/svg_geometry_float.js @@ -0,0 +1,46 @@ +(() => { + const checks = [], ns = 'http://www.w3.org/2000/svg'; + const check = (name,fn) => {try {checks.push({name,passed:fn()===true});} catch(error) {checks.push({name,passed:false,error:String(error)});}}; + const near = (a,b) => Math.abs(a-b) <= Math.max(1e-12,Math.abs(b)*2e-14); + const realms = [window,document.querySelector('iframe').contentWindow]; + const values = [0,-0,1,1+2**-24,1+3*2**-24,16777217,16777219,1e-46,-1,1e20,-1e20,Math.fround(3.4028234e38),null,true,'1.0000001788139343']; + const invalid = [undefined,NaN,Infinity,-Infinity,Number.MAX_VALUE,-Number.MAX_VALUE,2**128,-(2**128),Symbol(),1n]; + const typeError = (realm,fn) => {try {fn();} catch(error) {return Object.getPrototypeOf(error)===realm.TypeError.prototype;} return false;}; + for (const [ownerIndex,owner] of realms.entries()) { + const docs = [owner.document,owner.document.implementation.createHTMLDocument(''),owner.document.implementation.createDocument(ns,'svg'),new owner.DOMParser().parseFromString('','image/svg+xml')]; + for (const [docIndex,doc] of docs.entries()) { + const root=doc.createElementNS(ns,'svg');root.setAttribute('width','200');root.setAttribute('height','100');(doc.body||doc.documentElement).appendChild(root); + for (const [calleeIndex,callee] of realms.entries()) { + const prefix=`float/${ownerIndex}/${docIndex}/${calleeIndex}`; + const point=callee.SVGGeometryElement.prototype.getPointAtLength, total=callee.SVGGeometryElement.prototype.getTotalLength; + const path=doc.createElementNS(ns,'path');path.setAttribute('d','M0 0L134217728 0');root.appendChild(path); + for (const [index,value] of values.entries()) { + check(prefix+'/distance/'+index,()=>{const p=point.call(path,value);return near(p.x,Math.min(134217728,Math.max(0,Math.fround(Number(value)))))&&p.y===0;}); + check(prefix+'/object-distance/'+index,()=>{let n=0;const p=point.call(path,{valueOf(){n++;return value;}});return n===1&&near(p.x,Math.min(134217728,Math.max(0,Math.fround(Number(value)))))&&p.y===0;}); + } + const empty=doc.createElementNS(ns,'path');root.appendChild(empty); + for (const [index,value] of invalid.entries()) { + for (const [tag,element] of [['path',path],['empty',empty]]) { + check(prefix+'/'+tag+'/invalid/'+index,()=>typeError(callee,()=>point.call(element,value))); + } + } + check(prefix+'/arity',()=>typeError(callee,()=>point.call(path))); + check(prefix+'/conversion-exception',()=>{const sentinel={};let n=0;try {point.call(path,{valueOf(){n++;throw sentinel;}});} catch(error) {return error===sentinel&&n===1;}return false;}); + check(prefix+'/conversion-mutates-path',()=>{let n=0;const p=point.call(path,{valueOf(){n++;path.setAttribute('d','M7 9L134217735 9');return 16777217;}});return n===1&&near(p.x,16777223)&&p.y===9;}); + for (const [index,value] of [0.1,1.00000007,16777217,16777219,1000.00001].entries()) { + const line=doc.createElementNS(ns,'line');line.setAttribute('x2',String(value));root.appendChild(line); + check(prefix+'/total-float/'+index,()=>Object.is(total.call(line),Math.fround(value))); + line.remove(); + } + let conversions=0,traps=0; + const author=new Proxy(path,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 42;}}),revoked=Proxy.revocable(path,{});revoked.revoke(); + for (const [index,receiver] of [{},Object.create(path),Object.create(callee.SVGGeometryElement.prototype),author,revoked.proxy,doc.createElementNS(ns,'g')].entries()) { + check(prefix+'/brand/'+index,()=>typeError(callee,()=>point.call(receiver,{valueOf(){conversions++;return Number.MAX_VALUE;}}))&&conversions===0&&traps===0); + } + path.remove();empty.remove(); + } + root.remove(); + } + } + globalThis.__svgFloatResults={complete:true,total:checks.length,passed:checks.filter(row=>row.passed).length,checks};globalThis.__uiEventResults=globalThis.__svgFloatResults;return true; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/svg_geometry_methods.rs b/moli-renderer-v8/src/script_vm/tests/svg_geometry_methods.rs index f07ea81b5b..e9bc446304 100644 --- a/moli-renderer-v8/src/script_vm/tests/svg_geometry_methods.rs +++ b/moli-renderer-v8/src/script_vm/tests/svg_geometry_methods.rs @@ -90,3 +90,34 @@ fn svg_geometry_methods_use_registered_proxy_geometry_without_author_traps() { "true" ); } + +#[test] +fn svg_distance_queries_distinguish_empty_paths_from_zero_length_geometry() { + let mut vm = new_storage_page_task_executor_test_vm("https://svg-empty-paths.test/"); + vm.eval("document.body.innerHTML = ''") + .unwrap(); + vm.eval(include_str!("svg_geometry_path_states.js")) + .unwrap(); + assert_eq!(vm.eval("__svgGeometryResults.complete").unwrap(), "true"); + assert_eq!(vm.eval("__svgGeometryResults.total").unwrap(), "3616"); + assert_eq!( + vm.eval("JSON.stringify(__svgGeometryResults.checks.filter(row => !row.passed))") + .unwrap(), + "[]", + ); +} + +#[test] +fn svg_distance_queries_use_webidl_float_conversion() { + let mut vm = new_storage_page_task_executor_test_vm("https://svg-geometry-float.test/"); + vm.eval("document.body.innerHTML = ''") + .unwrap(); + vm.eval(include_str!("svg_geometry_float.js")).unwrap(); + assert_eq!(vm.eval("__svgFloatResults.complete").unwrap(), "true"); + assert_eq!(vm.eval("__svgFloatResults.total").unwrap(), "1024"); + assert_eq!( + vm.eval("JSON.stringify(__svgFloatResults.checks.filter(row => !row.passed))") + .unwrap(), + "[]" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/svg_geometry_path_states.js b/moli-renderer-v8/src/script_vm/tests/svg_geometry_path_states.js new file mode 100644 index 0000000000..0a089df1ff --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/svg_geometry_path_states.js @@ -0,0 +1,60 @@ +(() => { + 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 definitions = [ + ['path/missing','path',{},null,0], ['path/empty','path',{d:''},null,0], + ['path/none','path',{d:'none'},null,0], ['path/invalid-first','path',{d:'L 7 9'},null,0], + ['path/moveto','path',{d:'M 7 9'},[7,9],0], + ['path/multiple-moveto','path',{d:'M 7 9 M 12 13'},[7,9],0], + ['path/zero-line','path',{d:'M 7 9 L 7 9'},[7,9],0], + ['path/invalid-tail','path',{d:'M 7 9 X 1 2'},[7,9],0], + ['line/missing','line',{},[0,0],0], ['line/zero','line',{x1:7,y1:9,x2:7,y2:9},[7,9],0], + ['rect/missing','rect',{},null,0], ['rect/zero','rect',{x:7,y:9,width:0,height:0},null,0], + ['rect/zero-width','rect',{x:7,y:9,width:0,height:10},[7,9],20], + ['rect/zero-height','rect',{x:7,y:9,width:10,height:0},[7,9],20], + ['circle/missing','circle',{},null,0], ['circle/zero','circle',{cx:7,cy:9,r:0},null,0], + ['ellipse/missing','ellipse',{},null,0], ['ellipse/zero','ellipse',{cx:7,cy:9,rx:0,ry:0},null,0], + ['ellipse/zero-rx','ellipse',{cx:7,cy:9,rx:0,ry:10},[7,9],40], + ['ellipse/zero-ry','ellipse',{cx:7,cy:9,rx:10,ry:0},[17,9],40], + ['polyline/missing','polyline',{},null,0], ['polygon/missing','polygon',{},null,0], + ['polyline/one-point','polyline',{points:'7 9'},[7,9],0], + ['polygon/one-point','polygon',{points:'7 9'},[7,9],0], + ]; + const errorIn = (realm,name,fn) => {try {fn();} catch(error) {return Object.getPrototypeOf(error) === realm[name==='TypeError'?'TypeError':'DOMException'].prototype && error.name===name && (name==='TypeError'||error.code===11);} return false;}; + for (const [ownerIndex,owner] of realms.entries()) { + const docs=[owner.document,owner.document.implementation.createHTMLDocument(''),owner.document.implementation.createDocument(ns,'svg'),new owner.DOMParser().parseFromString('','image/svg+xml')]; + for(const [docIndex,doc] of docs.entries()) { + const root=doc.createElementNS(ns,'svg');(doc.body||doc.documentElement).appendChild(root); + for(const [calleeIndex,callee] of realms.entries()) { + const point=callee.SVGGeometryElement.prototype.getPointAtLength, length=callee.SVGGeometryElement.prototype.getTotalLength; + for(const [name,tag,attrs,expected,total] of definitions) { + const prefix=`${ownerIndex}/${docIndex}/${calleeIndex}/${name}`; + const element=doc.createElementNS(ns,tag);for(const [key,value] of Object.entries(attrs))element.setAttribute(key,value);root.appendChild(element); + for(const display of ['','none']) { + element.style.display=display; + check(prefix+'/length/'+display,()=>total===0?Object.is(length.call(element),0):Math.abs(length.call(element)-total)<1e-6); + check(prefix+'/point/'+display,()=> { + if(expected===null)return errorIn(callee,'InvalidStateError',()=>point.call(element,300)); + const value=point.call(element,300);return Object.getPrototypeOf(value)===callee.DOMPoint.prototype&&Math.abs(value.x-expected[0])<1e-7&&Math.abs(value.y-expected[1])<1e-7&&value.z===0&&value.w===1; + }); + } + check(prefix+'/detach',()=> {element.remove();if(expected===null)return errorIn(callee,'InvalidStateError',()=>point.call(element,300));const p=point.call(element,300);return Math.abs(p.x-expected[0])<1e-7&&Math.abs(p.y-expected[1])<1e-7;}); + if(expected===null) { + for(const argument of [NaN,Infinity,-Infinity,Symbol(),1n])check(prefix+'/conversion/'+typeof argument+'/'+String(argument),()=>errorIn(callee,'TypeError',()=>point.call(element,argument))); + check(prefix+'/missing-argument',()=>errorIn(callee,'TypeError',()=>point.call(element))); + check(prefix+'/conversion-exception',()=> {const sentinel={};let n=0;try{point.call(element,{valueOf(){n++;throw sentinel;}});}catch(error){return error===sentinel&&n===1;}return false;}); + check(prefix+'/conversion-before-empty',()=> {let n=0;return errorIn(callee,'InvalidStateError',()=>point.call(element,{valueOf(){n++;return 0;}}))&&n===1;}); + } + } + const path=doc.createElementNS(ns,'path');root.appendChild(path); + check(`${ownerIndex}/${docIndex}/${calleeIndex}/conversion-creates-path`,()=> {let n=0;const p=point.call(path,{valueOf(){n++;path.setAttribute('d','M 7 9 L 17 9');return 5;}});return n===1&&p.x===12&&p.y===9;}); + check(`${ownerIndex}/${docIndex}/${calleeIndex}/conversion-removes-path`,()=> {let n=0;return errorIn(callee,'InvalidStateError',()=>point.call(path,{valueOf(){n++;path.removeAttribute('d');return 5;}}))&&n===1;}); + let traps=0;const proxy=new Proxy(path,{get(){traps++;throw 42;},getPrototypeOf(){traps++;throw 42;}});const revoked=Proxy.revocable(path,{});revoked.revoke(); + for(const [index,receiver] of [{},Object.create(callee.SVGGeometryElement.prototype),Object.create(path),proxy,revoked.proxy,doc,doc.createElement('div'),doc.createElementNS(ns,'g')].entries())check(`${ownerIndex}/${docIndex}/${calleeIndex}/brand/${index}`,()=> {let n=0;return errorIn(callee,'TypeError',()=>point.call(receiver,{valueOf(){n++;return 0;}}))&&n===0&&traps===0;}); + } + } + } + globalThis.__svgGeometryResults={complete:true,total:checks.length,passed:checks.filter(x=>x.passed).length,checks};globalThis.__uiEventResults=globalThis.__svgGeometryResults;return true; +})() diff --git a/moli-wpt-compat/fixtures/wpt/ported/geometry/dompoint-dommatrix-basic.html b/moli-wpt-compat/fixtures/wpt/ported/geometry/dompoint-dommatrix-basic.html index 1951bc1737..0a200185b8 100644 --- a/moli-wpt-compat/fixtures/wpt/ported/geometry/dompoint-dommatrix-basic.html +++ b/moli-wpt-compat/fixtures/wpt/ported/geometry/dompoint-dommatrix-basic.html @@ -85,6 +85,8 @@ test(function () { test(function () { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.setAttribute("width", "10"); + rect.setAttribute("height", "10"); svg.appendChild(rect); const point = rect.getPointAtLength(0);