src/lib/form/adapters/__tests__/adapter-registry.test.ts

Total Symbols
2
Lines of Code
228
Avg Complexity
3.0
Symbol Types
1

File Relationships

graph LR makeAdapter["makeAdapter"] makeAdapter -->|calls| makeAdapter click makeAdapter "../symbols/7cbe03be6bdb4b2d.html"

Symbols by Kind

function 2

All Symbols

Name Kind Visibility Status Lines Signature
makeAdapter function - 14-49 makeAdapter( name: AdapterName, selector: string, opts: { matches?: boolean; fillResult?: boolean; fillThrows?: boolean; field?: Partial<FormField>; } = {}, ): : CustomComponentAdapter
makeField function - 165-179 makeField(adapterName?: AdapterName): : FormField

Full Source

// @vitest-environment happy-dom
import { describe, expect, it, vi, beforeEach } from "vitest";
import {
  registerAdapter,
  getAdapter,
  detectCustomComponents,
  fillCustomComponent,
} from "../adapter-registry";
import type { CustomComponentAdapter, AdapterName } from "../adapter.interface";
import type { FormField } from "@/types";

// ── Stub adapter factory ──────────────────────────────────────────────────────

function makeAdapter(
  name: AdapterName,
  selector: string,
  opts: {
    matches?: boolean;
    fillResult?: boolean;
    fillThrows?: boolean;
    field?: Partial<FormField>;
  } = {},
): CustomComponentAdapter {
  const el = document.createElement("div");
  const field: FormField = {
    element: el,
    selector: `[data-${name}]`,
    category: "unknown",
    fieldType: "unknown",
    label: name,
    name: "",
    id: "",
    placeholder: "",
    required: false,
    contextSignals: "",
    adapterName: name,
    ...(opts.field ?? {}),
  };

  return {
    name,
    selector,
    matches: vi.fn().mockReturnValue(opts.matches ?? true),
    buildField: vi.fn().mockReturnValue(field),
    fill: opts.fillThrows
      ? vi.fn().mockRejectedValue(new Error("fill error"))
      : vi.fn().mockResolvedValue(opts.fillResult ?? true),
  };
}

// ── getAdapter ────────────────────────────────────────────────────────────────

describe("getAdapter", () => {
  it("returns undefined for unknown adapter name", () => {
    expect(getAdapter("nonexistent-adapter" as AdapterName)).toBeUndefined();
  });

  it("returns adapter after registerAdapter", () => {
    const adapter = makeAdapter("select2" as AdapterName, ".select2-container");
    registerAdapter(adapter);
    expect(getAdapter("select2" as AdapterName)).toBe(adapter);
  });
});

// ── registerAdapter ───────────────────────────────────────────────────────────

describe("registerAdapter", () => {
  it("makes the adapter retrievable by name", () => {
    const adapter = makeAdapter(
      "ant-input" as AdapterName,
      ".ant-input-affix-wrapper",
    );
    registerAdapter(adapter);
    expect(getAdapter("ant-input" as AdapterName)).toBeDefined();
  });

  it("overwrites previous adapter with same name on next lookup", () => {
    const a1 = makeAdapter("ant-select" as AdapterName, ".ant-select-v1");
    const a2 = makeAdapter("ant-select" as AdapterName, ".ant-select-v2");
    registerAdapter(a1);
    registerAdapter(a2);
    // Map rebuild: last registered wins
    const result = getAdapter("ant-select" as AdapterName);
    expect(result?.selector).toBe(".ant-select-v2");
  });
});

// ── detectCustomComponents ────────────────────────────────────────────────────

