src/lib/form/__tests__/form-detector.test.ts

Total Symbols
1
Lines of Code
305
Avg Complexity
2.0
Symbol Types
1

Symbols by Kind

function 1

All Symbols

Name Kind Visibility Status Lines Signature
makeField function - 51-66 makeField(id: string, parent?: HTMLElement): : FormField

Full Source

/**
 * @vitest-environment happy-dom
 */
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { FormField } from "@/types";

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

vi.mock("@/lib/form/detectors/classifiers", () => ({
  DEFAULT_PIPELINE: {},
  DEFAULT_COLLECTION_PIPELINE: {},
  nativeInputDetector: { detect: vi.fn().mockReturnValue([]) },
  detectNativeFieldsAsync: vi.fn().mockResolvedValue([]),
  streamNativeFieldsAsync: vi.fn().mockReturnValue((async function* () {})()),
  classifyCustomFieldsSync: vi
    .fn()
    .mockImplementation((fields: FormField[]) => fields),
  classifyCustomFieldsAsync: vi
    .fn()
    .mockImplementation((fields: FormField[]) => Promise.resolve(fields)),
}));

vi.mock("@/lib/form/adapters/adapter-registry", () => ({
  detectCustomComponents: vi.fn().mockReturnValue([]),
}));

import {
  detectAllFields,
  detectAllFieldsAsync,
  detectFormFields,
  streamAllFields,
} from "@/lib/form/form-detector";
import {
  nativeInputDetector,
  detectNativeFieldsAsync,
  streamNativeFieldsAsync,
  classifyCustomFieldsSync,
  classifyCustomFieldsAsync,
} from "@/lib/form/detectors/classifiers";
import { detectCustomComponents } from "@/lib/form/adapters/adapter-registry";

function makeField(id: string, parent?: HTMLElement): FormField {
  const element = document.createElement("input");
  element.id = id;
  if (parent) parent.appendChild(element);
  return {
    element,
    selector: `#${id}`,
    fieldType: "text",
    label: id,
    name: id,
    id,
    placeholder: "",
    required: false,
    category: "generic",
  };
}

