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; assert_equals(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 => { target.addEventListener("transitionrun", event => { assert_equals(event.propertyName, customProperty, "TransitionEvent has the expected property name"); resolve(); }); }); target.style.transition = `${options.transitionProperty} 1s -500ms linear`; 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); };