refactor(frontend): simplify enterprise lead submission without service layer

- Remove form-submission-service directory (api + types + tests)
- Inline API call directly in useSubmitEnterpriseLead mutation hook
- Fix API endpoint to /api/forms/submit (was incorrectly /api/v1/forms/submit)
- Update tests to mock openHands.post directly

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-20 14:06:30 +00:00
parent 8f9a69b4d9
commit 2d0d763de5
5 changed files with 69 additions and 181 deletions

View File

@@ -1,90 +0,0 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { formSubmissionService } from "#/api/form-submission-service/form-submission-service.api";
const { mockPost } = vi.hoisted(() => ({ mockPost: vi.fn() }));
vi.mock("#/api/open-hands-axios", () => ({
openHands: { post: mockPost },
}));
describe("formSubmissionService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("submitEnterpriseLead", () => {
const mockFormData = {
requestType: "saas" as const,
name: "John Doe",
company: "Acme Corp",
email: "john@acme.com",
message: "Interested in enterprise plan.",
};
const mockResponse = {
id: "test-submission-id",
status: "pending",
created_at: "2025-03-19T00:00:00Z",
};
it("should call the correct endpoint with correct payload", async () => {
mockPost.mockResolvedValue({ data: mockResponse });
await formSubmissionService.submitEnterpriseLead(mockFormData);
expect(mockPost).toHaveBeenCalledTimes(1);
expect(mockPost).toHaveBeenCalledWith(
"/api/v1/forms/submit",
{
form_type: "enterprise_lead",
answers: {
request_type: "saas",
name: "John Doe",
company: "Acme Corp",
email: "john@acme.com",
message: "Interested in enterprise plan.",
},
},
{ withCredentials: true },
);
});
it("should return the response data", async () => {
mockPost.mockResolvedValue({ data: mockResponse });
const result =
await formSubmissionService.submitEnterpriseLead(mockFormData);
expect(result).toEqual(mockResponse);
});
it("should handle self-hosted request type", async () => {
mockPost.mockResolvedValue({ data: mockResponse });
const selfHostedFormData = {
...mockFormData,
requestType: "self-hosted" as const,
};
await formSubmissionService.submitEnterpriseLead(selfHostedFormData);
expect(mockPost).toHaveBeenCalledWith(
"/api/v1/forms/submit",
expect.objectContaining({
answers: expect.objectContaining({
request_type: "self-hosted",
}),
}),
{ withCredentials: true },
);
});
it("should propagate errors from the API", async () => {
const mockError = new Error("Network error");
mockPost.mockRejectedValue(mockError);
await expect(
formSubmissionService.submitEnterpriseLead(mockFormData),
).rejects.toThrow("Network error");
});
});
});

View File

