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);