js-dojo

4 who owns the document

GOAL

open the same mod 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 game'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 pause overlay a mod throws over the game. Shipped as a custom element it dims the whole HUD, because its backdrop hangs off the host's own body. Shipped in a frame it dims the panel the mod was given and nothing else - the rest of the HUD never hears about it.

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 mod 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 game'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 pause overlay a mod throws over the game. Shipped as a
//       custom element it dims the whole HUD, because its backdrop
//       hangs off the host's own body. Shipped in a frame it dims the
//       panel the mod was given and nothing else - the rest of the HUD
//       never hears about it.
// 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 a mod 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 {
      "mod-panel": HTMLAttributes<HTMLElement>;
    }
  }
}

// The mod, shipped as a custom element: somebody else's class, running
// inside the game's own page. The host writes the tag, nothing more.
class ModPanel 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 game's document, the very one the HUD's own
  // nodes came from.
  openOverlay(): HTMLElement {
    const backdrop = document.createElement("div");
    backdrop.dataset.backdrop = "mod-panel";
    return backdrop;
  }
}

customElements.define("mod-panel", ModPanel);

// 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 mod, 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 game instead
  // of the panel the mod 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 = "mod-frame";
  document.body.appendChild(backdrop);
  return backdrop;
}

// Given: the game's shell. It gives the custom-element mod a place in
// its own tree and the framed mod a frame. Nothing here is broken.
function Hud() {
  return (
    <div data-testid="hud">
      <mod-panel data-testid="mod-panel" />
      <iframe title="mod frame" />
    </div>
  );
}

// --- TODO 3 ----------------------------------------------------------
// Both copies of the mod 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 game 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(<Hud />);
    const panel = screen.getByTestId("mod-panel") as ModPanel;

    const backdrop = panel.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("hud").ownerDocument);
  });

  it("TODO 2 — the framed backdrop stays inside the frame", () => {
    render(<Hud />);
    const frame = screen.getByTitle("mod 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(<Hud />);
    const panel = screen.getByTestId("mod-panel") as ModPanel;
    const frame = screen.getByTitle("mod frame") as HTMLIFrameElement;

    panel.openOverlay();
    openOverlayInFrame(frame);

    // One backdrop per mod, and a query never reaches into a frame. So
    // the host's share is decided by how many mods run as elements in
    // the HUD - 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("mod-panel").length);
  });
});

Solution