@@ -1,10 +1,10 @@
import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { formSubmissionService } from "#/api/form-submission-service/form-submission-service.api";
import { openHands } from "#/api/open-hands-axios";
import { useSubmitEnterpriseLead } from "#/hooks/mutation/use-submit-enterprise-lead";
vi.mock("#/api/form-submission-service/form-submission-service.api");
vi.mock("#/api/open-hands-axios");
describe("useSubmitEnterpriseLead", () => {
const mockFormData = {
@@ -25,10 +25,8 @@ describe("useSubmitEnterpriseLead", () => {
vi.clearAllMocks();
});
it("should call submitEnterpriseLead with correct data", async () => {
vi.mocked(formSubmissionService.submitEnterpriseLead).mockResolvedValue(
mockResponse,
);
it("should call API with correct payload", async () => {
vi.mocked(openHands.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useSubmitEnterpriseLead(), {
wrapper: ({ children }) => (
@@ -41,16 +39,25 @@ describe("useSubmitEnterpriseLead", () => {
result.current.mutate(mockFormData);
await waitFor(() => {
expect(
formSubmissionService.submitEnterpriseLead,
).toHaveBeenCalledWith(mockFormData);
expect(openHands.post).toHaveBeenCalledWith(
"/api/forms/submit",
{
form_type: "enterprise_lead",
answers: {
request_type: "saas",
name: "John Doe",
company: "Acme Corp",
email: "john@acme.com",
message: "Interested in enterprise plan.",
},
},
{ withCredentials: true },
);
});
});
it("should return success state after successful submission", async () => {
vi.mocked(formSubmissionService.submitEnterpriseLead).mockResolvedValue(
mockResponse,
);
vi.mocked(openHands.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useSubmitEnterpriseLead(), {
wrapper: ({ children }) => (
@@ -70,9 +77,7 @@ describe("useSubmitEnterpriseLead", () => {
it("should return error state after failed submission", async () => {
const mockError = new Error("Network error");
vi.mocked(formSubmissionService.submitEnterpriseLead).mockRejectedValue(
mockError,
);
vi.mocked(openHands.post).mockRejectedValue(mockError);
const { result } = renderHook(() => useSubmitEnterpriseLead(), {
wrapper: ({ children }) => (
@@ -100,10 +105,8 @@ describe("useSubmitEnterpriseLead", () => {
});
});
it("should call submitEnterpriseLead with self-hosted request type", async () => {
vi.mocked(formSubmissionService.submitEnterpriseLead).mockResolvedValue(
mockResponse,
);
it("should handle self-hosted request type", async () => {
vi.mocked(openHands.post).mockResolvedValue({ data: mockResponse });
const selfHostedFormData = {
...mockFormData,
@@ -121,23 +124,28 @@ describe("useSubmitEnterpriseLead", () => {
result.current.mutate(selfHostedFormData);
await waitFor(() => {
expect(
formSubmissionService.submitEnterpriseLead,
).toHaveBeenCalledWith(selfHostedFormData);
expect(openHands.post).toHaveBeenCalledWith(
"/api/forms/submit",
expect.objectContaining({
answers: expect.objectContaining({
request_type: "self-hosted",
}),
}),
{ withCredentials: true },
);
});
});
it("should be in pending state while submitting", async () => {
// Create a promise that we can control
let resolvePromise: (value: typeof mockResponse) => void;
const controlledPromise = new Promise<typeof mockResponse>((resolve) => {
resolvePromise = resolve;
});
vi.mocked(formSubmissionService.submitEnterpriseLead).mockReturnValue(
controlledPromise,
let resolvePromise: (value: { data: typeof mockResponse }) => void;
const controlledPromise = new Promise<{ data: typeof mockResponse }>(
(resolve) => {
resolvePromise = resolve;
},
);
vi.mocked(openHands.post).mockReturnValue(controlledPromise);
const { result } = renderHook(() => useSubmitEnterpriseLead(), {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
@@ -152,8 +160,7 @@ describe("useSubmitEnterpriseLead", () => {
expect(result.current.isPending).toBe(true);
});
// Resolve the promise
resolvePromise!(mockResponse);
resolvePromise!({ data: mockResponse });
await waitFor(() => {
expect(result.current.isPending).toBe(false);

View File

@@ -1,35 +0,0 @@
import { openHands } from "../open-hands-axios";
import {
EnterpriseLeadFormData,
FormSubmissionResponse,
} from "./form-submission-service.types";
/**
* Form Submission Service API - Handles form submission endpoints
*/
export const formSubmissionService = {
/**
* Submit an enterprise lead capture form
* @param formData - The form data containing requestType, name, company, email, and message
* @returns The submission response with id, status, and created_at
*/
submitEnterpriseLead: async (
formData: EnterpriseLeadFormData,
): Promise<FormSubmissionResponse> => {
const { data } = await openHands.post<FormSubmissionResponse>(
"/api/v1/forms/submit",
{
form_type: "enterprise_lead",
answers: {
request_type: formData.requestType,
name: formData.name,
company: formData.company,
email: formData.email,
message: formData.message,
},
},
{ withCredentials: true },
);
return data;
},
};

View File

@@ -1,20 +0,0 @@
export type RequestType = "saas" | "self-hosted";
export interface EnterpriseLeadFormData {
requestType: RequestType;
name: string;
company: string;
email: string;
message: string;
}
export interface FormSubmissionRequest {
form_type: string;
answers: Record<string, unknown>;
}
export interface FormSubmissionResponse {
id: string;
status: string;
created_at: string;
}

View File

@@ -1,6 +1,13 @@
import { useMutation } from "@tanstack/react-query";
import { formSubmissionService } from "#/api/form-submission-service/form-submission-service.api";
import { EnterpriseLeadFormData } from "#/api/form-submission-service/form-submission-service.types";
import { openHands } from "#/api/open-hands-axios";
export interface EnterpriseLeadFormData {
requestType: "saas" | "self-hosted";
name: string;
company: string;
email: string;
message: string;
}
/**
* Hook for submitting enterprise lead capture forms.
@@ -8,6 +15,25 @@ import { EnterpriseLeadFormData } from "#/api/form-submission-service/form-submi
*/
export const useSubmitEnterpriseLead = () =>
useMutation({
mutationFn: (formData: EnterpriseLeadFormData) =>
formSubmissionService.submitEnterpriseLead(formData),
mutationFn: async (formData: EnterpriseLeadFormData) => {
const { data } = await openHands.post<{
id: string;
status: string;
created_at: string;
}>(
"/api/forms/submit",
{
form_type: "enterprise_lead",
answers: {
request_type: formData.requestType,
name: formData.name,
company: formData.company,
email: formData.email,
message: formData.message,
},
},
{ withCredentials: true },
);
return data;
},
});