utils.js (8496B)
1 let next_property_id = 1; 2 3 // Generate a unique property name on the form --prop-N. 4 function generate_name() { 5 return `--prop-${next_property_id++}`; 6 } 7 8 // Produce a compatible initial value for the specified syntax. 9 function any_initial_value(syntax) { 10 let components = syntax.split('|').map(x => x.trim()) 11 let first_component = components[0]; 12 13 if (first_component.endsWith('+') || first_component.endsWith('#')) 14 first_component = first_component.slice(0, -1); 15 16 switch (first_component) { 17 case '*': 18 case '<custom-ident>': 19 return 'NULL'; 20 case '<angle>': 21 return '0deg'; 22 case '<color>': 23 return 'rgb(0, 0, 0)'; 24 case '<image>': 25 case '<url>': 26 return 'url(0)'; 27 case '<integer>': 28 case '<length-percentage>': 29 case '<length>': 30 case '<number>': 31 return '0'; 32 case '<percentage>': 33 return '0%'; 34 case '<resolution>': 35 return '0dpi'; 36 case '<time>': 37 return '0s'; 38 case '<transform-function>': 39 case '<transform-list>': 40 return 'matrix(0, 0, 0, 0, 0, 0)'; 41 default: 42 // We assume syntax is a specific custom ident. 43 return first_component; 44 } 45 } 46 47 // Registers a unique property on the form '--prop-N' and returns the name. 48 // Any value except 'syntax' may be omitted, in which case the property will 49 // not inherit, and some undefined (but compatible) initial value will be 50 // generated. If a single string is used as the argument, it is assumed to be 51 // the syntax. 52 function generate_property(reg) { 53 // Verify that only valid keys are specified. This prevents the caller from 54 // accidentally supplying 'inherited' instead of 'inherits', for example. 55 if (typeof(reg) === 'object') { 56 const permitted = new Set(['name', 'syntax', 'initialValue', 'inherits']); 57 if (!Object.keys(reg).every(k => permitted.has(k))) 58 throw new Error('generate_property: invalid parameter'); 59 } 60 61 let syntax = typeof(reg) === 'string' ? reg : reg.syntax; 62 let initial = typeof(reg.initialValue) === 'undefined' ? any_initial_value(syntax) 63 : reg.initialValue; 64 let inherits = typeof(reg.inherits) === 'undefined' ? false : reg.inherits; 65 66 let name = generate_name(); 67 CSS.registerProperty({ 68 name: name, 69 syntax: syntax, 70 initialValue: initial, 71 inherits: inherits 72 }); 73 return name; 74 } 75 76 function all_syntaxes() { 77 return [ 78 '*', 79 '<angle>', 80 '<color>', 81 '<custom-ident>', 82 '<image>', 83 '<integer>', 84 '<length-percentage>', 85 '<length>', 86 '<number>', 87 '<percentage>', 88 '<resolution>', 89 '<time>', 90 '<transform-function>', 91 '<transform-list>', 92 '<url>' 93 ] 94 } 95 96 function with_style_node(text, fn) { 97 let node = document.createElement('style'); 98 node.textContent = text; 99 try { 100 document.body.append(node); 101 fn(node); 102 } finally { 103 node.remove(); 104 } 105 } 106 107 function with_at_property(desc, fn) { 108 let name = typeof(desc.name) === 'undefined' ? generate_name() : desc.name; 109 let text = `@property ${name} {`; 110 if (typeof(desc.syntax) !== 'undefined') 111 text += `syntax:${desc.syntax};`; 112 if (typeof(desc.initialValue) !== 'undefined') 113 text += `initial-value:${desc.initialValue};`; 114 if (typeof(desc.inherits) !== 'undefined') 115 text += `inherits:${desc.inherits};`; 116 text += '}'; 117 with_style_node(text, (node) => fn(name, node.sheet.rules[0])); 118 } 119 120 function test_with_at_property(desc, fn, description) { 121 test(() => with_at_property(desc, fn), description); 122 } 123 124 function test_with_style_node(text, fn, description) { 125 test(() => with_style_node(text, fn), description); 126 } 127 128 function animation_test(property, values, description) { 129 const name = generate_name(); 130 property.name = name; 131 CSS.registerProperty(property); 132 133 test(() => { 134 const duration = 1000; 135 const keyframes = {}; 136 keyframes[name] = values.keyframes; 137 138 const iterations = 3; 139 const composite = values.composite || "replace"; 140 const iterationComposite = values.iterationComposite || "replace"; 141 const animation = target.animate(keyframes, { composite, iterationComposite, iterations, duration }); 142 animation.pause(); 143 // We seek to the middle of the third iteration which will allow to test cases where 144 // iterationComposite is set to something other than "replace". 145 animation.currentTime = duration * 2.5; 146 147 const assert_equals_function = values.assert_function || assert_equals; 148 assert_equals_function(getComputedStyle(target).getPropertyValue(name), values.expected); 149 }, description); 150 }; 151 152 function discrete_animation_test(syntax, fromValue, toValue, description) { 153 test(() => { 154 const name = generate_name(); 155 156 CSS.registerProperty({ 157 name, 158 syntax, 159 inherits: false, 160 initialValue: fromValue 161 }); 162 163 const duration = 1000; 164 const keyframes = []; 165 keyframes[name] = toValue; 166 const animation = target.animate(keyframes, duration); 167 animation.pause(); 168 169 const checkAtProgress = (progress, expected) => { 170 animation.currentTime = duration * 0.25; 171 assert_equals(getComputedStyle(target).getPropertyValue(name), fromValue, `The correct value is used at progress = ${progress}`); 172 }; 173 174 checkAtProgress(0, fromValue); 175 checkAtProgress(0.25, fromValue); 176 checkAtProgress(0.49, fromValue); 177 checkAtProgress(0.5, toValue); 178 checkAtProgress(0.75, toValue); 179 checkAtProgress(1, toValue); 180 }, description || `Animating a custom property of type ${syntax} is discrete`); 181 } 182 183 function transition_test(options, description) { 184 promise_test(async () => { 185 const customProperty = generate_name(); 186 187 options.transitionProperty ??= customProperty; 188 189 CSS.registerProperty({ 190 name: customProperty, 191 syntax: options.syntax, 192 inherits: false, 193 initialValue: options.from 194 }); 195 196 assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.from, "Element has the expected initial value"); 197 198 const transitionEventPromise = new Promise(resolve => { 199 let listener = event => { 200 target.removeEventListener("transitionrun", listener); 201 assert_equals(event.propertyName, customProperty, "TransitionEvent has the expected property name"); 202 resolve(); 203 }; 204 target.addEventListener("transitionrun", listener); 205 }); 206 207 target.style.transition = `${options.transitionProperty} 1s -500ms linear`; 208 if (options.behavior) { 209 target.style.transitionBehavior = options.behavior; 210 } 211 target.style.setProperty(customProperty, options.to); 212 213 const animations = target.getAnimations(); 214 assert_equals(animations.length, 1, "A single animation is running"); 215 216 const transition = animations[0]; 217 assert_class_string(transition, "CSSTransition", "A CSSTransition is running"); 218 219 transition.pause(); 220 assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.expected, "Element has the expected animated value"); 221 222 await transitionEventPromise; 223 }, description); 224 } 225 226 function no_transition_test(options, description) { 227 test(() => { 228 const customProperty = generate_name(); 229 230 CSS.registerProperty({ 231 name: customProperty, 232 syntax: options.syntax, 233 inherits: false, 234 initialValue: options.from 235 }); 236 237 assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.from, "Element has the expected initial value"); 238 239 target.style.transition = `${customProperty} 1s -500ms linear`; 240 target.style.setProperty(customProperty, options.to); 241 242 assert_equals(target.getAnimations().length, 0, "No animation was created"); 243 assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.to, "Element has the expected final value"); 244 }, description); 245 }; 246 247 function test_initial_value_valid(syntax, initialValue) { 248 // No actual assertions, this just shouldn't throw 249 test(() => { 250 var name = generate_name(); 251 CSS.registerProperty({name: name, syntax: syntax, initialValue: initialValue, inherits: false}); 252 }, "syntax:'" + syntax + "', initialValue:'" + initialValue + "' is valid"); 253 } 254 255 function test_initial_value_invalid(syntax, initialValue) { 256 test(() =>{ 257 var name = generate_name(); 258 assert_throws_dom("SyntaxError", 259 () => CSS.registerProperty({name: name, syntax: syntax, initialValue: initialValue, inherits: false})); 260 }, "syntax:'" + syntax + "', initialValue:'" + initialValue + "' is invalid"); 261 }