src/lib/form/adapters/react-select/__tests__/react-select-adapter.test.ts

Total Symbols
2
Lines of Code
516
Avg Complexity
8.0
Symbol Types
1

File Relationships

graph LR makeWrapper["makeWrapper"] addPortaledMenu["addPortaledMenu"] makeWrapper -->|calls| makeWrapper makeWrapper -->|calls| addPortaledMenu click makeWrapper "../symbols/ea80e78e3a5fac0a.html" click addPortaledMenu "../symbols/4752dca3c051f36f.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'makeWrapper' has cyclomatic complexity 14 (max 10)
  • [warning] max-lines: 'makeWrapper' has 98 lines (max 80)

Symbols by Kind

function 2

All Symbols

Name Kind Visibility Status Lines Signature
makeWrapper function - 29-126 makeWrapper(options?: { disabled?: boolean; searchable?: boolean; multi?: boolean; hiddenInputName?: string; hiddenInputWrapped?: boolean; placeholder?: string; withInlineMenu?: string[]; withAriaControls?: string; visibleInputId?: string; noControl?: boolean; noInput?: boolean; }): : HTMLElement
addPortaledMenu function - 129-152 addPortaledMenu(listboxId: string, options: string[]): : HTMLElement

Full Source

/** @vitest-environment happy-dom */

/**
 * Testes unitários para reactSelectAdapter.
 * Cobre matches(), buildField(), fill() e waitForReactSelectMenu().
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

vi.mock("@/lib/form/extractors", () => ({
  getUniqueSelector: vi.fn((el: HTMLElement) => el.id || el.className || "sel"),
  buildSignals: vi.fn(() => "mock signals"),
  findLabelWithStrategy: vi.fn(() => null),
}));

vi.mock("@/lib/logger", () => ({
  createLogger: vi.fn(() => ({
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  })),
}));

import { reactSelectAdapter } from "../react-select-adapter";

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeWrapper(options?: {
  disabled?: boolean;
  searchable?: boolean;
  multi?: boolean;
  hiddenInputName?: string;
  hiddenInputWrapped?: boolean;
  placeholder?: string;
  withInlineMenu?: string[];
  withAriaControls?: string;
  visibleInputId?: string;
  noControl?: boolean;
  noInput?: boolean;
}): HTMLElement {
  const wrapper = document.createElement("div");
  wrapper.className = "react-select-container";
  if (options?.disabled) {
    wrapper.classList.add("react-select--is-disabled");
  }

  if (!options?.noControl) {
    const control = document.createElement("div");
    control.className = "react-select__control";

    const valueContainer = document.createElement("div");
    valueContainer.className = options?.multi
      ? "react-select__value-container react-select__value-container--is-multi"
      : "react-select__value-container";

    if (options?.placeholder) {
      const ph = document.createElement("div");
      ph.className = "react-select__placeholder";
      ph.textContent = options.placeholder;
      valueContainer.appendChild(ph);
    }

    if (!options?.noInput) {
      const inputContainer = document.createElement("div");
      inputContainer.className = "react-select__input-container";

      const input = document.createElement("input");
      input.setAttribute("role", "combobox");
      input.type = "text";

      if (options?.searchable !== false) {
        // searchable = default true
        input.className = "react-select__input";
      }

      if (options?.visibleInputId) {
        input.id = options.visibleInputId;
      }
      if (options?.withAriaControls) {
        input.setAttribute("aria-controls", options.withAriaControls);
      }

      inputContainer.appendChild(input);
      valueContainer.appendChild(inputContainer);
    }

    control.appendChild(valueContainer);
    wrapper.appendChild(control);
  }

  // Hidden input (carries name/value for submission)
  if (options?.hiddenInputName) {
    const hidden = document.createElement("input");
    hidden.type = "hidden";
    hidden.name = options.hiddenInputName;
    if (options?.hiddenInputWrapped) {
      const div = document.createElement("div");
      div.appendChild(hidden);
      wrapper.appendChild(div);
    } else {
      wrapper.appendChild(hidden);
    }
  }

  // Inline menu already rendered
  if (options?.withInlineMenu) {
    const menu = document.createElement("div");
    menu.className = "react-select__menu";
    const menuList = document.createElement("div");
    menuList.className = "react-select__menu-list";

    for (const optText of options.withInlineMenu) {
      const opt = document.createElement("div");
      opt.className = "react-select__option";
      opt.textContent = optText;
      opt.dataset["value"] = optText.toLowerCase();
      menuList.appendChild(opt);
    }

    menu.appendChild(menuList);
    wrapper.appendChild(menu);
  }

  return wrapper;
}

/** Cria um menu portalizado (appended to body) com aria-controls linkando */
function addPortaledMenu(listboxId: string, options: string[]): HTMLElement {
  const menu = document.createElement("div");
  menu.className = "react-select__menu";

  const menuList = document.createElement("div");
  menuList.className = "react-select__menu-list";

  const listbox = document.createElement("div");
  listbox.id = listboxId;
  listbox.setAttribute("role", "listbox");

  for (const optText of options) {
    const opt = document.createElement("div");
    opt.className = "react-select__option";
    opt.textContent = optText;
    opt.dataset["value"] = optText.toLowerCase();
    listbox.appendChild(opt);
  }

  menuList.appendChild(listbox);
  menu.appendChild(menuList);
  document.body.appendChild(menu);
  return menu;
}