describe("form-detector", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(nativeInputDetector.detect).mockReturnValue([]);
    vi.mocked(detectNativeFieldsAsync).mockResolvedValue([]);
    vi.mocked(streamNativeFieldsAsync).mockReturnValue(
      (async function* () {})(),
    );
    vi.mocked(classifyCustomFieldsSync).mockImplementation(
      (f) => f as FormField[],
    );
    vi.mocked(classifyCustomFieldsAsync).mockImplementation((f) =>
      Promise.resolve(f as FormField[]),
    );
    vi.mocked(detectCustomComponents).mockReturnValue([]);
  });

  // ─────────────────────── detectFormFields ───────────────────────────

  describe("detectFormFields", () => {
    it("retorna array de campos detectados", () => {
      const field = makeField("email");
      vi.mocked(nativeInputDetector.detect).mockReturnValue([field]);
      const result = detectFormFields();
      expect(result).toContain(field);
    });

    it("retorna array vazio quando não há campos", () => {
      expect(detectFormFields()).toEqual([]);
    });
  });

  // ─────────────────────── detectAllFields ────────────────────────────

  describe("detectAllFields", () => {
    it("retorna DetectionResult com propriedade fields", () => {
      const result = detectAllFields();
      expect(result).toHaveProperty("fields");
      expect(Array.isArray(result.fields)).toBe(true);
    });

    it("mescla campos nativos e customizados", () => {
      const native = makeField("native");
      const custom = makeField("custom");
      vi.mocked(nativeInputDetector.detect).mockReturnValue([native]);
      vi.mocked(detectCustomComponents).mockReturnValue([custom as any]);
      vi.mocked(classifyCustomFieldsSync).mockReturnValue([custom]);

      const { fields } = detectAllFields();
      expect(fields).toHaveLength(2);
      expect(fields).toContain(native);
      expect(fields).toContain(custom);
    });

    it("deduplica campos nativos contidos em wrapper de componente customizado", () => {
      const wrapper = document.createElement("div");
      document.body.appendChild(wrapper);

      const nativeElement = document.createElement("input");
      wrapper.appendChild(nativeElement);

      const nativeField: FormField = {
        element: nativeElement,
        selector: "div input",
        fieldType: "text",
        label: "native",
        name: "native",
        id: "native",
        placeholder: "",
        required: false,
        category: "generic",
      };

      const customField: FormField = {
        element: wrapper,
        selector: "div",
        fieldType: "text",
        label: "custom",
        name: "custom",
        id: "custom",
        placeholder: "",
        required: false,
        category: "generic",
      };

      vi.mocked(nativeInputDetector.detect).mockReturnValue([nativeField]);
      vi.mocked(detectCustomComponents).mockReturnValue([customField as any]);
      vi.mocked(classifyCustomFieldsSync).mockReturnValue([customField]);

      const { fields } = detectAllFields();
      // nativeField element is inside wrapper, so should be deduped
      expect(fields).not.toContain(nativeField);
      expect(fields).toContain(customField);

      wrapper.remove();
    });
  });

  // ─────────────────────── detectAllFieldsAsync ────────────────────────

  describe("detectAllFieldsAsync", () => {
    it("retorna DetectionResult assíncrono", async () => {
      const result = await detectAllFieldsAsync();
      expect(result).toHaveProperty("fields");
    });

    it("usa detectNativeFieldsAsync para campos nativos", async () => {
      const field = makeField("cpf");
      vi.mocked(detectNativeFieldsAsync).mockResolvedValue([field]);
      const { fields } = await detectAllFieldsAsync();
      expect(fields).toContain(field);
    });

    it("registra método de detecção em cada campo", async () => {
      const field = makeField("email");
      field.detectionMethod = "keyword";
      field.detectionConfidence = 0.9;
      field.detectionDurationMs = 5;
      vi.mocked(detectNativeFieldsAsync).mockResolvedValue([field]);
      const { fields } = await detectAllFieldsAsync();
      expect(fields).toContain(field);
    });

    it("não lança exceção quando campos têm propriedades ausentes", async () => {
      const field = makeField("test");
      // Remove optional properties
      delete (field as any).detectionMethod;
      delete (field as any).detectionDurationMs;
      vi.mocked(detectNativeFieldsAsync).mockResolvedValue([field]);
      await expect(detectAllFieldsAsync()).resolves.toHaveProperty("fields");
    });

    it("mescla campos customizados com nativos", async () => {
      const native = makeField("native");
      const custom = makeField("custom");
      vi.mocked(detectNativeFieldsAsync).mockResolvedValue([native]);
      vi.mocked(detectCustomComponents).mockReturnValue([custom as any]);
      vi.mocked(classifyCustomFieldsSync).mockReturnValue([custom]);

      const { fields } = await detectAllFieldsAsync();
      expect(fields).toHaveLength(2);
    });

    it("cobre elemento não-input, campos sem label/id/name e ordenação por duração", async () => {
      // Field with a <select> element (NOT HTMLInputElement) — covers the "—" branch
      const selectElement = document.createElement("select");
      const fieldNoInfo: FormField = {
        element: selectElement,
        selector: "select",
        fieldType: "select" as any,
        label: undefined,
        name: undefined,
        id: undefined,
        placeholder: "",
        required: false,
        category: "generic",
        detectionMethod: "custom-select",
        detectionConfidence: 1.0,
        detectionDurationMs: 15,
      };

      // Field with id but no label — covers label ?? id fallback in slowTop.map
      const field2: FormField = {
        element: document.createElement("input"),
        selector: "#f2",
        fieldType: "text" as any,
        label: undefined,
        name: undefined,
        id: "field2",
        placeholder: "",
        required: false,
        category: "generic",
        detectionMethod: "keyword",
        detectionDurationMs: 10,
      };

      // Field with name but no label or id — covers label ?? id ?? name fallback
      const field3: FormField = {
        element: document.createElement("input"),
        selector: "[name=f3]",
        fieldType: "text" as any,
        label: undefined,
        name: "field3",
        id: undefined,
        placeholder: "",
        required: false,
        category: "generic",
        detectionDurationMs: 5,
      };

      vi.mocked(detectNativeFieldsAsync).mockResolvedValue([
        fieldNoInfo,
        field2,
        field3,
      ]);
      const { fields } = await detectAllFieldsAsync();
      expect(fields).toHaveLength(3);
    });
  });

  // ─────────────────────── streamAllFields ────────────────────────────

  describe("streamAllFields", () => {
    it("emite campos nativos via stream", async () => {
      const field1 = makeField("f1");
      const field2 = makeField("f2");

      async function* fakeStream() {
        yield field1;
        yield field2;
      }
      vi.mocked(streamNativeFieldsAsync).mockReturnValue(fakeStream());

      const results: FormField[] = [];
      for await (const f of streamAllFields()) {
        results.push(f);
      }
      expect(results).toContain(field1);
      expect(results).toContain(field2);
    });

    it("emite campos customizados após campos nativos", async () => {
      const customField = makeField("custom");
      vi.mocked(streamNativeFieldsAsync).mockReturnValue(
        (async function* () {})(),
      );
      vi.mocked(detectCustomComponents).mockReturnValue([customField as any]);
      vi.mocked(classifyCustomFieldsSync).mockReturnValue([customField]);

      const results: FormField[] = [];
      for await (const f of streamAllFields()) {
        results.push(f);
      }
      expect(results).toContain(customField);
    });
  });
});