[@layer] Use layer order to resolve @property and @scroll-timeline name conflicts

This patch adds a new class AtRuleCascadeMap, a miniature CascadeMap,
to cascade @property and @scroll-time rules across origins and cascade
layers.

We can't use it to cascade other name-defining at-rules because:
- @font-face rules with the same 'font-family' may co-exist
- @counter-style and @keyframes rules are managed per tree scope, and
  allow the "append-only" update in Blink

Bug: 1095765
Change-Id: Iec5dad0b932b1aed6e9c18bca9da1443e2d1fd88
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3152514
Commit-Queue: Xiaocheng Hu <xiaochengh@chromium.org>
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Cr-Commit-Position: refs/heads/main@{#920947}
diff --git a/css/css-cascade/layer-property-override.html b/css/css-cascade/layer-property-override.html
new file mode 100644
index 0000000..f0f8d83
--- /dev/null
+++ b/css/css-cascade/layer-property-override.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>Resolving @property name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#target, #reference {
+  width: 100px;
+  height: 100px;
+}
+
+#reference {
+  background-color: green;
+}
+</style>
+
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, background color of #target should be green, same as #reference
+
+const testCases = [
+  {
+    title: '@property layered overrides unlayered',
+    style: `
+      #target {
+        background-color: var(--foo);
+      }
+
+      @layer {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: green;
+        }
+      }
+
+      @property --foo {
+        syntax: '<color>';
+        inherits: false;
+        initial-value: red;
+      }
+    `
+  },
+
+  {
+    title: '@property override between layers',
+    style: `
+      @layer base, override;
+
+      #target {
+        background-color: var(--foo);
+      }
+
+      @layer override {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: green;
+        }
+      }
+
+      @layer base {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: red;
+        }
+      }
+    `
+  },
+
+  {
+    title: '@property override update with appended sheet 1',
+    style: `
+      @layer base, override;
+
+      #target {
+        background-color: var(--foo);
+      }
+
+      @layer override {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: green;
+        }
+      }
+    `,
+    append: `
+      @layer base {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: red;
+        }
+      }
+    `
+  },
+
+  {
+    title: '@property override update with appended sheet 2',
+    style: `
+      @layer base, override;
+
+      #target {
+        background-color: var(--foo);
+      }
+
+      @layer base {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: red;
+        }
+      }
+    `,
+    append: `
+      @layer override {
+        @property --foo {
+          syntax: '<color>';
+          inherits: false;
+          initial-value: green;
+        }
+      }
+    `
+  },
+];
+
+for (let testCase of testCases) {
+  var documentStyle = document.createElement('style');
+  documentStyle.appendChild(document.createTextNode(testCase['style']));
+  document.head.appendChild(documentStyle);
+
+  var appendedStyle;
+  if (testCase['append']) {
+    document.body.offsetLeft;  // Force style update
+    appendedStyle = document.createElement('style');
+    appendedStyle.appendChild(document.createTextNode(testCase['append']));
+    document.head.appendChild(appendedStyle);
+  }
+
+  test(function () {
+    assert_equals(getComputedStyle(target).backgroundColor,
+                  getComputedStyle(reference).backgroundColor);
+  }, testCase['title']);
+
+  if (appendedStyle)
+    appendedStyle.remove();
+  documentStyle.remove();
+}
+</script>
diff --git a/css/css-cascade/layer-scroll-timeline-override.html b/css/css-cascade/layer-scroll-timeline-override.html
new file mode 100644
index 0000000..9a50914
--- /dev/null
+++ b/css/css-cascade/layer-scroll-timeline-override.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<title>Resolving @scroll-timeline name conflicts with cascade layers</title>
+<link rel="help" href="https://drafts.csswg.org/css-cascade-5/#layering">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+#scroller {
+  overflow: scroll;
+  width: 100px;
+  height: 100px;
+}
+
+#scroller div {
+  height: 200px;
+}
+
+@keyframes expand {
+  from { width: 100px; }
+  to { width: 200px; }
+}
+
+#target, #reference {
+  height: 100px;
+}
+
+#reference {
+  width: 150px;
+}
+
+#target {
+  animation: expand 10s linear;
+  height: 100px;
+}
+</style>
+
+<div id="scroller">
+  <div></div>
+</div>
+<div id="target"></div>
+<div id="reference"></div>
+
+<script>
+// In all tests, width of #target should be 150px, same as #reference
+
+const testCases = [
+  {
+    title: '@scroll-timeline layered overrides unlayered',
+    style: `
+      #target {
+        animation-timeline: timeline;
+      }
+
+      @layer {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 50px;
+        }
+      }
+
+      @scroll-timeline timeline {
+        source: selector(#scroller);
+        start: 0px;
+        end: 100px;
+      }
+    `
+  },
+
+  {
+    title: '@scroll-timeline override between layers',
+    style: `
+      @layer base, override;
+
+      #target {
+        animation-timeline: timeline;
+      }
+
+      @layer override {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 50px;
+        }
+      }
+
+      @layer base {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 100px;
+        }
+      }
+    `
+  },
+
+  {
+    title: '@scroll-timeline override update with appended sheet 1',
+    style: `
+      @layer base, override;
+
+      #target {
+        animation-timeline: timeline;
+      }
+
+      @layer override {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 50px;
+        }
+      }
+    `,
+    append: `
+      @layer base {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 100px;
+        }
+      }
+    `
+  },
+
+  {
+    title: '@scroll-timeline override update with appended sheet 2',
+    style: `
+      @layer base, override;
+
+      #target {
+        animation-timeline: timeline;
+      }
+
+      @layer base {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 100px;
+        }
+      }
+    `,
+    append: `
+      @layer override {
+        @scroll-timeline timeline {
+          source: selector(#scroller);
+          start: 0px;
+          end: 50px;
+        }
+      }
+    `
+  },
+];
+
+for (let testCase of testCases) {
+  promise_test(async function() {
+    assert_true(
+      CSS.supports('animation-timeline', 'foo'),
+      'This test requires @scroll-timeline support');
+
+    var documentStyle = document.createElement('style');
+    documentStyle.appendChild(document.createTextNode(testCase['style']));
+    document.head.appendChild(documentStyle);
+
+    var appendedStyle;
+    if (testCase['append']) {
+      document.body.offsetLeft;  // Force style update
+      appendedStyle = document.createElement('style');
+      appendedStyle.appendChild(document.createTextNode(testCase['append']));
+      document.head.appendChild(appendedStyle);
+    }
+
+    scroller.scrollTop = 25;
+    await waitForNextFrame();
+    assert_equals(getComputedStyle(target).width,
+                  getComputedStyle(reference).width);
+
+    if (appendedStyle)
+      appendedStyle.remove();
+    documentStyle.remove();
+  }, testCase['title']);
+}
+</script>