diff --git a/moli-core/tests/fixtures/runtime/chrome_cssstylesheet_rule_mutation_v2.html b/moli-core/tests/fixtures/runtime/chrome_cssstylesheet_rule_mutation_v2.html new file mode 100644 index 0000000000..a0f1cd60e0 --- /dev/null +++ b/moli-core/tests/fixtures/runtime/chrome_cssstylesheet_rule_mutation_v2.html @@ -0,0 +1,114 @@ + + + + + + diff --git a/moli-core/tests/web_apis.rs b/moli-core/tests/web_apis.rs index 3bad3e1eb8..4c8190628a 100644 --- a/moli-core/tests/web_apis.rs +++ b/moli-core/tests/web_apis.rs @@ -4156,7 +4156,7 @@ async fn chrome_cssstylesheet_rule_mutation_keeps_cssrules_list_operable_subset( let browser = Browser::new(AppConfig::default())?; let page = browser - .fetch(&server.url("/compat/chrome-cssstylesheet-rule-mutation")) + .fetch(&server.url("/compat/chrome-cssstylesheet-rule-mutation-v2")) .await?; assert!( @@ -4276,6 +4276,12 @@ async fn chrome_cssstylesheet_rule_mutation_keeps_cssrules_list_operable_subset( .unwrap() .contains("data-keyframe-type-after-csstext=\"8\"") ); + assert!( + page.serialize_html_async() + .await + .unwrap() + .contains("data-keyframe-invalid-error=\"SyntaxError\"") + ); assert!( page.serialize_html_async() .await diff --git a/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/grouping_rule.rs b/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/grouping_rule.rs index ca910a0e6a..49e84a6ed0 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/grouping_rule.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/grouping_rule.rs @@ -482,35 +482,34 @@ pub(crate) fn css_keyframes_rule_append_rule_callback<'s>( let attached_rule = parent_style_sheet.and_then(|sheet| { css_rule_attached_native_path(scope, this, sheet).map(|path| (sheet, path)) }); - if let Some((sheet, path)) = attached_rule { - if let Err(error) = apply_live_stylesheet_keyframe_rule_insert_mutation( + let result = if let Some((sheet, path)) = attached_rule { + apply_live_stylesheet_keyframe_rule_insert_mutation( scope, rules, &parsed.rule, index, sheet, &path, - ) { - throw_insert_rule_error(scope, error); - return; + ) + } else { + let result = apply_detached_keyframe_rule_insert_mutation( + scope, + this, + rules, + &parsed.rule, + index, + parent_style_sheet, + ); + if result.is_ok() { + sync_css_keyframes_rule_css_text_from_rules(scope, this, rules); } - rv.set_undefined(); - return; + result + }; + match result { + // Unlike CSSStyleSheet.insertRule, appendRule ignores invalid CSS. + Ok(()) | Err(CssRuleInsertError::Syntax) => rv.set_undefined(), + Err(error) => throw_insert_rule_error(scope, error), } - - if let Err(error) = apply_detached_keyframe_rule_insert_mutation( - scope, - this, - rules, - &parsed.rule, - index, - parent_style_sheet, - ) { - throw_insert_rule_error(scope, error); - return; - } - sync_css_keyframes_rule_css_text_from_rules(scope, this, rules); - rv.set_undefined(); } pub(crate) fn css_keyframes_rule_delete_rule_callback<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/specific_rules.rs b/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/specific_rules.rs index f92ea13215..79dbaf8872 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/specific_rules.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/specific_rules.rs @@ -2195,11 +2195,13 @@ pub(crate) fn css_keyframe_rule_key_text_setter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { - let Some(parsed) = webidl::parse_args::(scope, &args) else { + let Some(key_text) = + cssom_dom_string_property_value(scope, args.get(0), "CSSKeyframeRule", "keyText") + else { return; }; - let Some(key_text) = normalize_keyframe_selector_text_with_stylo(&parsed.key_text) else { - rv.set_undefined(); + let Some(key_text) = normalize_keyframe_selector_text_with_stylo(&key_text) else { + throw_dom_exception_value(scope, "Invalid keyframe selector.", "SyntaxError"); return; }; let this = css_rule_receiver(scope, &args); diff --git a/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/types.rs b/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/types.rs index 17d09f74ea..13d3582711 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/types.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_stylesheet_runtime/types.rs @@ -876,13 +876,6 @@ pub struct CssKeyframesRuleFindRuleArgs { pub key: String, } -#[derive(webidl::WebIdlArgs)] -#[webidl(prefix = "CSSKeyframeRule.keyText")] -pub struct CssKeyframeRuleKeyTextArgs { - #[webidl(required)] - pub key_text: String, -} - #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "CSSStyleSheet.replace")] pub struct CssStyleSheetReplaceArgs { diff --git a/moli-renderer-v8/src/script_vm/tests/css_keyframes_mutations.js b/moli-renderer-v8/src/script_vm/tests/css_keyframes_mutations.js new file mode 100644 index 0000000000..27b283d734 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/css_keyframes_mutations.js @@ -0,0 +1,44 @@ +((bindings, values, label) => { + const checks=[]; + const check=(name,fn)=>{try{const result=fn();checks.push({name:label+': '+name,passed:result===true,detail:result===true?null:String(result)});}catch(error){checks.push({name:label+': '+name,passed:false,detail:String(error)});}}; + const setter=Object.getOwnPropertyDescriptor(bindings.CSSKeyframeRule.prototype,'keyText').set; + const append=bindings.CSSKeyframesRule.prototype.appendRule; + const find=bindings.CSSKeyframesRule.prototype.findRule; + const remove=bindings.CSSKeyframesRule.prototype.deleteRule; + const syntaxError=error=>error instanceof bindings.DOMException && error.name==='SyntaxError' && error.code===12 && !(error instanceof bindings.SyntaxError); + for(const mode of ['attached','constructed','detached']) { + let owner; + const sheet=mode==='attached' ? (()=>{owner=values.document.createElement('style');owner.textContent='@keyframes original {from {left:0px} to {left:100px}}';values.document.head.appendChild(owner);return owner.sheet;})() : new values.CSSStyleSheet(); + if(mode!=='attached')sheet.replaceSync('@keyframes original {from {left:0px} to {left:100px}}'); + const frames=sheet.cssRules[0],list=frames.cssRules,frame=list[0],declaration=frame.style; + if(mode==='detached')sheet.deleteRule(0); + const snapshot=()=>JSON.stringify([frame.keyText,frame.cssText,declaration.cssText,frames.cssText,frames.length,sheet.cssRules.length]); + const initial=snapshot(); + const unchanged=()=>snapshot()===initial && frames.cssRules===list && list[0]===frame && frame.style===declaration; + try { + for(const [index,value] of ['', ' ', 'body', '-1%', '101%', '0', '0px', 'from,', 'from to', '0%, broken', '50% {}', '@keyframes k{}', '50%;', 'calc(50px)', undefined, null, false, 0, 1n, {}].entries()) { + check(mode+' invalid keyText '+index+' throws DOM SyntaxError',()=>{let error;try{setter.call(frame,value);}catch(caught){error=caught;}return syntaxError(error);}); + check(mode+' invalid keyText '+index+' preserves state and identities',unchanged); + } + check(mode+' omitted setter value converts undefined',()=>{let error;try{setter.call(frame);}catch(caught){error=caught;}return syntaxError(error) && unchanged();}); + check(mode+' symbol keyText throws TypeError',()=>{let error;try{setter.call(frame,Symbol('selector'));}catch(caught){error=caught;}return error instanceof bindings.TypeError && unchanged();}); + check(mode+' keyText conversion exception identity',()=>{const sentinel=new values.Error('keyText');let calls=0,error;try{setter.call(frame,{[Symbol.toPrimitive](hint){calls++;if(hint!=='string')throw Error(hint);throw sentinel;}});}catch(caught){error=caught;}return error===sentinel && calls===1 && unchanged();}); + for(const [index,value] of ['', ' ', 'body {}', '-1% {}', '101% {}', '50px {}', '50% trailing {}', 'from, {}', '@media all {}', '50%;', 'from {} to {}', '0%, bad {}', undefined, null, false, 0, 1n, {}].entries()) { + check(mode+' invalid appendRule '+index+' returns undefined',()=>append.call(frames,value)===undefined); + check(mode+' invalid appendRule '+index+' preserves state and identities',unchanged); + } + check(mode+' appendRule missing argument throws TypeError',()=>{let error;try{append.call(frames);}catch(caught){error=caught;}return error instanceof bindings.TypeError && unchanged();}); + check(mode+' appendRule symbol throws TypeError',()=>{let error;try{append.call(frames,Symbol('rule'));}catch(caught){error=caught;}return error instanceof bindings.TypeError && unchanged();}); + check(mode+' appendRule conversion exception identity',()=>{const sentinel=new values.Error('appendRule');let calls=0,error;try{append.call(frames,{toString(){calls++;throw sentinel;}});}catch(caught){error=caught;}return error===sentinel && calls===1 && unchanged();}); + check(mode+' receiver validation precedes mutation conversion',()=>{let calls=0,error;try{setter.call(frames,{toString(){calls++;return '50%';}});}catch(caught){error=caught;}return error instanceof bindings.TypeError && calls===0 && unchanged();}); + check(mode+' valid keyText converts once and normalizes',()=>{let calls=0;const result=setter.call(frame,{[Symbol.toPrimitive](hint){calls++;if(hint!=='string')throw Error(hint);return ' 25% , to ';}});return result===undefined && calls===1 && frame.keyText==='25%, 100%' && frame.style===declaration && list[0]===frame && find.call(frames,'25%, to')===frame;}); + check(mode+' valid appendRule converts once and appends',()=>{let calls=0;const result=append.call(frames,{toString(){calls++;return '75% {left:75px}';}});return result===undefined && calls===1 && frames.length===3 && list[2]===find.call(frames,'75%') && list[2].style.left==='75px';}); + const last=list[2]; + check(mode+' duplicate selector returns last rule',()=>{append.call(frames,'75% {left:76px}');return frames.length===4 && find.call(frames,'75%')===list[3] && list[2]===last;}); + check(mode+' deleteRule deletes last duplicate only',()=>{remove.call(frames,'75%');return frames.length===3 && find.call(frames,'75%')===last;}); + check(mode+' selector list ordering stays significant',()=>find.call(frames,'100%, 25%')===null && find.call(frames,'25%, 100%')===frame); + check(mode+' invalid findRule and deleteRule do not mutate',()=>{const before=snapshot();const result=find.call(frames,'body');remove.call(frames,'body');return result===null && snapshot()===before && frames.cssRules===list;}); + } finally {owner?.remove();} + } + return checks; +}) diff --git a/moli-renderer-v8/src/script_vm/tests/css_keyframes_mutations.rs b/moli-renderer-v8/src/script_vm/tests/css_keyframes_mutations.rs new file mode 100644 index 0000000000..d7fa39c4ed --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/css_keyframes_mutations.rs @@ -0,0 +1,93 @@ +use super::*; + +#[test] +fn css_keyframes_mutations_use_cssom_error_semantics_across_realms_and_rule_lifetimes() { + let mut vm = new_storage_page_task_executor_test_vm("https://css-keyframes.test/"); + vm.eval("document.body.innerHTML=''") + .unwrap(); + let source = format!( + "(() => {{const probe={};const realms=[globalThis,child.contentWindow];globalThis.mutationChecks=[];for(let a=0;a<2;a++)for(let b=0;b<2;b++)mutationChecks.push(...probe(realms[a],realms[b],a+'-'+b));return JSON.stringify(mutationChecks.filter(row=>!row.passed));}})()", + include_str!("css_keyframes_mutations.js") + ); + assert_eq!(vm.eval(&source).unwrap(), "[]"); + assert_eq!(vm.eval("mutationChecks.length").unwrap(), "1068"); +} + +#[test] +fn css_keyframes_mutations_accept_native_proxies_and_preserve_intrinsic_exception_realms() { + let mut vm = new_storage_page_task_executor_test_vm("https://css-keyframes.test/"); + vm.eval(r#" + document.body.innerHTML=''; + globalThis.other=child.contentWindow; + const sheet=new other.CSSStyleSheet();sheet.replaceSync('@keyframes native {from {left:0px}}'); + globalThis.keyframes=sheet.cssRules[0];globalThis.frame=keyframes[0]; + "#).unwrap(); + let context_ptr = &vm.page_default_runtime.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 (name, proxy_name) in [("keyframes", "keyframesProxy"), ("frame", "frameProxy")] { + let key = crate::util::v8str(scope, name); + let target = + v8::Local::::try_from(global.get(scope, key.into()).unwrap()).unwrap(); + let handler = crate::util::new_null_prototype_object(scope); + let proxy = v8::Proxy::new(scope, target, handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap(); + let key = crate::util::v8str(scope, proxy_name); + global.create_data_property(scope, key.into(), proxy.into()); + } + Ok(()) + }) + .unwrap(); + assert_eq!(vm.eval(r#" +(() => { + const assert=(value,message)=>{if(!value)throw Error(message);}; + const realms=[globalThis,other],exceptions=realms.map(realm=>realm.DOMException); + const descriptors=realms.map(realm=>Object.getOwnPropertyDescriptor(realm,'DOMException')); + let constructorReads=0; + try { + for(const realm of realms)Object.defineProperty(realm,'DOMException',{configurable:true,get(){constructorReads++;throw Error('public constructor');}}); + for(let index=0;index { + for(let index=0;index<20;index++) { + let error;try{frame.keyText='body';}catch(caught){error=caught;} + if(!(error instanceof DOMException) || error.name!=='SyntaxError')throw Error('invalid selector'); + if(keyframes.appendRule('body {}')!==undefined)throw Error('invalid append'); + } + return frame.keyText==='0%' && keyframes.length===2 && keyframes[0]===frame && item.getAnimations()[0]===animation; +})() +"#).unwrap(), "true"); + let metrics = crate::live_stylesheet::live_stylesheet_mutation_metrics_for_test(); + assert_eq!(metrics.native_keyframe_mutations, 0); + assert_eq!(metrics.native_rule_value_mutations, 0); + assert_eq!(metrics.native_top_level_mutations, 0); + assert_eq!(metrics.native_nested_mutations, 0); +} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/cssom/extracted/style_rule_mutations.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/cssom/extracted/style_rule_mutations.rs index 502f8a6467..db3bc89f16 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/cssom/extracted/style_rule_mutations.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/cssom/extracted/style_rule_mutations.rs @@ -460,7 +460,11 @@ fn css_keyframe_key_text_uses_stylo_selector_helpers() { keyframes.cssText, ].join('/'); - frame.keyText = 'body'; + let invalidError; + try { frame.keyText = 'body'; } catch (error) { invalidError = error; } + if (!(invalidError instanceof DOMException) || invalidError.name !== 'SyntaxError') { + throw Error('Invalid keyText must throw DOM SyntaxError'); + } const afterInvalid = [ frame.keyText, keyframes.findRule('body') === null, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 0b4478670a..02de4bea45 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2244,6 +2244,7 @@ mod cross_origin_symbol_fallback; mod cross_origin_window_indexes; mod cross_origin_window_names; mod css_animation_interfaces; +mod css_keyframes_mutations; mod css_keyframes_receivers; mod device_events; mod payment_events; diff --git a/moli-test-support/src/lib.rs b/moli-test-support/src/lib.rs index 52ac19bfea..77405e21f6 100644 --- a/moli-test-support/src/lib.rs +++ b/moli-test-support/src/lib.rs @@ -431,6 +431,9 @@ const CHROME_OVERFLOW_PROPERTY_HTML: &str = include_str!("../../moli-core/tests/fixtures/runtime/chrome_overflow_property.html"); const CHROME_CSSSTYLESHEET_RULE_MUTATION_HTML: &str = include_str!("../../moli-core/tests/fixtures/runtime/chrome_cssstylesheet_rule_mutation.html"); +const CHROME_CSSSTYLESHEET_RULE_MUTATION_V2_HTML: &str = include_str!( + "../../moli-core/tests/fixtures/runtime/chrome_cssstylesheet_rule_mutation_v2.html" +); const CHROME_DELETE_RULE_NO_CRASH_HTML: &str = include_str!("../../moli-core/tests/fixtures/runtime/chrome_delete_rule_no_crash.html"); const CHROME_IMPORTANT_JS_OVERRIDE_HTML: &str = diff --git a/moli-test-support/src/routes_core.rs b/moli-test-support/src/routes_core.rs index 8ef578138b..c1f922398a 100644 --- a/moli-test-support/src/routes_core.rs +++ b/moli-test-support/src/routes_core.rs @@ -1496,6 +1496,10 @@ pub(super) async fn chrome_cssstylesheet_rule_mutation_page() -> Html<&'static s Html(CHROME_CSSSTYLESHEET_RULE_MUTATION_HTML) } +pub(super) async fn chrome_cssstylesheet_rule_mutation_v2_page() -> Html<&'static str> { + Html(CHROME_CSSSTYLESHEET_RULE_MUTATION_V2_HTML) +} + pub(super) async fn chrome_delete_rule_no_crash_page() -> Html<&'static str> { Html(CHROME_DELETE_RULE_NO_CRASH_HTML) } diff --git a/moli-test-support/src/server_routes.rs b/moli-test-support/src/server_routes.rs index 217213f32e..9759031c56 100644 --- a/moli-test-support/src/server_routes.rs +++ b/moli-test-support/src/server_routes.rs @@ -683,6 +683,10 @@ pub(super) fn build_router() -> Router { "/compat/chrome-cssstylesheet-rule-mutation", get(chrome_cssstylesheet_rule_mutation_page), ) + .route( + "/compat/chrome-cssstylesheet-rule-mutation-v2", + get(chrome_cssstylesheet_rule_mutation_v2_page), + ) .route( "/compat/chrome-delete-rule-no-crash", get(chrome_delete_rule_no_crash_page),