chromium/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-animation.html

<!DOCTYPE html>
<link rel="help" href="https://drafts.css-houdini.org/css-properties-values-api-1">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/utils.js"></script>
<div id=outer>
  <div id=div></div>
</div>
<script>

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  with_style_node(`
    @keyframes test {
      from { ${name}: 100px; }
      to { ${name}: 200px; }
    }
    #div { animation: test 100s -50s linear; }
  `, () => {
    assert_equals(getComputedStyle(div).getPropertyValue(name), '150px');
  });
}, '@keyframes works with @property');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  with_style_node(`
    @property ${name} {
      syntax: "<color>";
      inherits: false;
      initial-value: black;
    }
    @keyframes test {
      from { ${name}: rgb(100, 100, 100); }
      to { ${name}: rgb(200, 200, 200); }
    }
    #div { animation: test 100s -50s linear; }
  `, () => {
    assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(150, 150, 150)');
  });
}, '@keyframes picks up the latest @property in the document');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  // These keyframes are initially invalid for the declared custom property.
  let animation = div.animate([
    { [name]: 'rgb(100, 100, 100)'},
    { [name]: 'rgb(200, 200, 200)'},
  ], { duration: 10000, delay: -5000, easing: 'linear' });
  let cs = getComputedStyle(div);
  assert_equals(cs.getPropertyValue(name), '0px');

  // Redeclare the property as a <color>, effectively making the existing
  // keyframes valid.
  with_at_property({
    name: name,
    syntax: '"<color>"',
    inherits: false,
    initialValue: 'black'
  }, (name) => {
    assert_equals(cs.getPropertyValue(name), 'rgb(150, 150, 150)');
  });

  animation.finish();
}, 'Ongoing animation picks up redeclared custom property');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  // These keyframes are initially invalid for the declared custom property.
  let animation = div.animate([
    { [name]: 'rgb(100, 100, 100)'},
    { [name]: 'rgb(200, 200, 200)'},
  ], { duration: 10000, delay: -5000, easing: 'linear' });
  let cs = getComputedStyle(div);
  assert_equals(cs.getPropertyValue(name), '0px');

  // Setting the keyframes to something that matches <length> makes the
  // interpolation valid.
  animation.effect.setKeyframes([
    {[name]: '100px'},
    {[name]: '200px'}
  ]);
  assert_equals(cs.getPropertyValue(name), '150px');

  animation.finish();
}, 'Ongoing animation matches new keyframes against the current registration');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  let animation = div.animate([
    { [name]: 'initial'},
    { [name]: '400px'},
  ], { duration: 10000, delay: -5000, easing: 'linear' });
  let cs = getComputedStyle(div);
  assert_equals(cs.getPropertyValue(name), '200px');

  // Change initial value.
  with_at_property({
    name: name,
    syntax: '"<length>"',
    inherits: false,
    initialValue: '100px'
  }, (name) => {
    assert_equals(cs.getPropertyValue(name), '250px');
  });

  animation.finish();
}, 'Ongoing animation picks up redeclared intial value');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  try {
    document.body.style = `${name}: 100px`;
    // Note that 'inherit' here refers to #outer, which has the initial
    // value. (#outer did not inherit from body, since the property is not
    // yet declared as inherited).
    let animation = div.animate([
      { [name]: 'inherit'},
      { [name]: '400px'},
    ], { duration: 10000, delay: -5000, easing: 'linear' });
    let cs = getComputedStyle(div);
    assert_equals(cs.getPropertyValue(name), '200px');

    // Change inherits to 'true'. The value should now propagate from body
    // to #outer.
    with_at_property({
      name: name,
      syntax: '"<length>"',
      inherits: true,
      initialValue: '0px'
    }, (name) => {
      assert_equals(cs.getPropertyValue(name), '250px');
    });

    animation.finish();
  } finally {
    document.body.style = '';
  }
}, 'Ongoing animation picks up redeclared inherits flag');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  try {
    outer.style = `${name}: 100px`;
    // 'unset' should take the initial value (not the value from #outer), since
    // the property is not declared as inherited.
    let animation = div.animate([
      { [name]: 'unset'},
      { [name]: '400px'},
    ], { duration: 10000, delay: -5000, easing: 'linear' });
    let cs = getComputedStyle(div);
    assert_equals(cs.getPropertyValue(name), '200px');

    // Change inherits to 'true'. 'unset' now refers to #outer's value.
    with_at_property({
      name: name,
      syntax: '"<length>"',
      inherits: true,
      initialValue: '0px'
    }, (name) => {
      assert_equals(cs.getPropertyValue(name), '250px');
    });

    animation.finish();
  } finally {
    outer.style = '';
  }
}, 'Ongoing animation picks up redeclared meaning of \'unset\'');

