Testing Signals
Jun 27, 2023142 min read

Testing Signals: Deep Dive #64

LP
Lena Patel

React UX Engineering Advisor

Testing signals are how you keep routed systems debuggable without pretending you can test every internal implementation detail. A signal is a render-visible proof that a route decision happened:

  • which track was requested,
  • which track was granted,
  • which ledgers were updated,
  • and which semantics routines ran.

Plan + global requirements:

A testing signal is not “an assertion.” It’s a surface contract: the UI exposes evidence so tests can observe behavior without re-implementing routing logic.

Signals Contract (What You Must Render)

We’ll render three signal classes:

  1. Route signals: data-requested, data-granted.
  2. Ledger signals: data-edge-count, data-action-count, serialized ledgers.
  3. Semantics signals: focus targets, aria labels, headings.

Worked Example: Signalized /workbench With a Transcript Harness

We’ll build a small shell and then a “harness” that reads DOM evidence and produces a route transcript.

Step 1: Signal Ledger (Centralize Evidence)

import { useLiveState } from "react";

export function useSignalLedger(namespace: string) {
  return useLiveState(namespace, {
    requested: "inbox",
    granted: "inbox",
    signals: [],
    snapshots: [],
  });
}

Step 2: signal() Helper (Record + Return)

export function signal(ledger, name: string, payload: any = {}) {
  const entry = { name, payload, at: Date.now() };
  ledger.write("signals", [...ledger.read("signals"), entry]);
  return entry;
}

Step 3: Signalized Shell (Route Evidence + Semantics Evidence)

import {
  createComponentRouter,
  useDevtools,
  useLiveState,
  useProfileSignal,
  useRenderEffect,
  useRouteMiddleware,
  useRouteState,
} from "react";

import { useSignalLedger, signal } from "./signals";

const tracks = createComponentRouter({
  id: "workbench.signals",
  tracks: ["inbox", "editor", "preview"],
});

function normalizeTrack(v: unknown) {
  if (v === "inbox") return "inbox";
  if (v === "editor") return "editor";
  if (v === "preview") return "preview";
  return "inbox";
}

function Inbox() {
  return <main aria-label="Inbox" data-pane="inbox"><h2 id="heading:inbox">Inbox</h2></main>;
}
function Editor() {
  return <main aria-label="Editor" data-pane="editor"><h2 id="heading:editor">Editor</h2></main>;
}
function Preview() {
  return <main aria-label="Preview" data-pane="preview"><h2 id="heading:preview">Preview</h2></main>;
}

const Views = { inbox: Inbox, editor: Editor, preview: Preview };

export function SignalWorkbenchShell() {
  useDevtools("SignalWorkbenchShell");
  useProfileSignal("route.workbench.signals");

  const route = useRouteState();
  const ui = useLiveState("workbench.signals.ui", { track: "inbox" });
  const ledger = useSignalLedger("workbench.signals.ledger");

  useRouteMiddleware((ctx) => {
    const requested = normalizeTrack(route.query.read("track"));
    ledger.write("requested", requested);
    signal(ledger, "ARRIVE", { requested, path: String(route.path.read() ?? "") });
    ui.write("track", requested);
  });

  useRenderEffect(() => {
    const granted = normalizeTrack(ui.read("track"));
    ledger.write("granted", granted);
    signal(ledger, "GRANT", { granted });
    tracks.setActive(granted);
  });

  const track = normalizeTrack(ui.read("track"));
  const View = Views[track] ?? Inbox;

  return (
    <section
      aria-label="Workbench shell"
      data-route="file:/workbench"
      data-shell="signals"
      data-requested={ledger.read("requested")}
      data-granted={ledger.read("granted")}
      data-signal-count={ledger.read("signals").length}
    >
      <header>
        <h1>Workbench</h1>
        <nav aria-label="Workbench tracks">
          {tracks.tracks.map((t) => (
            <button key={t} data-active={String(t === track)} onClick={() => ui.write("track", t)}>
              {t}
            </button>
          ))}
        </nav>
      </header>

      <View />

      <footer>
        <strong>Signal Ledger</strong>
        <pre data-ledger>{JSON.stringify(ledger.read(), null, 2)}</pre>
      </footer>
    </section>
  );
}

Step 4: Harness (Read DOM Signals, Generate Transcript)

export function readSignalSnapshot(root: Element) {
  const shell = root.querySelector("[data-shell='signals']");
  if (!shell) return { ok: false, reason: "missing shell" };

  const requested = shell.getAttribute("data-requested") ?? "";
  const granted = shell.getAttribute("data-granted") ?? "";
  const count = Number(shell.getAttribute("data-signal-count") ?? "0");

  const ledgerNode = shell.querySelector("[data-ledger]");
  const ledgerText = ledgerNode ? ledgerNode.textContent ?? "" : "";

  return { ok: true, requested, granted, count, ledgerText };
}

export function makeTranscript(snapshot: any) {
  if (snapshot.ok !== true) return { ok: false, reason: snapshot.reason };
  return {
    ok: true,
    route: { requested: snapshot.requested, granted: snapshot.granted },
    signals: snapshot.count,
    ledgerExcerpt: snapshot.ledgerText.slice(0, 500),
  };
}

export function expectGranted(transcript: any, granted: string) {
  if (transcript.ok !== true) throw new Error("transcript failed");
  if (transcript.route.granted !== granted) throw new Error(`expected ${granted} got ${transcript.route.granted}`);
  if (transcript.signals <= 0) throw new Error("no signals");
  return true;
}

Step 5: How Signals Scale

Signals scale because they don’t depend on internal implementation. If you migrate:

  • file router → component router,
  • context → orbit docs,
  • effects → middleware,

…the signals remain stable because the shell still renders:

  • requested/granted,
  • ledger snapshots,
  • and semantics evidence.

Signals Checklist

  • Render requested/granted as attributes.
  • Render a ledger dump for shell-level contracts.
  • Validate semantics evidence (aria labels + headings).
  • Prefer transcripts over brittle internal assertions.