| let next_property_id = 1; |
| |
| // Generate a unique property name on the form --prop-N. |
| function generate_name() { |
| return `--prop-${next_property_id++}`; |
| } |
| |
| // Produce a compatible initial value for the specified syntax. |
| function any_initial_value(syntax) { |
| let components = syntax.split('|').map(x => x.trim()) |
| let first_component = components[0]; |
| |
| if (first_component.endsWith('+') || first_component.endsWith('#')) |
| first_component = first_component.slice(0, -1); |
| |
| switch (first_component) { |
| case '*': |
| case '<custom-ident>': |
| return 'NULL'; |
| case '<angle>': |
| return '0deg'; |
| case '<color>': |
| return 'rgb(0, 0, 0)'; |
| case '<image>': |
| case '<url>': |
| return 'url(0)'; |
| case '<integer>': |
| case '<length-percentage>': |
| case '<length>': |
| case '<number>': |
| return '0'; |
| case '<percentage>': |
| return '0%'; |
| case '<resolution>': |
| return '0dpi'; |
| case '<time>': |
| return '0s'; |
| case '<transform-function>': |
| case '<transform-list>': |
| return 'matrix(0, 0, 0, 0, 0, 0)'; |
| default: |
| // We assume syntax is a specific custom ident. |
| return first_component; |
| } |
| } |
| |
| // Registers a unique property on the form '--prop-N' and returns the name. |
| // Any value except 'syntax' may be omitted, in which case the property will |
| // not inherit, and some undefined (but compatible) initial value will be |
| // generated. If a single string is used as the argument, it is assumed to be |
| // the syntax. |
| function generate_property(reg) { |
| // Verify that only valid keys are specified. This prevents the caller from |
| // accidentally supplying 'inherited' instead of 'inherits', for example. |
| if (typeof(reg) === 'object') { |
| const permitted = new Set(['name', 'syntax', 'initialValue', 'inherits']); |
| if (!Object.keys(reg).every(k => permitted.has(k))) |
| throw new Error('generate_property: invalid parameter'); |
| } |
| |
| let syntax = typeof(reg) === 'string' ? reg : reg.syntax; |
| let initial = typeof(reg.initialValue) === 'undefined' ? any_initial_value(syntax) |
| : reg.initialValue; |
| let inherits = typeof(reg.inherits) === 'undefined' ? false : reg.inherits; |
| |
| let name = generate_name(); |
| CSS.registerProperty({ |
| name: name, |
| syntax: syntax, |
| initialValue: initial, |
| inherits: inherits |
| }); |
| return name; |
| } |
| |
| function all_syntaxes() { |
| return [ |
| '*', |
| '<angle>', |
| '<color>', |
| '<custom-ident>', |
| '<image>', |
| '<integer>', |
| '<length-percentage>', |
| '<length>', |
| '<number>', |
| '<percentage>', |
| '<resolution>', |
| '<time>', |
| '<transform-function>', |
| '<transform-list>', |
| '<url>' |
| ] |
| } |
| |
| function with_style_node(text, fn) { |
| let node = document.createElement('style'); |
| node.textContent = text; |
| try { |
| document.body.append(node); |
| fn(node); |
| } finally { |
| node.remove(); |
| } |
| } |
| |
| function with_at_property(desc, fn) { |
| let name = typeof(desc.name) === 'undefined' ? generate_name() : desc.name; |
| let text = `@property ${name} {`; |
| if (typeof(desc.syntax) !== 'undefined') |
| text += `syntax:${desc.syntax};`; |
| if (typeof(desc.initialValue) !== 'undefined') |
| text += `initial-value:${desc.initialValue};`; |
| if (typeof(desc.inherits) !== 'undefined') |
| text += `inherits:${desc.inherits};`; |
| text += '}'; |
| with_style_node(text, (node) => fn(name, node.sheet.rules[0])); |
| } |
| |
| function test_with_at_property(desc, fn, description) { |
| test(() => with_at_property(desc, fn), description); |
| } |
| |
| function test_with_style_node(text, fn, description) { |
| test(() => with_style_node(text, fn), description); |
| } |
| |
| function animation_test(property, values, description) { |
| const name = generate_name(); |
| property.name = name; |
| CSS.registerProperty(property); |
| |
| test(() => { |
| const duration = 1000; |
| const keyframes = {}; |
| keyframes[name] = values.keyframes; |
| |
| const iterations = 3; |
| const composite = values.composite || "replace"; |
| const iterationComposite = values.iterationComposite || "replace"; |
| const animation = target.animate(keyframes, { composite, iterationComposite, iterations, duration }); |
| animation.pause(); |
| // We seek to the middle of the third iteration which will allow to test cases where |
| // iterationComposite is set to something other than "replace". |
| animation.currentTime = duration * 2.5; |
| |
| const assert_equals_function = values.assert_function || assert_equals; |
| assert_equals_function(getComputedStyle(target).getPropertyValue(name), values.expected); |
| }, description); |
| }; |
| |
| function discrete_animation_test(syntax, fromValue, toValue, description) { |
| test(() => { |
| const name = generate_name(); |
| |
| CSS.registerProperty({ |
| name, |
| syntax, |
| inherits: false, |
| initialValue: fromValue |
| }); |
| |
| const duration = 1000; |
| const keyframes = []; |
| keyframes[name] = toValue; |
| const animation = target.animate(keyframes, duration); |
| animation.pause(); |
| |
| const checkAtProgress = (progress, expected) => { |
| animation.currentTime = duration * 0.25; |
| assert_equals(getComputedStyle(target).getPropertyValue(name), fromValue, `The correct value is used at progress = ${progress}`); |
| }; |
| |
| checkAtProgress(0, fromValue); |
| checkAtProgress(0.25, fromValue); |
| checkAtProgress(0.49, fromValue); |
| checkAtProgress(0.5, toValue); |
| checkAtProgress(0.75, toValue); |
| checkAtProgress(1, toValue); |
| }, description || `Animating a custom property of type ${syntax} is discrete`); |
| } |
| |
| function transition_test(options, description) { |
| promise_test(async () => { |
| const customProperty = generate_name(); |
| |
| options.transitionProperty ??= customProperty; |
| |
| CSS.registerProperty({ |
| name: customProperty, |
| syntax: options.syntax, |
| inherits: false, |
| initialValue: options.from |
| }); |
| |
| assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.from, "Element has the expected initial value"); |
| |
| const transitionEventPromise = new Promise(resolve => { |
| let listener = event => { |
| target.removeEventListener("transitionrun", listener); |
| assert_equals(event.propertyName, customProperty, "TransitionEvent has the expected property name"); |
| resolve(); |
| }; |
| target.addEventListener("transitionrun", listener); |
| }); |
| |
| target.style.transition = `${options.transitionProperty} 1s -500ms linear`; |
| if (options.behavior) { |
| target.style.transitionBehavior = options.behavior; |
| } |
| target.style.setProperty(customProperty, options.to); |
| |
| const animations = target.getAnimations(); |
| assert_equals(animations.length, 1, "A single animation is running"); |
| |
| const transition = animations[0]; |
| assert_class_string(transition, "CSSTransition", "A CSSTransition is running"); |
| |
| transition.pause(); |
| assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.expected, "Element has the expected animated value"); |
| |
| await transitionEventPromise; |
| }, description); |
| } |
| |
| function no_transition_test(options, description) { |
| test(() => { |
| const customProperty = generate_name(); |
| |
| CSS.registerProperty({ |
| name: customProperty, |
| syntax: options.syntax, |
| inherits: false, |
| initialValue: options.from |
| }); |
| |
| assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.from, "Element has the expected initial value"); |
| |
| target.style.transition = `${customProperty} 1s -500ms linear`; |
| target.style.setProperty(customProperty, options.to); |
| |
| assert_equals(target.getAnimations().length, 0, "No animation was created"); |
| assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.to, "Element has the expected final value"); |
| }, description); |
| }; |