test_with_at_property({
  syntax: '"<color>"',
  inherits: false,
  initialValue: 'red'
}, (name) => {
  try {
    assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(255, 0, 0)');
    div.style = `transition: ${name} steps(2, start) 100s; ${name}: blue`;
    assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(128, 0, 128)');
  } finally {
    div.style = '';
  }
}, 'Transitioning from initial value');

test_with_at_property({
  syntax: '"<color>"',
  inherits: false,
  initialValue: 'red'
}, (name) => {
  try {
    div.style = `${name}: blue;`;
    assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(0, 0, 255)');
    div.style = `transition: ${name} steps(2, start) 100s; ${name}: green`;
    assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(0, 64, 128)');
  } finally {
    div.style = '';
  }
}, 'Transitioning from specified value');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '100px'
}, (name) => {
    with_style_node(`div { transition: ${name} steps(2, start) 100s; }`, () => {
      assert_equals(getComputedStyle(div).getPropertyValue(name), '100px');
      // Re-declaring the property with a different initial value effectively
      // means the computed value has changed. This means we should transition
      // from the old initial value to the new initial value.
      with_at_property({
        name: name,
        syntax: '"<length>"',
        inherits: false,
        initialValue: '200px'
      }, () => {
        assert_equals(getComputedStyle(div).getPropertyValue(name), '150px');
      });
    });
}, 'Transition triggered by initial value change');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '100px'
}, (name) => {
    with_style_node(`div { transition: ${name} steps(2, start) 100s; }`, () => {
      assert_equals(getComputedStyle(div).getPropertyValue(name), '100px');
      with_at_property({
        name: name,
        syntax: '"<color>"',
        inherits: false,
        initialValue: 'green'
      }, () => {
        assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(0, 128, 0)');
      });
    });
}, 'No transition when changing types');

test(() => {
  let name = generate_name();
  with_style_node(`div { ${name}: 100px; transition: ${name} steps(2, start) 100s; }`, () => {
    assert_equals(getComputedStyle(div).getPropertyValue(name), '100px');

    let style1 = document.createElement('style');
    style1.textContent = `
      @property ${name} {
        syntax: "<length>";
        inherits: false;
        initial-value: 200px;
      }
    `;

    let style2 = document.createElement('style');
    style2.textContent = `div { ${name}: 400px; }`;

    try {
      // Register the property:
      document.body.append(style1);
      // The token sequence ' 100px' is now interpreted as a length '100px'.
      assert_equals(getComputedStyle(div).getPropertyValue(name), '100px');

      // Change the computed value:
      document.body.append(style2);
      // This should cause an interpolation between 100px and 400px:
      assert_equals(getComputedStyle(div).getPropertyValue(name), '250px');

      // In the middle of the transition above, remove the @property rule
      // (making the computed value a token sequence again). We should snap
      // to the new token sequence.
      style1.remove();
      assert_equals(getComputedStyle(div).getPropertyValue(name), '400px');
    } finally {
      style1.remove();
      style2.remove();
    }
  });
}, 'No transition when removing @property rule');

test(() => {
  let name = generate_name();
  with_style_node(`div { transition: ${name} steps(2, start) 100s; }`, () => {
    let style1 = document.createElement('style');
    style1.textContent = `
      @property ${name} {
        syntax: "<angle>";
        inherits: false;
        initial-value: -90deg;
      }
    `;

    let style2 = document.createElement('style');
    style2.textContent = `div { ${name}: 90deg; }`;

    try {
      // Register the property:
      document.body.append(style1);
      // No transition in the beginning.
      assert_equals(getComputedStyle(div).getPropertyValue(name), '-90deg');

      // Change the computed value:
      document.body.append(style2);
      // This should cause an interpolation between -90deg and 90deg (for more
      // specifically, it is 50% from -90deg to 90deg, with steps(2, start)).
      assert_equals(getComputedStyle(div).getPropertyValue(name), '0deg');

      // In the middle of the transition above, remove style2, which creates
      // a transition which reverses the existing one, from 0deg to -90deg.
      // Also, it is 50% from 0deg to -90deg, with steps(2, start).
      style2.remove();
      assert_equals(getComputedStyle(div).getPropertyValue(name), '-45deg');
    } finally {
      style1.remove();
      style2.remove();
    }
  });
}, 'Ongoing transition reverts to its initial state');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  with_style_node(`
    @keyframes test {
      from { ${name}: 100px; }
      to { ${name}: 200px; }
    }
    #div {
        animation: test 100s -50s linear;
        --unregistered: var(${name});
    }
  `, () => {
    assert_equals(getComputedStyle(div).getPropertyValue('--unregistered'), '150px');
  });
}, 'Unregistered properties referencing animated properties update correctly.');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  with_style_node(`
    @keyframes test {
      from { ${name}: 100px; }
      to { ${name}: 200px; }
    }
    @property --registered {
        syntax: "<length>";
        inherits: false;
        initialValue: 0px;
    }
    #div {
        animation: test 100s -50s linear;
        --registered: var(${name});
    }
  `, () => {
    assert_equals(getComputedStyle(div).getPropertyValue('--registered'), '150px');
  });
}, 'Registered properties referencing animated properties update correctly.');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  with_style_node(`
    @keyframes test {
      from { ${name}: inherit; }
      to { ${name}: 300px; }
    }
    #outer {
      ${name}: 100px;
    }
    #div {
      animation: test 100s -50s linear paused;
    }
  `, () => {
    assert_equals(getComputedStyle(div).getPropertyValue(name), '200px');

    outer.style.setProperty(name, '200px');
    assert_equals(getComputedStyle(div).getPropertyValue(name), '250px');

    outer.style.setProperty(name, '0px');
    assert_equals(getComputedStyle(div).getPropertyValue(name), '150px');

    outer.style.removeProperty(name);
  });
}, 'CSS animation setting "inherit" for a custom property on a keyframe is responsive to changing that custom property on the parent.');

