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.tsxSource
// 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);
});
});