describe("detectCustomComponents", () => {
  beforeEach(() => {
    document.body.innerHTML = "";
  });

  it("returns empty array when no matching elements in the DOM", () => {
    const fields = detectCustomComponents();
    expect(Array.isArray(fields)).toBe(true);
  });

  it("detects elements matching adapter selector when matches() is true", () => {
    const adapter = makeAdapter("detect-ok" as AdapterName, ".detect-ok");
    registerAdapter(adapter);

    document.body.innerHTML = `<div class="detect-ok"></div>`;
    const fields = detectCustomComponents();

    expect(adapter.matches).toHaveBeenCalled();
    expect(adapter.buildField).toHaveBeenCalled();
    expect(fields.some((f) => f.adapterName === "detect-ok")).toBe(true);
  });

  it("skips elements where matches() returns false", () => {
    const adapter = makeAdapter("detect-skip" as AdapterName, ".detect-skip", {
      matches: false,
    });
    registerAdapter(adapter);

    document.body.innerHTML = `<div class="detect-skip"></div>`;
    const fields = detectCustomComponents();

    expect(adapter.matches).toHaveBeenCalled();
    expect(adapter.buildField).not.toHaveBeenCalled();
    expect(fields.some((f) => f.adapterName === "detect-skip")).toBe(false);
  });

  it("does not claim same element twice (WeakSet deduplication)", () => {
    const adapter1 = makeAdapter("dup-1" as AdapterName, ".dup-target");
    const adapter2 = makeAdapter("dup-2" as AdapterName, ".dup-target");
    registerAdapter(adapter1);
    registerAdapter(adapter2);

    document.body.innerHTML = `<div class="dup-target"></div>`;
    const fields = detectCustomComponents();

    // Only the first adapter should claim the element
    const dupFields = fields.filter(
      (f) => f.adapterName === "dup-1" || f.adapterName === "dup-2",
    );
    expect(dupFields.length).toBe(1);
    expect(dupFields[0].adapterName).toBe("dup-1");
  });

  it("handles buildField errors gracefully", () => {
    const adapter = makeAdapter(
      "build-err" as AdapterName,
      ".build-err-target",
    );
    adapter.buildField = vi.fn().mockImplementation(() => {
      throw new Error("buildField exploded");
    });
    registerAdapter(adapter);

    document.body.innerHTML = `<div class="build-err-target"></div>`;
    const fields = detectCustomComponents();

    // Should not throw, error is caught internally
    expect(adapter.buildField).toHaveBeenCalled();
    expect(fields.some((f) => f.adapterName === "build-err")).toBe(false);
  });
});

// ── fillCustomComponent ───────────────────────────────────────────────────────

describe("fillCustomComponent", () => {
  function makeField(adapterName?: AdapterName): FormField {
    return {
      element: document.createElement("div"),
      selector: "#x",
      category: "unknown",
      fieldType: "unknown",
      label: "x",
      name: "",
      id: "",
      placeholder: "",
      required: false,
      contextSignals: "",
      adapterName,
    };
  }

  it("returns false when field has no adapterName", async () => {
    const result = await fillCustomComponent(makeField(), "value");
    expect(result).toBe(false);
  });

  it("returns false when adapterName not found in registry", async () => {
    const result = await fillCustomComponent(
      makeField("ghost-adapter" as AdapterName),
      "value",
    );
    expect(result).toBe(false);
  });

  it("calls adapter.fill and returns true on success", async () => {
    const adapter = makeAdapter("fill-ok" as AdapterName, ".fill-ok", {
      fillResult: true,
    });
    registerAdapter(adapter);

    const field = makeField("fill-ok" as AdapterName);
    const result = await fillCustomComponent(field, "hello");
    expect(result).toBe(true);
    expect(adapter.fill).toHaveBeenCalledWith(field.element, "hello");
  });

  it("returns false when adapter.fill returns false", async () => {
    const adapter = makeAdapter("fill-false" as AdapterName, ".fill-false", {
      fillResult: false,
    });
    registerAdapter(adapter);

    const field = makeField("fill-false" as AdapterName);
    const result = await fillCustomComponent(field, "v");
    expect(result).toBe(false);
  });

  it("returns false (does not throw) when adapter.fill throws", async () => {
    const adapter = makeAdapter("fill-throw" as AdapterName, ".fill-throw", {
      fillThrows: true,
    });
    registerAdapter(adapter);

    const field = makeField("fill-throw" as AdapterName);
    const result = await fillCustomComponent(field, "v");
    expect(result).toBe(false);
  });
});