src/lib/form/adapters/antd/antd-utils.ts

Total Symbols
8
Lines of Code
162
Avg Complexity
2.9
Avg Coverage
100.0%

Symbols by Kind

function 8

All Symbols

Name Kind Visibility Status Lines Signature
findAntLabel function exported- 16-29 findAntLabel(wrapper: HTMLElement): : string | undefined
findAntId function exported- 35-40 findAntId(wrapper: HTMLElement): : string | undefined
getAntdSelector function exported- 55-64 getAntdSelector(wrapper: HTMLElement): : string
findAntName function exported- 69-74 findAntName(wrapper: HTMLElement): : string | undefined
isAntRequired function exported- 79-85 isAntRequired(wrapper: HTMLElement): : boolean
setReactInputValue function exported- 92-116 setReactInputValue( input: HTMLInputElement | HTMLTextAreaElement, value: string, ): : void
simulateClick function exported- 121-125 simulateClick(el: HTMLElement): : void
waitForElement function exported- 131-159 waitForElement( selector: string, timeoutMs = 500, ): : Promise<HTMLElement | null>

Full Source

/**
 * Ant Design Shared Utilities
 *
 * Helpers shared across all Ant Design component adapters:
 *   - Label extraction from `.ant-form-item-label`
 *   - Event dispatching that React picks up
 *   - Placeholder extraction
 */

import { getUniqueSelector, findLabelWithStrategy } from "../../extractors";

/**
 * Extracts the label text from the nearest `.ant-form-item` wrapper.
 * Falls back to the standard label extractor.
 */
export function findAntLabel(wrapper: HTMLElement): string | undefined {
  const formItem = wrapper.closest(".ant-form-item");
  if (formItem) {
    const labelEl = formItem.querySelector<HTMLElement>(
      ".ant-form-item-label label",
    );
    if (labelEl?.textContent?.trim()) {
      return labelEl.textContent.trim();
    }
  }
  // Fallback to standard label extraction
  const result = findLabelWithStrategy(wrapper);
  return result?.text;
}

/**
 * Extracts the id from Ant Design's form item.
 * Antd assigns id to the inner control based on `name` prop.
 */
export function findAntId(wrapper: HTMLElement): string | undefined {
  const input = wrapper.querySelector<HTMLElement>(
    "input, textarea, [role='combobox'], [role='listbox']",
  );
  return input?.id || wrapper.id || undefined;
}

/**
 * Returns a stable CSS selector for an Ant Design wrapper element.
 *
 * Prefers anchoring on a stable inner input id (e.g. "rc_select_0") over
 * a positional CSS path. In React/antd apps, re-renders can change sibling
 * order and invalidate nth-of-type selectors, causing ignored-field checks
 * to silently fail. Using the inner input's id avoids this problem.
 *
 * Priority:
 *   1. wrapper.id         → `#wrapperId`
 *   2. inner input/control id → `#innerId`
 *   3. CSS path          → getUniqueSelector(wrapper) (fallback)
 */
export function getAntdSelector(wrapper: HTMLElement): string {
  if (wrapper.id) return `#${CSS.escape(wrapper.id)}`;

  const inner = wrapper.querySelector<HTMLElement>(
    "input[id], textarea[id], [role='combobox'][id], [role='listbox'][id]",
  );
  if (inner?.id) return `#${CSS.escape(inner.id)}`;

  return getUniqueSelector(wrapper);
}

/**
 * Extracts the name from Ant Design's inner control.
 */
export function findAntName(wrapper: HTMLElement): string | undefined {
  const input = wrapper.querySelector<HTMLInputElement | HTMLTextAreaElement>(
    "input, textarea",
  );
  return input?.name || undefined;
}

/**
 * Checks if the component is required by looking for the asterisk in the form item.
 */
export function isAntRequired(wrapper: HTMLElement): boolean {
  const formItem = wrapper.closest(".ant-form-item");
  if (formItem) {
    return formItem.querySelector(".ant-form-item-required") !== null;
  }
  return false;
}

/**
 * Dispatches React-compatible input events.
 * Antd uses React synthetic events — we need to set the value via
 * the native setter and dispatch properly.
 */
export function setReactInputValue(
  input: HTMLInputElement | HTMLTextAreaElement,
  value: string,
): void {
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
    window.HTMLInputElement.prototype,
    "value",
  )?.set;
  const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
    window.HTMLTextAreaElement.prototype,
    "value",
  )?.set;

  if (input instanceof HTMLInputElement && nativeInputValueSetter) {
    nativeInputValueSetter.call(input, value);
  } else if (
    input instanceof HTMLTextAreaElement &&
    nativeTextAreaValueSetter
  ) {
    nativeTextAreaValueSetter.call(input, value);
  }

  input.dispatchEvent(new Event("input", { bubbles: true }));
  input.dispatchEvent(new Event("change", { bubbles: true }));
}

/**
 * Simulates a mouse click on an element (used to open dropdowns).
 */
export function simulateClick(el: HTMLElement): void {
  el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
  el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
  el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}

/**
 * Waits for an element matching the selector to appear in the DOM.
 * Returns `null` if the element doesn't appear within the timeout.
 */
export function waitForElement(
  selector: string,
  timeoutMs = 500,
): Promise<HTMLElement | null> {
  const existing = document.querySelector<HTMLElement>(selector);
  if (existing) return Promise.resolve(existing);

  return new Promise((resolve) => {
    const observer = new MutationObserver(() => {
      const el = document.querySelector<HTMLElement>(selector);
      if (el) {
        observer.disconnect();
        resolve(el);
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["class", "style"],
    });

    setTimeout(() => {
      observer.disconnect();
      resolve(null);
    }, timeoutMs);
  });
}

export { getUniqueSelector };