src/lib/form/__tests__/form-filler.test.ts
File Relationships
Symbols by Kind
function
3
All Symbols
| Name | Kind | Visibility | Status | Lines | Signature |
|---|---|---|---|---|---|
| makeInput | function | - | 95-102 | makeInput(type = "text", id = "", name = ""): : HTMLInputElement |
|
| makeField | function | - | 104-119 | makeField(
el: HTMLElement,
overrides: Partial<FormField> = {},
): : FormField |
|
| mockStreamFields | function | - | 148-152 | mockStreamFields(fields: FormField[]): : void |
Full Source
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { FormField, GenerationResult, SavedForm, Settings } from "@/types";
// ── Mocks ────────────────────────────────────────────────────────────────────
const {
mockDetectAllFieldsAsync,
mockStreamAllFields,
mockResolveFieldValue,
mockIsChromeAiAvailable,
mockChromeAiGenerate,
mockGenerateFormContextValuesViaProxy,
mockTensorFlowGenerate,
mockGetSettings,
mockGetIgnoredFieldsForUrl,
mockSetFillingInProgress,
mockFillCustomComponent,
mockExtractCustomComponentValue,
mockGenerate,
mockCreateProgressNotification,
} = vi.hoisted(() => {
const progressMock = {
show: vi.fn(),
showAiGenerating: vi.fn(),
hideAiGenerating: vi.fn(),
addDetecting: vi.fn(),
updateDetected: vi.fn(),
addFilling: vi.fn(),
updateFilled: vi.fn(),
updateError: vi.fn(),
done: vi.fn(),
destroy: vi.fn(),
};
return {
mockDetectAllFieldsAsync: vi.fn(),
mockStreamAllFields: vi.fn(),
mockResolveFieldValue: vi.fn(),
mockIsChromeAiAvailable: vi.fn().mockResolvedValue(false),
mockChromeAiGenerate: vi.fn(),
mockGenerateFormContextValuesViaProxy: vi.fn().mockResolvedValue({}),
mockTensorFlowGenerate: vi.fn(),
mockGetSettings: vi.fn(),
mockGetIgnoredFieldsForUrl: vi.fn().mockResolvedValue([]),
mockSetFillingInProgress: vi.fn(),
mockFillCustomComponent: vi.fn().mockResolvedValue(false),
mockExtractCustomComponentValue: vi.fn().mockReturnValue(null),
mockGenerate: vi.fn().mockReturnValue("generated-value"),
mockCreateProgressNotification: vi.fn().mockReturnValue(progressMock),
};
});
vi.mock("../form-detector", () => ({
detectAllFieldsAsync: mockDetectAllFieldsAsync,
streamAllFields: mockStreamAllFields,
}));
vi.mock("@/lib/rules/rule-engine", () => ({
resolveFieldValue: mockResolveFieldValue,
}));
vi.mock("@/lib/ai/chrome-ai-proxy", () => ({
isAvailableViaProxy: mockIsChromeAiAvailable,
generateFieldValueViaProxy: mockChromeAiGenerate,
generateFormContextValuesViaProxy: mockGenerateFormContextValuesViaProxy,
}));
vi.mock("@/lib/ai/tensorflow-generator", () => ({
generateWithTensorFlow: mockTensorFlowGenerate,
}));
vi.mock("@/lib/storage/storage", () => ({
getSettings: mockGetSettings,
getIgnoredFieldsForUrl: mockGetIgnoredFieldsForUrl,
}));
vi.mock("../dom-watcher", () => ({
setFillingInProgress: mockSetFillingInProgress,
}));
vi.mock("../adapters/adapter-registry", () => ({
fillCustomComponent: mockFillCustomComponent,
extractCustomComponentValue: mockExtractCustomComponentValue,
}));
vi.mock("@/lib/generators", () => ({ generate: mockGenerate }));
vi.mock("@/lib/logger", () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
logAuditFill: vi.fn(),
}));
vi.mock("../progress-notification", () => ({
createProgressNotification: mockCreateProgressNotification,
}));
// ── Helpers ──────────────────────────────────────────────────────────────────
function makeInput(type = "text", id = "", name = ""): HTMLInputElement {
const el = document.createElement("input");
el.type = type;
if (id) el.id = id;
if (name) el.name = name;
document.body.appendChild(el);
return el;
}
function makeField(
el: HTMLElement,
overrides: Partial<FormField> = {},
): FormField {
return {
element: el as HTMLInputElement,
selector: `#${(el as HTMLInputElement).id || "field"}`,
fieldType: "text",
category: "text",
label: "Label",
detectionMethod: "keyword",
detectionConfidence: 0.9,
required: false,
...overrides,
} as FormField;
}
const DEFAULT_SETTINGS: Settings = {
autoFillOnLoad: false,
defaultStrategy: "random",
useChromeAI: false,
forceAIFirst: false,
shortcut: "Alt+Shift+F",
locale: "pt-BR",
highlightFilled: false,
cacheEnabled: false,
showFieldIcon: false,
fieldIconPosition: "inside",
detectionPipeline: [],
debugLog: false,
logLevel: "warn",
uiLanguage: "auto",
fillEmptyOnly: false,
watcherEnabled: false,
watcherDebounceMs: 600,
watcherAutoRefill: true,
watcherShadowDOM: false,
logMaxEntries: 1000,
aiTimeoutMs: 5000,
showAiBadge: false,
showFillToast: false,
};
/** Helper: wraps an array of fields into an async generator for streamAllFields mock */
function mockStreamFields(fields: FormField[]): void {
mockStreamAllFields.mockImplementation(async function* () {
for (const f of fields) yield f;
});
}
// ── Import SUT AFTER mocks ────────────────────────────────────────────────────
import {
applyTemplate,
captureFormValues,
fillAllFields,
fillContextualAI,
fillSingleField,
} from "../form-filler";
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("form-filler", () => {
beforeEach(() => {
vi.clearAllMocks();
document.body.innerHTML = "";
mockGetSettings.mockResolvedValue({ ...DEFAULT_SETTINGS });
mockGetIgnoredFieldsForUrl.mockResolvedValue([]);
mockIsChromeAiAvailable.mockResolvedValue(false);
});
// ── fillAllFields ──────────────────────────────────────────────────────────
describe("fillAllFields", () => {
it("fills all detected fields and returns results", async () => {
const el = makeInput("text", "email-field");
const field = makeField(el, { fieldType: "email", label: "Email" });
mockStreamFields([field]);
mockResolveFieldValue.mockResolvedValue({
fieldSelector: "#email-field",
value: "test@example.com",
source: "generator",
} satisfies GenerationResult);
const results = await fillAllFields();
expect(results).toHaveLength(1);
expect(results[0].value).toBe("test@example.com");
expect(mockSetFillingInProgress).toHaveBeenCalledWith(true);
expect(mockSetFillingInProgress).toHaveBeenCalledWith(false);
});
it("skips ignored fields", async () => {
const el = makeInput("text", "ignored-field");
const field = makeField(el, { selector: "#ignored-field" });
mockStreamFields([field]);
mockGetIgnoredFieldsForUrl.mockResolvedValue([
{ selector: "#ignored-field", label: "Ignored" },
]);
const results = await fillAllFields();
expect(results).toHaveLength(0);
expect(mockResolveFieldValue).not.toHaveBeenCalled();
});
it("skips fields with existing values when fillEmptyOnly=true", async () => {
const el = makeInput("text", "pre-filled");
el.value = "existing value";
const field = makeField(el, { selector: "#pre-filled" });
mockStreamFields([field]);
mockGetSettings.mockResolvedValue({
...DEFAULT_SETTINGS,
fillEmptyOnly: true,
});
const results = await fillAllFields();
expect(results).toHaveLength(0);
});
it("respects fillEmptyOnly override via options", async () => {
const el = makeInput("text", "pre-filled2");
el.value = "existing";
const field = makeField(el, { selector: "#pre-filled2" });
mockStreamFields([field]);
mockGetSettings.mockResolvedValue({
...DEFAULT_SETTINGS,
fillEmptyOnly: false,
});
mockResolveFieldValue.mockResolvedValue({
fieldSelector: "#pre-filled2",
value: "x",
source: "generator",
});
// Explicitly pass fillEmptyOnly=true to override setting
const results = await fillAllFields({ fillEmptyOnly: true });
expect(results).toHaveLength(0);
});
it("returns empty array when no fields detected", async () => {
mockStreamFields([]);
const results = await fillAllFields();
expect(results).toHaveLength(0);
});
it("always resets setFillingInProgress(false) even when it throws", async () => {
const el = makeInput("text", "err-field");
const field = makeField(el);
mockStreamFields([field]);
mockResolveFieldValue.mockRejectedValue(new Error("resolver failed"));
// Should not throw, just skip
const results = await fillAllFields();
expect(results).toHaveLength(0);
expect(mockSetFillingInProgress).toHaveBeenCalledWith(false);
});
it("fills select element by choosing a random option", async () => {
const sel = document.createElement("select");
sel.id = "sel-field";
const opt = document.createElement("option");
opt.value = "BR";
opt.text = "Brazil";
sel.appendChild(opt);
document.body.appendChild(sel);
const field = makeField(sel as unknown as HTMLInputElement, {
element: sel as unknown as HTMLInputElement,
selector: "#sel-field",
fieldType: "select",
});
mockStreamFields([field]);
mockResolveFieldValue.mockResolvedValue({
fieldSelector: "#sel-field",
value: "BR",
source: "rule",
});
const results = await fillAllFields();
expect(results).toHaveLength(1);
expect(sel.value).toBe("BR");
});
it("handles checkbox field", async () => {
const cb = makeInput("checkbox", "cb-field");
const field = makeField(cb, {
fieldType: "checkbox",
selector: "#cb-field",
});
mockStreamFields([field]);
mockResolveFieldValue.mockResolvedValue({
fieldSelector: "#cb-field",
value: "true",
source: "generator",
});
await fillAllFields();
expect(cb.checked).toBe(true);
});
});
// ── fillSingleField ────────────────────────────────────────────────────────
describe("fillSingleField", () => {
it("fills single field and returns result", async () => {
const el = makeInput("email", "single-email");
const field = makeField(el, { fieldType: "email" });
mockGetSettings.mockResolvedValue({ ...DEFAULT_SETTINGS });
mockResolveFieldValue.mockResolvedValue({
fieldSelector: "#single-email",
value: "user@test.com",
source: "ai",
} satisfies GenerationResult);
const result = await fillSingleField(field);
expect(result).not.toBeNull();
expect(result?.value).toBe("user@test.com");
});
it("returns null when resolver throws", async () => {
const el = makeInput("text", "bad-field");
const field = makeField(el);
mockResolveFieldValue.mockRejectedValue(new Error("AI down"));
const result = await fillSingleField(field);
expect(result).toBeNull();
});
});
// ── captureFormValues ──────────────────────────────────────────────────────
describe("captureFormValues", () => {
it("captures values indexed by id", async () => {
const el = makeInput("text", "my-name");
el.value = "Marcus";
const field = makeField(el, { id: "my-name", selector: "#my-name" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
const values = await captureFormValues();
expect(values["my-name"]).toBe("Marcus");
});
it("captures values indexed by name when id missing", async () => {
const el = makeInput("text", "", "user_email");
el.value = "me@test.com";
const field = makeField(el, {
id: undefined,
name: "user_email",
selector: 'input[name="user_email"]',
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
const values = await captureFormValues();
expect(values["user_email"]).toBe("me@test.com");
});
it("captures checkbox state as string", async () => {
const el = makeInput("checkbox", "my-cb");
el.checked = true;
const field = makeField(el, { id: "my-cb", selector: "#my-cb" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
const values = await captureFormValues();
expect(values["my-cb"]).toBe("true");
});
it("captures textarea value", async () => {
const ta = document.createElement("textarea");
ta.id = "my-ta";
ta.value = "hello textarea";
document.body.appendChild(ta);
const field = makeField(ta as unknown as HTMLInputElement, {
element: ta as unknown as HTMLInputElement,
id: "my-ta",
selector: "#my-ta",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
const values = await captureFormValues();
expect(values["my-ta"]).toBe("hello textarea");
});
it("returns empty object when no fields detected", async () => {
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [] });
const values = await captureFormValues();
expect(values).toEqual({});
});
it("captures value via custom adapter extractor when adapterName is set", async () => {
const wrapper = document.createElement("div");
wrapper.id = "custom";
// adapterName is arbitrary here, the mock will supply the result
const field = makeField(wrapper, {
id: "custom",
selector: "#custom",
adapterName: "antd-select",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockExtractCustomComponentValue.mockReturnValue("from-adapter");
const values = await captureFormValues();
expect(values["custom"]).toBe("from-adapter");
expect(mockExtractCustomComponentValue).toHaveBeenCalledWith(field);
});
});
// ── applyTemplate ──────────────────────────────────────────────────────────
describe("applyTemplate", () => {
it("applies fixed-value template by selector", async () => {
const el = makeInput("text", "t-name");
const field = makeField(el, { selector: "#t-name", fieldType: "text" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
const form: SavedForm = {
id: "form-1",
name: "Test Form",
urlPattern: "*",
createdAt: Date.now(),
updatedAt: Date.now(),
templateFields: [
{
key: "#t-name",
label: "Name",
mode: "fixed" as const,
fixedValue: "Alice",
},
],
fields: {},
};
const { filled } = await applyTemplate(form);
expect(filled).toBe(1);
expect(el.value).toBe("Alice");
});
it("uses custom adapter filler when field.adapterName is present", async () => {
const wrapper = document.createElement("div");
wrapper.id = "a1";
const field = makeField(wrapper, {
selector: "#a1",
id: "a1",
adapterName: "antd-select",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGetSettings.mockResolvedValue(DEFAULT_SETTINGS);
const form: SavedForm = {
id: "form-1",
name: "Test Form",
urlPattern: "*",
fields: { a1: "val" },
createdAt: Date.now(),
updatedAt: Date.now(),
};
const { filled } = await applyTemplate(form);
expect(mockFillCustomComponent).toHaveBeenCalledWith(field, "val");
expect(filled).toBe(1);
});
it("applies generator-mode template by selector", async () => {
const el = makeInput("text", "t-email");
const field = makeField(el, {
selector: "#t-email",
fieldType: "email",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGenerate.mockReturnValue("gen@example.com");
const form: SavedForm = {
id: "form-2",
name: "Gen Form",
urlPattern: "*",
createdAt: Date.now(),
updatedAt: Date.now(),
templateFields: [
{
key: "#t-email",
label: "Email",
mode: "generator" as const,
generatorType: "email",
},
],
fields: {},
};
const { filled } = await applyTemplate(form);
expect(filled).toBe(1);
expect(mockGenerate).toHaveBeenCalledWith("email", undefined);
});
it("applies type-based template matching all fields of given type", async () => {
const el1 = makeInput("email", "email-1");
const el2 = makeInput("email", "email-2");
const field1 = makeField(el1, {
selector: "#email-1",
fieldType: "email",
});
const field2 = makeField(el2, {
selector: "#email-2",
fieldType: "email",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field1, field2] });
const form: SavedForm = {
id: "form-3",
name: "Type Form",
urlPattern: "*",
createdAt: Date.now(),
updatedAt: Date.now(),
templateFields: [
{
key: "email",
label: "Email",
mode: "fixed" as const,
fixedValue: "type@example.com",
matchByFieldType: "email",
},
],
fields: {},
};
const { filled } = await applyTemplate(form);
expect(filled).toBe(2);
});
it("falls back to legacy fields when templateFields is empty", async () => {
const el = makeInput("text", "legacy-id");
const field = makeField(el, { id: "legacy-id", selector: "#legacy-id" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
const form: SavedForm = {
id: "form-4",
name: "Legacy Form",
urlPattern: "*",
createdAt: Date.now(),
updatedAt: Date.now(),
templateFields: [],
fields: { "legacy-id": "Legacy Value" },
};
const { filled } = await applyTemplate(form);
expect(filled).toBe(1);
expect(el.value).toBe("Legacy Value");
});
it("returns filled=0 when no fields match template", async () => {
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [] });
const form: SavedForm = {
id: "form-5",
name: "Empty Form",
urlPattern: "*",
createdAt: Date.now(),
updatedAt: Date.now(),
templateFields: [],
fields: { "#nonexistent": "value" },
};
const { filled } = await applyTemplate(form);
expect(filled).toBe(0);
});
});
// ── fillContextualAI ───────────────────────────────────────────────────────
describe("fillContextualAI", () => {
it("fills fields using values from contextMap", async () => {
const el = makeInput("text", "name-field");
const field = makeField(el, {
selector: "#name-field",
fieldType: "name",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGenerateFormContextValuesViaProxy.mockResolvedValue({
"0": "João Silva",
});
const results = await fillContextualAI();
expect(results).toHaveLength(1);
expect(results[0]!.value).toBe("João Silva");
expect(el.value).toBe("João Silva");
});
it("returns empty array when no fields detected", async () => {
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [] });
const results = await fillContextualAI();
expect(results).toHaveLength(0);
expect(mockGenerateFormContextValuesViaProxy).not.toHaveBeenCalled();
});
it("skips ignored fields", async () => {
const el1 = makeInput("text", "normal-field");
const el2 = makeInput("text", "ignored-field");
const field1 = makeField(el1, { selector: "#normal-field" });
const field2 = makeField(el2, { selector: "#ignored-field" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field1, field2] });
mockGetIgnoredFieldsForUrl.mockResolvedValue([
{ selector: "#ignored-field", label: "Ignored", urlPattern: "*" },
]);
mockGenerateFormContextValuesViaProxy.mockResolvedValue({ "0": "value" });
const results = await fillContextualAI();
// Only field1 should be in contextInputs (index 0)
expect(results).toHaveLength(1);
expect(results[0]!.fieldSelector).toBe("#normal-field");
expect(el2.value).toBe("");
});
it("returns empty when all fields are ignored", async () => {
const el = makeInput("text", "only-field");
const field = makeField(el, { selector: "#only-field" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGetIgnoredFieldsForUrl.mockResolvedValue([
{ selector: "#only-field", label: "Only", urlPattern: "*" },
]);
const results = await fillContextualAI();
expect(results).toHaveLength(0);
expect(mockGenerateFormContextValuesViaProxy).not.toHaveBeenCalled();
});
it("skips fields with values when fillEmptyOnly=true", async () => {
const el1 = makeInput("text", "empty-field");
const el2 = makeInput("text", "filled-field");
el2.value = "already filled";
const field1 = makeField(el1, { selector: "#empty-field" });
const field2 = makeField(el2, { selector: "#filled-field" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field1, field2] });
mockGetSettings.mockResolvedValue({
...DEFAULT_SETTINGS,
fillEmptyOnly: true,
});
mockGenerateFormContextValuesViaProxy.mockResolvedValue({
"0": "new value",
});
const results = await fillContextualAI();
// Only empty field1 should be filled (index 0)
expect(results).toHaveLength(1);
expect(results[0]!.fieldSelector).toBe("#empty-field");
expect(el2.value).toBe("already filled");
// AI receives only 1 field (the empty one)
expect(mockGenerateFormContextValuesViaProxy).toHaveBeenCalledWith(
expect.arrayContaining([expect.objectContaining({ index: 0 })]),
undefined,
undefined,
undefined,
);
expect(
mockGenerateFormContextValuesViaProxy.mock.calls[0]![0],
).toHaveLength(1);
});
it("returns empty when all fields are pre-filled and fillEmptyOnly=true", async () => {
const el = makeInput("text", "pre-filled");
el.value = "has value";
const field = makeField(el, { selector: "#pre-filled" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGetSettings.mockResolvedValue({
...DEFAULT_SETTINGS,
fillEmptyOnly: true,
});
const results = await fillContextualAI();
expect(results).toHaveLength(0);
expect(mockGenerateFormContextValuesViaProxy).not.toHaveBeenCalled();
});
it("falls back to fillAllFields when AI returns empty contextMap", async () => {
const el = makeInput("text", "fallback-field");
const field = makeField(el, {
selector: "#fallback-field",
fieldType: "email",
});
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGenerateFormContextValuesViaProxy.mockResolvedValue({});
// fillAllFields fallback uses streamAllFields
mockStreamAllFields.mockImplementation(async function* () {
yield field;
});
mockResolveFieldValue.mockResolvedValue({
fieldSelector: "#fallback-field",
value: "fallback@test.com",
source: "generator",
});
const results = await fillContextualAI();
expect(results).toHaveLength(1);
expect(results[0]!.source).toBe("generator");
});
it("always calls setFillingInProgress(false) even when AI throws", async () => {
const el = makeInput("text", "err-field-ai");
const field = makeField(el, { selector: "#err-field-ai" });
mockDetectAllFieldsAsync.mockResolvedValue({ fields: [field] });
mockGenerateFormContextValuesViaProxy.mockRejectedValue(
new Error("AI unavailable"),
);
await expect(fillContextualAI()).rejects.toThrow("AI unavailable");
expect(mockSetFillingInProgress).toHaveBeenCalledWith(false);
});
});
});