{"version":3,"file":"mount.cjs","names":[],"sources":["../../src/testing/mount.ts"],"sourcesContent":["/**\n * Component mounting utilities for test environments.\n */\n\nimport { type QueryScope, within } from '@vielzeug/assay';\nimport type { Readable } from '@vielzeug/ripple';\n\nimport type { ComponentDefinition } from '../component-types';\nimport { define } from '../define';\nimport type { HTMLResult } from '../template/result';\nimport { setAttr } from '../utils/dom';\nimport { type FlushOptions, flush } from './flush';\nimport { _disposeRenderHooks } from './render-hook';\nimport { resetOreForTests } from './reset';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n// Fixture inherits Assay's complete QueryScope. Keeping that relationship structural means new\n// scoped query capabilities are immediately available to fixtures without a duplicate contract.\n/**\n * A mounted component ready for assertions. All inherited `QueryScope` methods are scoped to\n * `element.shadowRoot`, falling back to `element` for light-DOM components (`shadow: false`).\n */\nexport interface Fixture<T extends HTMLElement = HTMLElement> extends QueryScope {\n  /** Run a callback then flush — the standard way to trigger and assert a reactive update */\n  act(fn: () => unknown): Promise<void>;\n  /** Set an attribute (boolean `false` removes it) then flush */\n  attr(name: string, value: string | number | boolean): Promise<void>;\n  /** Set multiple attributes then flush */\n  attrs(record: Record<string, string | number | boolean>): Promise<void>;\n  /** Remove the component from the DOM. Idempotent. */\n  dispose(): void;\n  /** `true` after `dispose()` has been called. */\n  readonly disposed: boolean;\n  /** The component element */\n  element: T;\n  /** Wait for all reactive updates and animation frames */\n  flush(options?: FlushOptions): Promise<void>;\n  /** The component's shadow root (null for light-DOM components) */\n  readonly shadow: ShadowRoot | null;\n  /** Delegates to `dispose()`. Enables `using` declarations. */\n  [Symbol.dispose](): void;\n}\n\nexport interface MountOptions {\n  /** HTML attributes to set on the element */\n  attrs?: Record<string, string | number | boolean>;\n  /** Extra component options when passing an inline setup function */\n  componentOptions?: Omit<ComponentDefinition<Record<string, unknown>>, 'setup'>;\n  /** Parent container (default: document.body) */\n  container?: HTMLElement | ShadowRoot;\n  /** Inner HTML for slot content */\n  html?: string;\n  /** Properties assigned directly onto the element */\n  props?: Record<string, unknown>;\n}\n\ntype MountProps = { readonly [x: string]: Readable<unknown> };\n\n// Bivariant callback type keeps inline test callbacks ergonomic across varying prop specializations.\nexport type MountSetup = {\n  bivarianceHack: (props: MountProps) => HTMLResult | null;\n}['bivarianceHack'];\n\n// ─── Test environment state ───────────────────────────────────────────────────\n\nexport const _mountedElements: HTMLElement[] = [];\n\n// Monotonic across the whole test run — never reset: custom element registrations\n// are permanent, so a re-used tag name would throw on re-define. Deterministic\n// within a run (trial-1, trial-2, ...) without a random suffix.\nlet _componentTagCounter = 0;\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction applyAttr(element: Element, name: string, value: string | number | boolean): void {\n  // Same write path as the runtime's attribute bindings, so tests never set up\n  // states the runtime itself can't produce (e.g. boolean true → \"true\").\n  setAttr(element, name, value);\n}\n\nconst toError = (value: unknown): Error => {\n  return value instanceof Error ? value : new Error(String(value));\n};\n\nconst withWindowErrorCapture = async <T>(action: () => Promise<T>): Promise<T> => {\n  if (typeof window === 'undefined') return action();\n\n  let captured: Error | null = null;\n  const onError = (event: ErrorEvent) => {\n    captured = toError(event.error ?? event.message);\n    event.preventDefault();\n  };\n  const onUnhandledRejection = (event: PromiseRejectionEvent) => {\n    captured = toError(event.reason);\n    event.preventDefault();\n  };\n\n  window.addEventListener('error', onError);\n  window.addEventListener('unhandledrejection', onUnhandledRejection);\n\n  try {\n    const result = await action();\n\n    if (captured) throw captured;\n\n    return result;\n  } finally {\n    window.removeEventListener('error', onError);\n    window.removeEventListener('unhandledrejection', onUnhandledRejection);\n  }\n};\n\n// ─── API ─────────────────────────────────────────────────────────────────────\n\n/**\n * Mount a component into the DOM and return a test fixture.\n *\n * Accepts a registered tag name, an inline setup function, or a component\n * options object. Setup functions are auto-registered with generated tag names.\n *\n * @example — inline setup function\n * const { query } = await mount(() => {\n *   const count = signal(0);\n *   return html`<button @click=${() => count.value++}>${count}</button>`;\n * });\n *\n * @example — registered tag name\n * const { query } = await mount('my-counter');\n */\nexport async function mount<T extends HTMLElement = HTMLElement>(\n  tagOrSetup: string,\n  options?: MountOptions,\n): Promise<Fixture<T>>;\nexport async function mount<T extends HTMLElement = HTMLElement>(\n  tagOrSetup: MountSetup,\n  options?: MountOptions,\n): Promise<Fixture<T>>;\nexport async function mount<T extends HTMLElement = HTMLElement>(\n  tagOrSetup: string | MountSetup,\n  options: MountOptions = {},\n): Promise<Fixture<T>> {\n  const { attrs = {}, componentOptions, container = document.body, html, props = {} } = options;\n\n  let tagName: string;\n  let inlineDefinition: ComponentDefinition<Record<string, unknown>> | undefined;\n\n  if (typeof tagOrSetup === 'string') {\n    tagName = tagOrSetup;\n  } else {\n    tagName = `trial-${++_componentTagCounter}`;\n    inlineDefinition = {\n      ...(componentOptions ?? {}),\n      setup: tagOrSetup,\n    };\n  }\n\n  if (inlineDefinition) {\n    define(tagName, inlineDefinition);\n  }\n\n  const element = document.createElement(tagName) as T;\n\n  if (html) element.innerHTML = html;\n\n  if (Object.keys(props).length) Object.assign(element, props);\n\n  for (const [name, value] of Object.entries(attrs)) applyAttr(element, name, value);\n\n  try {\n    await withWindowErrorCapture(async () => {\n      container.appendChild(element);\n      _mountedElements.push(element);\n      await flush();\n    });\n  } catch (err) {\n    element.remove();\n\n    const i = _mountedElements.indexOf(element);\n\n    if (i !== -1) _mountedElements.splice(i, 1);\n\n    throw err;\n  }\n\n  let isDisposed = false;\n\n  function dispose() {\n    if (isDisposed) return;\n\n    isDisposed = true;\n    element.remove();\n\n    const i = _mountedElements.indexOf(element);\n\n    if (i !== -1) _mountedElements.splice(i, 1);\n  }\n\n  const scope = within((element.shadowRoot ?? element) as Element);\n\n  return {\n    ...scope,\n\n    async act(fn) {\n      await fn();\n      await flush();\n    },\n\n    async attr(name, value) {\n      applyAttr(element, name, value);\n      await flush();\n    },\n\n    async attrs(record) {\n      for (const [name, value] of Object.entries(record)) applyAttr(element, name, value);\n      await flush();\n    },\n\n    dispose,\n\n    get disposed(): boolean {\n      return isDisposed;\n    },\n\n    element,\n\n    flush,\n\n    get shadow(): ShadowRoot | null {\n      return element.shadowRoot;\n    },\n\n    [Symbol.dispose]() {\n      dispose();\n    },\n  };\n}\n\n/**\n * Register and mount a component definition in a single call.\n *\n * Combines `define(tag, definition)` + `mount(tag, options)` — the standard\n * pattern for testing full custom-element lifecycle (props, reconnect, etc.).\n *\n * @example\n * const { query } = await mountComponent('my-counter', {\n *   props: { count: prop.number(0) },\n *   setup: (props) => html`<div>${props.count}</div>`,\n * });\n */\nexport async function mountComponent<Props extends Record<string, unknown>, T extends HTMLElement = HTMLElement>(\n  tag: string,\n  definition: ComponentDefinition<Props>,\n  options?: MountOptions,\n): Promise<Fixture<T>> {\n  define(tag, definition);\n\n  return mount<T>(tag, options);\n}\n\n/**\n * Register a stub custom element (no-op if already defined).\n *\n * @example\n * mock('child-button', '<slot></slot>');\n */\nexport function mock(tagName: string, template = ''): void {\n  if (!customElements.get(tagName)) {\n    customElements.define(\n      tagName,\n      class extends HTMLElement {\n        connectedCallback() {\n          this.innerHTML = template;\n        }\n      },\n    );\n  }\n}\n\n/**\n * Remove all elements mounted via `mount()`.\n * Call in `afterEach` to keep tests isolated.\n *\n * @example\n * afterEach(() => cleanup());\n */\nexport function cleanup(): void {\n  for (const el of _mountedElements) el.remove();\n  _mountedElements.length = 0;\n  _disposeRenderHooks();\n  resetOreForTests();\n}\n\n/** @internal re-export for within() */\nexport type { QueryScope };\n"],"mappings":"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"}