sitez

The runtime in a browser

define in happy-dom: setup runs once per connection with a fresh signal, and a disconnect aborts it and calls the cleanup setup returned. The source the host writes into its script (String(define)) defines elements too.

import { Window } from "happy-dom";
import { afterAll, expect, test } from "vite-plus/test";
import { define, type Setup } from "./runtime.ts";

const window = new Window();
const globals = globalThis as Record<string, unknown>;
const added = ["HTMLElement", "customElements"].filter((key) => !(key in globals));
for (const key of added) globals[key] = (window as unknown as Record<string, unknown>)[key];
afterAll(async () => {
  for (const key of added) delete globals[key];
  await window.happyDOM.close();
});
const { document } = window;
const settled = () => new Promise((done) => setTimeout(done, 0));

test("runs setup per connection, and aborts and cleans up on disconnect", async () => {
  const log: string[] = [];
  const signals: AbortSignal[] = [];
  define("log-box", ((el, { signal }) => {
    signals.push(signal);
    log.push(`setup ${el.id}`);
    return () => log.push("cleanup");
  }) as Setup);
  const el = document.createElement("log-box");
  el.id = "a";
  document.body.append(el);
  await settled();
  el.remove();
  await settled();
  document.body.append(el);
  await settled();
  expect(log).toEqual(["setup a", "cleanup", "setup a"]);
  expect(signals.map((signal) => signal.aborted)).toEqual([true, false]);
});

test("waits for an async setup's cleanup", async () => {
  let cleaned = 0;
  define("lazy-box", (async () => {
    await settled();
    return () => cleaned++;
  }) as Setup);
  const el = document.createElement("lazy-box");
  document.body.append(el);
  el.remove();
  await settled();
  await settled();
  expect(cleaned).toBe(1);
});

test("defines an element from its source, as the site's script holds it", async () => {
  const source = `export default ${String(define)}`;
  const fromSource = (await import(`data:text/javascript,${encodeURIComponent(source)}`))
    .default as typeof define;
  let ran = false;
  fromSource("text-box", () => {
    ran = true;
  });
  document.body.append(document.createElement("text-box"));
  await settled();
  expect(ran).toBe(true);
});