test_with_at_property({
  syntax: '"<length>"',
  inherits: false,
  initialValue: '0px'
}, (name) => {
  with_style_node(`
    #outer {
      ${name}: 100px;
    }
  `, () => {
    const animation = div.animate({ [name]: ["inherit", "300px"]}, 1000);
    animation.currentTime = 500;
    animation.pause();

    assert_equals(getComputedStyle(div).getPropertyValue(name), '200px');

    outer.style.setProperty(name, '200px');
    assert_equals(getComputedStyle(div).getPropertyValue(name), '250px');

    outer.style.setProperty(name, '0px');
    assert_equals(getComputedStyle(div).getPropertyValue(name), '150px');

    outer.style.removeProperty(name);
  });
}, 'JS-originated animation setting "inherit" for a custom property on a keyframe is responsive to changing that custom property on the parent.');

test_with_at_property({
  syntax: '"<color>"',
  inherits: false,
  initialValue: 'black'
}, (name) => {
  with_style_node(`
    @keyframes test {
      from { ${name}: currentcolor; }
      to { ${name}: rgb(200, 200, 200); }
    }
    #outer {
      color: rgb(100, 100, 100);
    }
    #div {
      animation: test 100s -50s linear paused;
    }
  `, (style) => {
    // See https://github.com/w3c/csswg-drafts/issues/10371 for the two
    // possibilities.
    assert_in_array(getComputedStyle(div).getPropertyValue(name), [
      'color-mix(in srgb, currentcolor, rgb(200, 200, 200))',
      'rgb(150, 150, 150)',
    ]);

    outer.style.color = 'rgb(50, 50, 50)';
    assert_in_array(getComputedStyle(div).getPropertyValue(name), [
      'color-mix(in srgb, currentcolor, rgb(200, 200, 200))',
      'rgb(125, 125, 125)',
    ]);

    outer.style.color = 'rgb(150, 150, 150)';
    assert_in_array(getComputedStyle(div).getPropertyValue(name), [
      'color-mix(in srgb, currentcolor, rgb(200, 200, 200))',
      'rgb(175, 175, 175)',
    ]);

    outer.style.removeProperty("color");
  });
}, 'CSS animation setting "currentColor" for a custom property on a keyframe is responsive to changing "color" on the parent.');

test_with_at_property({
  syntax: '"<color>"',
  inherits: false,
  initialValue: 'black'
}, (name) => {
  with_style_node(`
    #outer {
      color: rgb(100, 100, 100);
    }
  `, () => {
    const animation = div.animate({ [name]: ["currentcolor", "rgb(200, 200, 200)"]}, 1000);
    animation.currentTime = 500;
    animation.pause();

    assert_in_array(getComputedStyle(div).getPropertyValue(name), [
      'color-mix(in srgb, currentcolor, rgb(200, 200, 200))',
      'rgb(150, 150, 150)',
    ]);

    outer.style.color = 'rgb(50, 50, 50)';
    assert_in_array(getComputedStyle(div).getPropertyValue(name), [
      'color-mix(in srgb, currentcolor, rgb(200, 200, 200))',
      'rgb(125, 125, 125)',
    ]);

    outer.style.color = 'rgb(150, 150, 150)';
    assert_in_array(getComputedStyle(div).getPropertyValue(name), [
      'color-mix(in srgb, currentcolor, rgb(200, 200, 200))',
      'rgb(175, 175, 175)',
    ]);

    outer.style.removeProperty("color");
  });
}, 'JS-originated animation setting "currentColor" for a custom property on a keyframe is responsive to changing "color" on the parent.');

</script>