// ─── matches() ───────────────────────────────────────────────────────────────

describe("reactSelectAdapter.matches()", () => {
  it("retorna true para .react-select-container sem disabled", () => {
    const el = makeWrapper();
    expect(reactSelectAdapter.matches(el)).toBe(true);
  });

  it("retorna false para .react-select--is-disabled", () => {
    const el = makeWrapper({ disabled: true });
    expect(reactSelectAdapter.matches(el)).toBe(false);
  });

  it("retorna false para elemento sem react-select-container", () => {
    const el = document.createElement("div");
    el.className = "other-class";
    expect(reactSelectAdapter.matches(el)).toBe(false);
  });
});

// ─── buildField() ────────────────────────────────────────────────────────────

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

  it("fieldType = select para single-select", () => {
    const wrapper = makeWrapper({ hiddenInputName: "estado" });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.fieldType).toBe("select");
    expect(field.adapterName).toBe("react-select");
    expect(field.name).toBe("estado");
  });

  it("fieldType = multiselect para multi-select", () => {
    const wrapper = makeWrapper({ multi: true });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.fieldType).toBe("multiselect");
  });

  it("lê placeholder de .react-select__placeholder", () => {
    const wrapper = makeWrapper({ placeholder: "Selecione a cidade" });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.placeholder).toBe("Selecione a cidade");
  });

  it("placeholder undefined quando não há .react-select__placeholder", () => {
    const wrapper = makeWrapper();
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.placeholder).toBeUndefined();
  });

  it("hidden input como filho direto: name é preenchido", () => {
    const wrapper = makeWrapper({ hiddenInputName: "cidade" });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.name).toBe("cidade");
  });

  it("hidden input embrulhado em div (multi-select): name é preenchido", () => {
    const wrapper = makeWrapper({
      hiddenInputName: "tags",
      hiddenInputWrapped: true,
      multi: true,
    });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.name).toBe("tags");
    expect(field.fieldType).toBe("multiselect");
  });

  it("sem hidden input: name é undefined", () => {
    const wrapper = makeWrapper({ searchable: true });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.name).toBeUndefined();
  });

  it("id vem do visibleInput quando presente", () => {
    const wrapper = makeWrapper({
      searchable: true,
      visibleInputId: "estado-select",
    });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.id).toBe("estado-select");
  });

  it("id vem do wrapper quando visibleInput não tem id", () => {
    const wrapper = makeWrapper({ searchable: true });
    wrapper.id = "campo-select";
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.id).toBe("campo-select");
  });

  it("id é undefined quando nem visibleInput nem wrapper têm id", () => {
    const wrapper = makeWrapper({ searchable: true });
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.id).toBeUndefined();
  });

  it("contextSignals é preenchido via buildSignals", () => {
    const wrapper = makeWrapper();
    document.body.appendChild(wrapper);

    const field = reactSelectAdapter.buildField(wrapper);

    expect(field.contextSignals).toBe("mock signals");
  });
});

// ─── fill() ──────────────────────────────────────────────────────────────────

