{"protocol":"clank-doc/1","frameworkVersion":"0.24.0","slug":"component-harness","title":"Interactive component specimens","description":"The optional @clank.run/framework/component harness module runs the same typed fixture through server rendering, hydration and a local interactive browser. Use a dedicated development page or iframe with disposable data. Each document hosts","group":{"id":"framework","title":"Framework"},"url":"https://docs.clank.run/docs/component-harness","source":"docs/component-harness.md","headings":["Define and render a fixture","Hydrate and interact","Replay native keyboard and focus behavior","Limits and compatibility"],"tableOfContents":[{"id":"define-and-render-a-fixture","title":"Define and render a fixture","level":2},{"id":"replay-native-keyboard-and-focus-behavior","title":"Replay native keyboard and focus behavior","level":2},{"id":"limits-and-compatibility","title":"Limits and compatibility","level":2}],"markdown":"# Interactive component specimens\n\nThe optional `@clank.run/framework/component-harness` module runs the same typed fixture through\nserver rendering, hydration and a local interactive browser. Use a dedicated development page\nor iframe with disposable data. Each document hosts one active harness; its root must be a\nconnected element below the document body. Importing the module mounts nothing and starts no\nserver. It adds no framework dependencies.\n\nA specimen captures schema-validated JSON props, a synchronous instance factory, a UI manifest,\nstable semantic part IDs, current-state assertions and full browser journeys. Every render,\nreset and selection gets a fresh instance. The renderer owns reactive effects, lifecycle hooks\nand portals created inside the factory. Register external resources with `onCleanup` or return\nan idempotent `dispose` callback; the harness invokes that callback once per created instance.\n\n## Define and render a fixture\n\n```ts clank-run=component-specimen-ssr\nimport assert from \"node:assert/strict\";\nimport { s } from \"@clank.run/framework/ai\";\nimport { h } from \"@clank.run/framework/dom\";\nimport { createSwitch } from \"@clank.run/framework/ui/controls\";\nimport { defineComponentSpecimen, renderComponentSpecimen, exportComponentAssertions } from \"@clank.run/framework/component-harness\";\n\nconst notifications = defineComponentSpecimen({\n  name: \"notifications\", revision: \"switch/1\", label: \"Notifications switch\",\n  props: s.object({ enabled: s.boolean() }), value: { enabled: false },\n  parts: { root: \"notifications\" },\n  assertions: [{ target: \"notifications\", state: { role: \"switch\", checked: false } }],\n  journeys: [390, 1280].map(width => ({\n    name: `Switch keyboard ${width}`, start: \"/specimens/notifications\",\n    viewport: { width, height: 844 },\n    steps: [\n      { wait: { target: \"notifications\", state: { checked: false } } },\n      { focus: \"notifications\" }, { press: \"Enter\" },\n      { expect: { target: \"notifications\", state: { checked: true }, focused: \"notifications\", noHorizontalOverflow: true } },\n      { press: \"Space\" }, { expect: { target: \"notifications\", state: { checked: false } } }\n    ]\n  })),\n  create(props) {\n    const control = createSwitch({ id: \"notifications\", defaultChecked: props.enabled });\n    return {\n      view: h(\"button\", control.root({ nativeButton: true, agentLabel: \"Notifications\" }), \"Notifications\"),\n      manifest: () => control.manifest(), dispose() {}\n    };\n  }\n});\nconst rendered = await renderComponentSpecimen(notifications);\nassert.match(rendered.html, /aria-checked=\"false\"/);\nassert.equal(rendered.snapshot.protocol, \"clank-component-snapshot/1\");\nassert.equal(exportComponentAssertions(notifications), exportComponentAssertions(notifications));\n```\n\nShare the exported specimen definition between your SSR route and browser entry. Embed\n`rendered.html` in the dedicated root and serialize `rendered.snapshot` with `serializeState`\nfrom the SSR module; do not interpolate JSON into a script without escaping it. Serve browser\nmodules through your ordinary development build. The factory is trusted developer code, not a\nJavaScript sandbox. Keep the fixture route local or behind your development authentication and\nnever point destructive journeys at production data.\n\n## Hydrate and interact\n\n```ts\nimport { readState } from \"@clank.run/framework/ssr\";\nimport { hydrateComponentSpecimen, mountComponentHarnessControls } from \"@clank.run/framework/component-harness\";\nimport { notifications } from \"./specimens.js\";\n\nconst root = document.getElementById(\"specimen\")!;\nconst harness = await hydrateComponentSpecimen(root, notifications, readState().snapshot);\nconst cleanup = mountComponentHarnessControls(document.getElementById(\"tools\")!, harness, [notifications]);\naddEventListener(\"pagehide\", cleanup, { once: true });\n```\n\nThe controls provide selection, reset, current assertions, assertion export and disposal.\nCurrent assertions read the mounted semantic surface and declared part roles. Required manifest\nparts must have unique mappings; a conditional popup may be absent until a journey opens it.\nDeclare assertions for those mounted states in the journey. A current assertion expecting the\ninitial unchecked state will correctly fail after the person checks the switch; reset restores\nits captured starting state.\n\n`mountComponentSpecimen(root, specimen)` starts without SSR. `harness.snapshot()` returns its\nphase, generation, instance creation/disposal counts, normalized common UI contract and bounded\nstructural hydration diagnostics. Counts record disposal callback invocations; external resource\ncleanup remains the fixture's responsibility. Snapshots exclude DOM nodes and rendered values.\nText corrections preserve SSR nodes. Structural mismatches use the renderer's normal remount\nfallback and release the abandoned instance. The `hydrated` phase identifies the hydration\nentry path; inspect diagnostics to distinguish a remount.\n\nThe SSR fingerprint covers name, revision, captured props, mappings, assertions and journeys.\nChange the revision when changing factory behavior. Fingerprint or instance-contract mismatches\nfail before accepting attachment. An optional `AbortSignal` cancels pending fingerprint validation\nbefore the factory runs. Moving or detaching the root during validation fails before factory execution and releases its\noriginal document reservation. Cancelled work cannot clear a newer mount. Reset, selection and disposal\ninvalidate in-flight checks; a stale check returns `null`. The controls fence late status updates.\nUse the controls' returned cleanup when the host closes or removes the panel; it removes the\npanel and disposes the harness. Direct harness disposal does not remove a separately mounted\ncontrols panel.\n\n## Replay native keyboard and focus behavior\n\nSave `exportComponentAssertions(specimen)` or the controls' read-only JSON to a local file, then\nrun it against the disposable fixture route:\n\n```sh\nclank journey journeys/notifications.json --url=http://127.0.0.1:4100 --output=.clank/components.json --json\n```\n\nThe stable `clank-component-assertions/1` envelope contains CLI-compatible `journeys`, semantic\npart mappings and current assertions. It excludes props and timing results. The CLI replays the\njourneys; current assertions run through the harness's Check button. Use different journey names\nfor desktop and narrow viewports. Native `focus`, `press`, `focused` and `noHorizontalOverflow`\nchecks expose real browser focus, Tab order, modal wrapping and responsive overflow. Chrome\nreceives key input through DevTools and applies its native default behavior. No synthetic DOM\nkeyboard event is used to claim native keyboard acceptance. The DOM journey adapter reports\nfocus and layout but deliberately has no native `press` capability. See [browser journeys](browser-journeys.md).\n\n## Limits and compatibility\n\nDefinitions accept finite plain JSON only: 64 KiB props, 16 KiB mappings, depth 16 and 10,000\nvisited values. There are at most 64 parts, 64 current assertions, 100 combined current steps,\n10 unique journeys and 50 specimen choices. The general serialized definition/export/snapshot\nlimit and rendered SSR output limit are 256 KiB. HTML is checked after trusted rendering; these\nlimits do not sandbox arbitrary factory execution. Factories and manifests must return\nsynchronously; their rejected promises are contained and rejected as invalid contracts. A view\nmay use the renderer's supported async SSR values.\n\nThe harness retains at most 1,000 hydration entries and marks truncated capture. It reserves one\nactive document to isolate portal/focus interactions, but does not replace browser-origin or\napplication authorization boundaries. No database, persistent schema, network protocol or\nproduction route changes are required. Existing journeys and drivers remain compatible because\nnative focus/key/layout capabilities are optional. Roll back by removing the optional fixture\nroute and imports; existing framework applications do not load the harness automatically.\n"}