js-dojo

4 who owns the document

GOAL

open the same widget's overlay twice - once from a custom element, once from inside a frame - and prove by containment alone which document each backdrop ended up in.

CONCEPT

a custom element has no document of its own. The `document` it reaches for is the host page's document, so a backdrop it appends to document.body becomes one of the host's own nodes. A frame carries a second document; a node appended inside frame.contentDocument.body never enters the host's at all.

HINT

containment, not appearance. Ask which document object made the node and whose body it was appended to - .ownerDocument and .contains() answer both, and neither needs a pixel.

MIRRORS

the dimming backdrop a third-party widget puts behind its overlay. Shipped as a custom element it dims the whole host page, because the backdrop hangs off the host's own body. Shipped in a frame - a legacy admin screen, say - it dims the box the widget was given and nothing else.

Run

This koan renders React, so it needs a one-time setup.

pnpm koan 10-embedded-mods/04-who-owns-the-document.tsx

Source

// DOJO · Module 10 / Exercise 4 — who owns the document
// GOAL: open the same widget's overlay twice - once from a custom
//       element, once from inside a frame - and prove by containment
//       alone which document each backdrop ended up in.
// CONCEPT: a custom element has no document of its own. The `document`
//       it reaches for is the host page's document, so a backdrop it
//       appends to document.body becomes one of the host's own nodes.
//       A frame carries a second document; a node appended inside
//       frame.contentDocument.body never enters the host's at all.
// HINT: containment, not appearance. Ask which document object made
//       the node and whose body it was appended to - .ownerDocument
//       and .contains() answer both, and neither needs a pixel.
// MIRRORS: the dimming backdrop a third-party widget puts behind its
//       overlay. Shipped as a custom element it dims the whole host
//       page, because the backdrop hangs off the host's own body.
//       Shipped in a frame - a legacy admin screen, say - it dims the
//       box the widget was given and nothing else.
// Run: pnpm koan 10-embedded-mods/04-who-owns-the-document.tsx

import type { HTMLAttributes } from "react";
import { afterEach, describe, expect, it } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";

afterEach(() => {
  cleanup();
  // cleanup() unmounts the React tree. A node the widget hung straight
  // off the host's body is not in that tree, so the fixture sweeps
  // those up itself between exercises.
  for (const node of document.querySelectorAll("[data-backdrop]")) {
    node.remove();
  }
});

// Given: JSX only accepts a tag it has been told about.
declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "report-widget": HTMLAttributes<HTMLElement>;
    }
  }
}

// The widget another team ships, as a custom element: somebody else's
// class, running inside the host app's own page. The host writes the
// tag, nothing more.
class ReportWidget extends HTMLElement {
  // --- TODO 1 --------------------------------------------------------
  // BROKEN: the backdrop is built and then dropped on the floor. It is
  // handed back without ever being appended, so nothing dims.
  // Append it to the body of the document this element is running in.
  // There is only one document in reach here: inside a custom element
  // `document` IS the host app's document, the very one the page's own
  // nodes came from.
  openOverlay(): HTMLElement {
    const backdrop = document.createElement("div");
    backdrop.dataset.backdrop = "report-widget";
    return backdrop;
  }
}

customElements.define("report-widget", ReportWidget);

// Given: the frame's own document, or a loud failure if it has none.
function documentOf(frame: HTMLIFrameElement): Document {
  const frameDocument = frame.contentDocument;
  if (frameDocument === null) {
    throw new Error("the frame carries no document");
  }
  return frameDocument;
}

// The frame-side copy of the same widget, opening the same overlay.
function openOverlayInFrame(frame: HTMLIFrameElement): HTMLElement {
  // --- TODO 2 --------------------------------------------------------
  // BROKEN: builds the backdrop out of the HOST's document and hangs
  // it on the HOST's body, so this overlay dims the whole page instead
  // of the frame the widget was handed. Both halves - who creates the
  // node, and whose body it is appended to - have to come from the
  // frame's document, which documentOf(frame) hands you.
  void frame; // never asks the frame for a document at all
  const backdrop = document.createElement("div");
  backdrop.dataset.backdrop = "report-frame";
  document.body.appendChild(backdrop);
  return backdrop;
}

// Given: the host app's page. It gives the custom-element widget a
// place in its own tree and the framed copy a frame. Nothing here is
// broken.
function HostApp() {
  return (
    <div data-testid="host-app">
      <report-widget data-testid="report-widget" />
      <iframe title="embed frame" />
    </div>
  );
}

// --- TODO 3 ----------------------------------------------------------
// Both copies of the widget have opened their overlay: the custom
// element and the framed one. Once TODO 1 and TODO 2 are right, count
// the backdrops the HOST's document is carrying - those are the ones a
// fixed backdrop would dim the whole page with - and write that number
// down.
const BACKDROPS_IN_HOST_DOCUMENT: number | null = null;

describe("Module 10 / Exercise 4 — who owns the document", () => {
  it("TODO 1 — the backdrop lands in the host's document", () => {
    render(<HostApp />);
    const widget = screen.getByTestId("report-widget") as ReportWidget;

    const backdrop = widget.openOverlay();

    expect(
      document.body.contains(backdrop),
      "TODO 1 — the backdrop has to hang off the host's own body",
    ).toBe(true);
    expect(
      backdrop.ownerDocument,
      "TODO 1 — a custom element builds nodes in the host's document",
    ).toBe(screen.getByTestId("host-app").ownerDocument);
  });

  it("TODO 2 — the framed backdrop stays inside the frame", () => {
    render(<HostApp />);
    const frame = screen.getByTitle("embed frame") as HTMLIFrameElement;
    const frameDocument = documentOf(frame);

    const backdrop = openOverlayInFrame(frame);

    expect(
      frameDocument.body.contains(backdrop),
      "TODO 2 — the backdrop belongs to the frame's own body",
    ).toBe(true);
    expect(
      document.body.contains(backdrop),
      "TODO 2 — the host's document must not contain it at all",
    ).toBe(false);
    expect(
      backdrop.ownerDocument,
      "TODO 2 — build it out of the frame's document, not the host's",
    ).toBe(frameDocument);
  });

  it("TODO 3 — count what the host's document is carrying", () => {
    render(<HostApp />);
    const widget = screen.getByTestId("report-widget") as ReportWidget;
    const frame = screen.getByTitle("embed frame") as HTMLIFrameElement;

    widget.openOverlay();
    openOverlayInFrame(frame);

    // One backdrop per copy, and a query never reaches into a frame. So
    // the host's share is decided by how many widgets run as elements
    // in the host page - not by how many overlays were opened in total.
    expect(
      BACKDROPS_IN_HOST_DOCUMENT,
      "TODO 3 — count the backdrops inside the host's document",
    ).toBe(document.querySelectorAll("report-widget").length);
  });
});

Solution