describe("reactSelectAdapter.fill()", () => {
  beforeEach(() => {
    document.body.innerHTML = "";
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
    document.body.innerHTML = "";
  });

  it("retorna false quando .react-select__control não existe", async () => {
    const wrapper = makeWrapper({ noControl: true });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "valor");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(false);
  });

  it("retorna false quando menu não aparece (timeout)", async () => {
    // Sem menu inline, sem menu portalizado → timeout de 800ms → false
    const wrapper = makeWrapper({ searchable: true });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "opção");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(false);
  });

  it("retorna false quando menu está presente mas sem opções", async () => {
    // Menu inline com zero opções disponíveis
    const wrapper = makeWrapper({ withInlineMenu: [] });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "qualquer");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(false);
  });

  it("retorna true no single-select com opção encontrada por textContent", async () => {
    const wrapper = makeWrapper({
      searchable: false,
      withInlineMenu: ["São Paulo", "Rio de Janeiro", "Belo Horizonte"],
    });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "São Paulo");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(true);
  });

  it("retorna true no single-select sem match exato (usa primeiro disponível)", async () => {
    const wrapper = makeWrapper({
      searchable: false,
      withInlineMenu: ["Opção A", "Opção B"],
    });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "inexistente");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(true);
  });

  it("retorna true no single-select com match por dataset.value", async () => {
    const wrapper = makeWrapper({
      searchable: false,
      withInlineMenu: ["Estado A"],
    });
    document.body.appendChild(wrapper);

    // dataset.value é setado como optText.toLowerCase() pelo helper
    const fillPromise = reactSelectAdapter.fill(wrapper, "estado a");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(true);
  });

  it("retorna true no multi-select (clica em até 3 opções)", async () => {
    const wrapper = makeWrapper({
      multi: true,
      withInlineMenu: ["Tag 1", "Tag 2", "Tag 3"],
    });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "Tag 1");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(true);
  });

  it("single-select searchable: dispara eventos input/change no searchInput", async () => {
    const wrapper = makeWrapper({
      searchable: true,
      withInlineMenu: ["Resultado Filtrado"],
    });
    document.body.appendChild(wrapper);

    const searchInput = wrapper.querySelector<HTMLInputElement>(
      ".react-select__input",
    )!;

    const inputSpy = vi.fn();
    const changeSpy = vi.fn();
    searchInput.addEventListener("input", inputSpy);
    searchInput.addEventListener("change", changeSpy);

    const fillPromise = reactSelectAdapter.fill(wrapper, "resultado");
    await vi.runAllTimersAsync();
    await fillPromise;

    expect(inputSpy).toHaveBeenCalledOnce();
    expect(changeSpy).toHaveBeenCalledOnce();
  });
});

// ─── waitForReactSelectMenu() via fill() ─────────────────────────────────────

describe("reactSelectAdapter.fill() — waitForReactSelectMenu", () => {
  beforeEach(() => {
    document.body.innerHTML = "";
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
    document.body.innerHTML = "";
  });

  it("resolve imediatamente quando menu inline já existe no wrapper", async () => {
    const wrapper = makeWrapper({
      searchable: false,
      withInlineMenu: ["Opção Inline"],
    });
    document.body.appendChild(wrapper);

    const fillPromise = reactSelectAdapter.fill(wrapper, "Opção Inline");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(true);
  });

  it("resolve imediatamente quando menu portalizado já existe via aria-controls", async () => {
    const listboxId = "react-select-portal-listbox";
    const wrapper = makeWrapper({
      searchable: false,
      withAriaControls: listboxId,
    });
    document.body.appendChild(wrapper);
    addPortaledMenu(listboxId, ["Opção Portalizada"]);

    const fillPromise = reactSelectAdapter.fill(wrapper, "Opção Portalizada");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(true);
  });

  it("sem aria-controls e sem menu portalizado → findPortaled retorna null", async () => {
    // Wrapper sem aria-controls no input; nenhum .react-select__menu em document.body
    // → findPortaled() retorna null → timeout → false
    const wrapper = makeWrapper({ searchable: false });
    document.body.appendChild(wrapper);
    const fillPromise = reactSelectAdapter.fill(wrapper, "valor");
    await vi.runAllTimersAsync();
    const result = await fillPromise;

    expect(result).toBe(false);
  });

  it("MutationObserver detecta menu adicionado ao wrapper após fill() iniciar", async () => {
    vi.useRealTimers();

    const wrapper = makeWrapper({ searchable: false });
    document.body.appendChild(wrapper);

    // Inicia o fill — menu ainda não existe, MutationObserver ficará escutando
    const fillPromise = reactSelectAdapter.fill(wrapper, "Dinâmico");

    // Simula react-select adicionando o menu inline ao wrapper após um tick
    await new Promise<void>((r) => setTimeout(r, 10));

    const menu = document.createElement("div");
    menu.className = "react-select__menu";
    const opt = document.createElement("div");
    opt.className = "react-select__option";
    opt.textContent = "Dinâmico";
    menu.appendChild(opt);
    // Append ao wrapper: findInline() irá encontrar
    wrapper.appendChild(menu);

    const result = await fillPromise;
    expect(result).toBe(true);
  }, 2000);
});

// ─── name e selector ─────────────────────────────────────────────────────────

describe("reactSelectAdapter — propriedades", () => {
  it("name está correto", () => {
    expect(reactSelectAdapter.name).toBe("react-select");
  });

  it("selector está correto", () => {
    expect(reactSelectAdapter.selector).toBe(
      ".react-select-container:not(.react-select--is-disabled)",
    );
  });
});