at-property.html (12611B)
1 <!DOCTYPE html> 2 <link rel="help" href="https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule"> 3 <script src="/resources/testharness.js"></script> 4 <script src="/resources/testharnessreport.js"></script> 5 <script src="./resources/utils.js"></script> 6 <div id="outer"> 7 <div id="target"></div> 8 </div> 9 <script> 10 11 // Parsing: 12 13 let uppercase_first = (x) => x.charAt(0).toUpperCase() + x.slice(1); 14 let to_camel_case = (x) => x.split('-')[0] + x.split('-').slice(1).map(uppercase_first).join(''); 15 16 function get_cssom_descriptor_value(rule, descriptor) { 17 switch (descriptor) { 18 case 'syntax': 19 return rule.syntax; 20 case 'inherits': 21 return rule.inherits; 22 case 'initial-value': 23 return rule.initialValue; 24 default: 25 assert_true(false, 'Should not reach here'); 26 return null; 27 } 28 } 29 30 // Test that for the given descriptor (e.g. 'syntax'), the specified value 31 // will yield the expected_value when observed using CSSOM. If the expected_value 32 // is omitted, it is the same as the specified value. 33 function test_descriptor(descriptor, specified_value, expected_value, other_descriptors) { 34 // Try and build a valid @property form the specified descriptor. 35 let at_property = { [to_camel_case(descriptor)]: specified_value }; 36 37 // If extra values are specified in other_descriptors, just use them. 38 if (typeof(other_descriptors) !== 'unspecified') { 39 for (let name in other_descriptors) { 40 if (other_descriptors.hasOwnProperty(name)) { 41 if (name == descriptor) { 42 throw `Unexpected ${name} in other_descriptors`; 43 } 44 at_property[to_camel_case(name)] = other_descriptors[name]; 45 } 46 } 47 } 48 49 if (!('syntax' in at_property)) { 50 // The syntax descriptor is required. Use the universal one as a fallback. 51 // https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor 52 at_property.syntax = '"*"'; 53 } 54 if (!('inherits' in at_property)) { 55 // The inherits descriptor is required. Make it true as a fallback. 56 // https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor 57 at_property.inherits = true; 58 } 59 if (!at_property.syntax.match(/^"\s*\*\s*"$/) && 60 !('initialValue' in at_property)) { 61 // The initial-value is required for non-universal syntax. 62 // Pick a computationally independent value that follows specified syntax. 63 // https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor 64 at_property.initialValue = (() => { 65 let first_syntax_component = specified_value 66 .replace(/^"(.*)"$/, '$1') // unquote 67 .replace(/[\s\uFEFF\xA0]+/g, ' ') // collapse whitespaces 68 .match(/^[^|\#\+]*/)[0] // pick first component 69 .trim(); 70 switch (first_syntax_component) { 71 case '<color>': return 'blue'; 72 case '<length>': return '42px'; 73 default: 74 if (first_syntax_component.startsWith('<')) { 75 throw `Unsupported data type name '${first_syntax_component}'`; 76 } 77 return first_syntax_component; // <custom-ident> 78 } 79 })(); 80 } 81 82 if (expected_value === null) { 83 test_with_at_property(at_property, (name, rule) => { 84 assert_true(!rule); 85 }, `Attribute '${descriptor}' makes the @property rule invalid for [${specified_value}]`); 86 } else { 87 if (typeof(expected_value) === 'undefined') 88 expected_value = specified_value; 89 test_with_at_property(at_property, (name, rule) => { 90 assert_equals(get_cssom_descriptor_value(rule, descriptor), expected_value); 91 }, `Attribute '${descriptor}' returns expected value for [${specified_value}]`); 92 } 93 } 94 95 // syntax 96 test_descriptor('syntax', '"<color>"', '<color>'); 97 test_descriptor('syntax', '"<color> | none"', '<color> | none'); 98 test_descriptor('syntax', '"<color># | <image> | none"', '<color># | <image> | none'); 99 test_descriptor('syntax', '"foo | <length>#"', 'foo | <length>#'); 100 test_descriptor('syntax', '"foo | bar | baz"', 'foo | bar | baz'); 101 test_descriptor('syntax', '"notasyntax"', 'notasyntax'); 102 103 // syntax: universal 104 for (const syntax of ["*", " * ", "* ", "\t*\t"]) { 105 test_descriptor('syntax', `"${syntax}"`, syntax); 106 } 107 108 // syntax: <color> value 109 test_descriptor('syntax', '"red"', "red"); // treated as <custom-ident>. 110 test_descriptor('syntax', '"rgb(255, 0, 0)"', null); 111 112 // syntax: missing quotes 113 test_descriptor('syntax', '<color>', null); 114 test_descriptor('syntax', 'foo | bar', null); 115 116 // syntax: invalid <custom-ident> 117 // https://drafts.csswg.org/css-values-4/#custom-idents 118 for (const syntax of 119 ["default", 120 "initial", 121 "inherit", 122 "unset", 123 "revert", 124 "revert-layer", 125 ]) { 126 test_descriptor('syntax', `"${syntax}"`, null); 127 test_descriptor('syntax', `"${uppercase_first(syntax)}"`, null); 128 } 129 130 // syntax: pipe between components 131 test_descriptor('syntax', '"foo bar"', null, {'initial-value': 'foo bar'}); 132 test_descriptor('syntax', '"Foo <length>"', null, {'initial-value': 'Foo 42px'}); 133 test_descriptor('syntax', '"foo, bar"', null, {'initial-value': 'foo, bar'}); 134 test_descriptor('syntax', '"<length> <percentage>"', null, {'initial-value': '42px 100%'}); 135 136 // syntax: leading bar 137 test_descriptor('syntax', '"|<length>"', null, {'initial-value': '42px'}); 138 139 // initial-value 140 test_descriptor('initial-value', '10px'); 141 test_descriptor('initial-value', 'rgb(1, 2, 3)'); 142 test_descriptor('initial-value', 'red'); 143 test_descriptor('initial-value', 'foo'); 144 test_descriptor('initial-value', 'foo(){}'); 145 146 // initial-value: not computationally independent 147 test_descriptor('initial-value', '3em', null, {'syntax': '"<length>"'}); 148 test_descriptor('initial-value', 'var(--x)', null); 149 150 // inherits 151 test_descriptor('inherits', 'true', true); 152 test_descriptor('inherits', 'false', false); 153 154 test_descriptor('inherits', 'none', null); 155 test_descriptor('inherits', '0', null); 156 test_descriptor('inherits', '1', null); 157 test_descriptor('inherits', '"true"', null); 158 test_descriptor('inherits', '"false"', null); 159 test_descriptor('inherits', 'calc(0)', null); 160 161 test_with_style_node('@property foo { }', (node) => { 162 assert_equals(node.sheet.rules.length, 0); 163 }, 'Invalid property name does not parse [foo]'); 164 165 test_with_style_node('@property -foo { }', (node) => { 166 assert_equals(node.sheet.rules.length, 0); 167 }, 'Invalid property name does not parse [-foo]'); 168 169 // Applying @property rules 170 171 function test_applied(syntax, initial, inherits, expected) { 172 test_with_at_property({ 173 syntax: `"${syntax}"`, 174 initialValue: initial, 175 inherits: inherits 176 }, (name, rule) => { 177 let actual = getComputedStyle(target).getPropertyValue(name); 178 assert_equals(actual, expected); 179 }, `Rule applied [${syntax}, ${initial}, ${inherits}]`); 180 } 181 182 function test_not_applied(syntax, initial, inherits) { 183 test_with_at_property({ 184 syntax: `"${syntax}"`, 185 initialValue: initial, 186 inherits: inherits 187 }, (name, rule) => { 188 let actual = getComputedStyle(target).getPropertyValue(name); 189 assert_equals(actual, ''); 190 }, `Rule not applied [${syntax}, ${initial}, ${inherits}]`); 191 } 192 193 // syntax, initialValue, inherits, expected 194 test_applied('*', 'foo(){}', false, 'foo(){}'); 195 test_applied('<angle>', '42deg', false, '42deg'); 196 test_applied('<angle>', '1turn', false, '360deg'); 197 test_applied('<color>', 'green', false, 'rgb(0, 128, 0)'); 198 test_applied('<color>', 'rgb(1, 2, 3)', false, 'rgb(1, 2, 3)'); 199 test_applied('<image>', 'url("http://a/")', false, 'url("http://a/")'); 200 test_applied('<integer>', '5', false, '5'); 201 test_applied('<length-percentage>', '10px', false, '10px'); 202 test_applied('<length-percentage>', '10%', false, '10%'); 203 test_applied('<length-percentage>', 'calc(10% + 10px)', false, 'calc(10% + 10px)'); 204 test_applied('<length>', '10px', false, '10px'); 205 test_applied('<number>', '2.5', false, '2.5'); 206 test_applied('<percentage>', '10%', false, '10%'); 207 test_applied('<resolution>', '50dppx', false, '50dppx'); 208 test_applied('<resolution>', '96dpi', false, '1dppx'); 209 test_applied('<time>', '10s', false, '10s'); 210 test_applied('<time>', '1000ms', false, '1s'); 211 test_applied('<transform-function>', 'rotateX(0deg)', false, 'rotateX(0deg)'); 212 test_applied('<transform-list>', 'rotateX(0deg)', false, 'rotateX(0deg)'); 213 test_applied('<transform-list>', 'rotateX(0deg) translateX(10px)', false, 'rotateX(0deg) translateX(10px)'); 214 test_applied('<url>', 'url("http://a/")', false, 'url("http://a/")'); 215 216 test_applied("<string>", "'foo bar'", false, '"foo bar"'); 217 test_applied("<string>", " 'foo bar' ", false, '"foo bar"'); 218 test_applied("<string>", `'"foo" bar'`, false, '"\\"foo\\" bar"'); 219 test_applied("<string>", '"bar baz"', false, '"bar baz"'); 220 test_applied("<string>", `"bar 'baz'"`, false, `"bar 'baz'"`); 221 test_applied("<string>+", "'foo' 'bar'", false, '"foo" "bar"'); 222 test_applied("<string>#", "'foo', 'bar'", false, '"foo", "bar"'); 223 test_applied("<string>+ | <string>#", "'foo' 'bar'", false, '"foo" "bar"'); 224 test_applied("<string>+ | <string>#", " 'foo' 'bar'", false, '"foo" "bar"'); 225 test_applied("<string>+ | <string>#", `'foo' "bar"`, false, '"foo" "bar"'); 226 test_applied("<string># | <string>+", "'foo', 'bar'", false, '"foo", "bar"'); 227 test_applied("<string># | <string>+", "'foo', 'bar' ", false, '"foo", "bar"'); 228 test_applied("<string># | <string>+", `"foo", 'bar'`, false, '"foo", "bar"'); 229 230 test_not_applied("<string>", "1px", false); 231 test_not_applied("<string>", "foo", false); 232 test_not_applied("<string>", "calc(1 + 2)", false); 233 test_not_applied("<string>", "rgb(255, 99, 71)", false); 234 test_not_applied("<string>", "'foo' 2px", false); 235 236 // inherits: true/false 237 test_applied('<color>', 'tomato', false, 'rgb(255, 99, 71)'); 238 test_applied('<color>', 'tomato', true, 'rgb(255, 99, 71)'); 239 240 test_with_at_property({ syntax: '"*"', inherits: true }, (name, rule) => { 241 try { 242 outer.style.setProperty(name, 'foo'); 243 let actual = getComputedStyle(target).getPropertyValue(name); 244 assert_equals(actual, 'foo'); 245 } finally { 246 outer.style = ''; 247 } 248 }, 'Rule applied for "*", even with no initial value'); 249 250 test_not_applied(undefined, 'green', false); 251 test_not_applied('<color>', undefined, false); 252 test_not_applied('<color>', 'green', undefined); 253 test_not_applied('<gandalf>', 'grey', false); 254 test_not_applied('gandalf', 'grey', false); 255 test_not_applied('<color>', 'notacolor', false); 256 test_not_applied('<length>', '10em', false); 257 258 test_not_applied('<transform-function>', 'translateX(1em)', false); 259 test_not_applied('<transform-function>', 'translateY(1lh)', false); 260 test_not_applied('<transform-list>', 'rotate(10deg) translateX(1em)', false); 261 test_not_applied('<transform-list>', 'rotate(10deg) translateY(1lh)', false); 262 263 // Inheritance 264 265 test_with_at_property({ 266 syntax: '"<length>"', 267 inherits: false, 268 initialValue: '0px' 269 }, (name, rule) => { 270 try { 271 outer.style = `${name}: 40px`; 272 assert_equals(getComputedStyle(outer).getPropertyValue(name), '40px'); 273 assert_equals(getComputedStyle(target).getPropertyValue(name), '0px'); 274 } finally { 275 outer.style = ''; 276 } 277 }, 'Non-inherited properties do not inherit'); 278 279 test_with_at_property({ 280 syntax: '"<length>"', 281 inherits: true, 282 initialValue: '0px' 283 }, (name, rule) => { 284 try { 285 outer.style = `${name}: 40px`; 286 assert_equals(getComputedStyle(outer).getPropertyValue(name), '40px'); 287 assert_equals(getComputedStyle(target).getPropertyValue(name), '40px'); 288 } finally { 289 outer.style = ''; 290 } 291 }, 'Inherited properties inherit'); 292 293 // Initial values 294 295 test_with_at_property({ 296 syntax: '"<color>"', 297 inherits: true, 298 initialValue: 'green' 299 }, (name, rule) => { 300 try { 301 target.style = `--x:var(${name})`; 302 assert_equals(getComputedStyle(target).getPropertyValue(name), 'rgb(0, 128, 0)'); 303 } finally { 304 target.style = ''; 305 } 306 }, 'Initial values substituted as computed value'); 307 308 test_with_at_property({ 309 syntax: '"<length>"', 310 inherits: false, 311 initialValue: undefined 312 }, (name, rule) => { 313 try { 314 target.style = `${name}: calc(1px + 1px);`; 315 assert_equals(getComputedStyle(target).getPropertyValue(name), 'calc(1px + 1px)'); 316 } finally { 317 target.style = ''; 318 } 319 }, 'Non-universal registration are invalid without an initial value'); 320 321 test_with_at_property({ 322 syntax: '"*"', 323 inherits: false, 324 initialValue: undefined 325 }, (name, rule) => { 326 try { 327 // If the registration suceeded, ${name} does *not* inherit, and hence 328 // the computed value on 'target' should be empty. 329 outer.style = `${name}: calc(1px + 1px);`; 330 assert_equals(getComputedStyle(target).getPropertyValue(name), ''); 331 } finally { 332 outer.style = ''; 333 } 334 }, 'Initial value may be omitted for universal registration'); 335 336 </script>