chromium/third_party/blink/web_tests/http/tests/intersection-observer/resources/root-bounds-popup.html

<!DOCTYPE html>
<div id="target" style="width:100px;height:100px;background-color:red;"></div>
<script>
function clientRectToJson(rect) {
  if (!rect)
    return "null";
  return {
    top: rect.top,
    right: rect.right,
    bottom: rect.bottom,
    left: rect.left
  };
}
function entryToJson(entry) {
  return {
    time: entry.time,
    callbackTime: performance.now(),
    boundingClientRect: clientRectToJson(entry.boundingClientRect),
    intersectionRect: clientRectToJson(entry.intersectionRect),
    rootBounds: clientRectToJson(entry.rootBounds),
    target: entry.target.id
  };
}

// The window that launched this popup will send a "start" message; the
// only purpose of that message is to give a return address (event.source) for
// this window to send messages back.
window.addEventListener("message", event => {
  // Do a DOM modification here to ensure that this window will generate a
  // frame after each message received.
  var d = document.createElement("d");
  d.innerHTML = "Received '" + event.data + "' message";
  document.body.appendChild(d);
  if (event.data == "start") {
    let observer = new IntersectionObserver(entries => {
      entries.map(e => { event.source.postMessage(entryToJson(e), "*") });
    });
    observer.observe(document.getElementById("target"));
    requestAnimationFrame(() => {
      event.source.postMessage("start", "*");
    });
  }
});
</script>