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