Merge branch 'APP-972/lead-capture-form' of https://github.com/OpenHands/OpenHands into APP-972/lead-capture-form

This commit is contained in:
HeyItsChloe
2026-03-19 09:48:19 -07:00
6 changed files with 293 additions and 3 deletions

View File

@@ -180,17 +180,18 @@ describe("InformationRequestForm", () => {
expect(messageInput).toHaveAttribute("aria-invalid", "false");
});
it("should not navigate when form is submitted with empty fields", async () => {
it("should not show modal when form is submitted with empty fields", async () => {
const user = userEvent.setup();
renderWithRouter();
const submitButton = screen.getByRole("button", { name: "ENTERPRISE$FORM_SUBMIT" });
await user.click(submitButton);
expect(screen.queryByTestId("request-submitted-modal")).not.toBeInTheDocument();
expect(mockNavigate).not.toHaveBeenCalled();
});
it("should navigate when form is submitted with all fields filled", async () => {
it("should show modal when form is submitted with all fields filled", async () => {
const user = userEvent.setup();
renderWithRouter();
@@ -202,6 +203,26 @@ describe("InformationRequestForm", () => {
const submitButton = screen.getByRole("button", { name: "ENTERPRISE$FORM_SUBMIT" });
await user.click(submitButton);
expect(screen.getByTestId("request-submitted-modal")).toBeInTheDocument();
});
it("should navigate to homepage when modal Done button is clicked", async () => {
const user = userEvent.setup();
renderWithRouter();
// Fill form and submit
await user.type(screen.getByTestId("form-input-name"), "John Doe");
await user.type(screen.getByTestId("form-input-company"), "Acme Inc");
await user.type(screen.getByTestId("form-input-email"), "john@example.com");
await user.type(screen.getByTestId("form-input-message"), "Hello world");
const submitButton = screen.getByRole("button", { name: "ENTERPRISE$FORM_SUBMIT" });
await user.click(submitButton);
// Click Done on modal
const doneButton = screen.getByRole("button", { name: "ENTERPRISE$DONE_BUTTON" });
await user.click(doneButton);
expect(mockNavigate).toHaveBeenCalledWith("/");
});

View File

@@ -0,0 +1,113 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { RequestSubmittedModal } from "#/components/features/onboarding/request-submitted-modal";
describe("RequestSubmittedModal", () => {
const defaultProps = {
onClose: vi.fn(),
};
it("should render the modal", () => {
render(<RequestSubmittedModal {...defaultProps} />);
expect(screen.getByTestId("request-submitted-modal")).toBeInTheDocument();
});
it("should render the title", () => {
render(<RequestSubmittedModal {...defaultProps} />);
expect(
screen.getByText("ENTERPRISE$REQUEST_SUBMITTED_TITLE"),
).toBeInTheDocument();
});
it("should render the description", () => {
render(<RequestSubmittedModal {...defaultProps} />);
expect(
screen.getByText("ENTERPRISE$REQUEST_SUBMITTED_DESCRIPTION"),
).toBeInTheDocument();
});
it("should render the Done button", () => {
render(<RequestSubmittedModal {...defaultProps} />);
expect(
screen.getByRole("button", { name: "ENTERPRISE$DONE_BUTTON" }),
).toBeInTheDocument();
});
it("should render the close button", () => {
render(<RequestSubmittedModal {...defaultProps} />);
expect(
screen.getByRole("button", { name: "MODAL$CLOSE_BUTTON_LABEL" }),
).toBeInTheDocument();
});
it("should call onClose when Done button is clicked", async () => {
const mockOnClose = vi.fn();
const user = userEvent.setup();
render(<RequestSubmittedModal onClose={mockOnClose} />);
const doneButton = screen.getByRole("button", {
name: "ENTERPRISE$DONE_BUTTON",
});
await user.click(doneButton);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it("should call onClose when close button is clicked", async () => {
const mockOnClose = vi.fn();
const user = userEvent.setup();
render(<RequestSubmittedModal onClose={mockOnClose} />);
const closeButton = screen.getByRole("button", {
name: "MODAL$CLOSE_BUTTON_LABEL",
});
await user.click(closeButton);
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it("should call onClose when Escape key is pressed", async () => {
const mockOnClose = vi.fn();
const user = userEvent.setup();
render(<RequestSubmittedModal onClose={mockOnClose} />);
await user.keyboard("{Escape}");
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it("should call onClose when backdrop is clicked", async () => {
const mockOnClose = vi.fn();
const user = userEvent.setup();
render(<RequestSubmittedModal onClose={mockOnClose} />);
// Click on the backdrop (the semi-transparent overlay)
const backdrop = screen.getByRole("dialog").querySelector(".bg-black");
if (backdrop) {
await user.click(backdrop);
}
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it("should have proper accessibility attributes", () => {
render(<RequestSubmittedModal {...defaultProps} />);
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("aria-modal", "true");
expect(dialog).toHaveAttribute(
"aria-label",
"ENTERPRISE$REQUEST_SUBMITTED_TITLE",
);
});
});

View File

@@ -5,6 +5,7 @@ import { I18nKey } from "#/i18n/declaration";
import { Card } from "#/ui/card";
import { Text } from "#/ui/typography";
import { FormInput } from "./form-input";
import { RequestSubmittedModal } from "./request-submitted-modal";
import OpenHandsLogoWhite from "#/assets/branding/openhands-logo-white.svg?react";
import CloudIcon from "#/icons/cloud-minimal.svg?react";
import StackedIcon from "#/icons/stacked.svg?react";
@@ -29,6 +30,7 @@ export function InformationRequestForm({
message: "",
});
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
const [showModal, setShowModal] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -45,7 +47,12 @@ export function InformationRequestForm({
return;
}
// TODO: Implement form submission
// TODO: Implement actual form submission API call
setShowModal(true);
};
const handleModalClose = () => {
setShowModal(false);
navigate("/");
};
@@ -185,6 +192,8 @@ export function InformationRequestForm({
<Text className="text-[#8C8C8C]">{cardDescription}</Text>
</Card>
</div>
{showModal && <RequestSubmittedModal onClose={handleModalClose} />}
</div>
);
}

View File

@@ -0,0 +1,75 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
interface RequestSubmittedModalProps {
onClose: () => void;
}
export function RequestSubmittedModal({ onClose }: RequestSubmittedModalProps) {
const { t } = useTranslation();
return (
<ModalBackdrop
onClose={onClose}
aria-label={t(I18nKey.ENTERPRISE$REQUEST_SUBMITTED_TITLE)}
>
<div
data-testid="request-submitted-modal"
className="w-[448px] bg-black rounded-md border border-[#242424] border-t-[#242424]"
style={{
boxShadow:
"0px 4px 6px -4px rgba(0, 0, 0, 0.1), 0px 10px 15px -3px rgba(0, 0, 0, 0.1)",
}}
>
{/* Header with close button */}
<div className="relative p-6 pb-0">
<button
type="button"
onClick={onClose}
aria-label={t(I18nKey.MODAL$CLOSE_BUTTON_LABEL)}
className="absolute top-[17px] right-[17px] w-4 h-4 flex items-center justify-center opacity-70 hover:opacity-100 transition-opacity rounded-sm"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 4L4 12M4 4L12 12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{/* Title and description */}
<div className="flex flex-col gap-1.5 pr-8">
<h2 className="text-lg font-semibold text-white leading-[18px] tracking-[-0.45px]">
{t(I18nKey.ENTERPRISE$REQUEST_SUBMITTED_TITLE)}
</h2>
<p className="text-sm text-[#8C8C8C] leading-5">
{t(I18nKey.ENTERPRISE$REQUEST_SUBMITTED_DESCRIPTION)}
</p>
</div>
</div>
{/* Footer with Done button */}
<div className="p-6 pt-4 flex justify-end">
<button
type="button"
onClick={onClose}
aria-label={t(I18nKey.ENTERPRISE$DONE_BUTTON)}
className="px-4 py-2 text-sm font-medium bg-white text-black rounded hover:bg-gray-100 transition-colors"
>
{t(I18nKey.ENTERPRISE$DONE_BUTTON)}
</button>
</div>
</div>
</ModalBackdrop>
);
}

View File

@@ -1218,5 +1218,9 @@ export enum I18nKey {
ENTERPRISE$FORM_MESSAGE_SAAS_PLACEHOLDER = "ENTERPRISE$FORM_MESSAGE_SAAS_PLACEHOLDER",
ENTERPRISE$FORM_MESSAGE_SELF_HOSTED_PLACEHOLDER = "ENTERPRISE$FORM_MESSAGE_SELF_HOSTED_PLACEHOLDER",
ENTERPRISE$FORM_SUBMIT = "ENTERPRISE$FORM_SUBMIT",
ENTERPRISE$REQUEST_SUBMITTED_TITLE = "ENTERPRISE$REQUEST_SUBMITTED_TITLE",
ENTERPRISE$REQUEST_SUBMITTED_DESCRIPTION = "ENTERPRISE$REQUEST_SUBMITTED_DESCRIPTION",
ENTERPRISE$DONE_BUTTON = "ENTERPRISE$DONE_BUTTON",
COMMON$BACK = "COMMON$BACK",
MODAL$CLOSE_BUTTON_LABEL = "MODAL$CLOSE_BUTTON_LABEL",
}

View File

@@ -20711,6 +20711,57 @@
"tr": "Gönder",
"uk": "Надіслати"
},
"ENTERPRISE$REQUEST_SUBMITTED_TITLE": {
"en": "Request submitted",
"ja": "リクエストが送信されました",
"zh-CN": "请求已提交",
"zh-TW": "請求已提交",
"ko-KR": "요청이 제출되었습니다",
"no": "Forespørsel sendt",
"ar": "تم إرسال الطلب",
"de": "Anfrage gesendet",
"fr": "Demande soumise",
"it": "Richiesta inviata",
"pt": "Solicitação enviada",
"es": "Solicitud enviada",
"ca": "Sol·licitud enviada",
"tr": "İstek gönderildi",
"uk": "Запит надіслано"
},
"ENTERPRISE$REQUEST_SUBMITTED_DESCRIPTION": {
"en": "Your request has been submitted. We will follow up with next steps shortly.",
"ja": "リクエストが送信されました。次のステップについて、まもなくご連絡いたします。",
"zh-CN": "您的请求已提交。我们将很快与您联系后续步骤。",
"zh-TW": "您的請求已提交。我們將很快與您聯繫後續步驟。",
"ko-KR": "요청이 제출되었습니다. 곧 다음 단계에 대해 연락드리겠습니다.",
"no": "Forespørselen din er sendt. Vi følger opp med neste trinn snart.",
"ar": "تم إرسال طلبك. سنتواصل معك قريباً بشأن الخطوات التالية.",
"de": "Ihre Anfrage wurde gesendet. Wir werden uns in Kürze mit den nächsten Schritten bei Ihnen melden.",
"fr": "Votre demande a été soumise. Nous vous contacterons prochainement pour les prochaines étapes.",
"it": "La tua richiesta è stata inviata. Ti contatteremo a breve con i prossimi passi.",
"pt": "Sua solicitação foi enviada. Entraremos em contato em breve com os próximos passos.",
"es": "Su solicitud ha sido enviada. Nos pondremos en contacto pronto con los próximos pasos.",
"ca": "La seva sol·licitud s'ha enviat. Ens posarem en contacte aviat amb els propers passos.",
"tr": "İsteğiniz gönderildi. En kısa sürede sonraki adımlar hakkında sizinle iletişime geçeceğiz.",
"uk": "Ваш запит надіслано. Ми зв'яжемося з вами найближчим часом щодо наступних кроків."
},
"ENTERPRISE$DONE_BUTTON": {
"en": "Done",
"ja": "完了",
"zh-CN": "完成",
"zh-TW": "完成",
"ko-KR": "완료",
"no": "Ferdig",
"ar": "تم",
"de": "Fertig",
"fr": "Terminé",
"it": "Fatto",
"pt": "Concluído",
"es": "Hecho",
"ca": "Fet",
"tr": "Tamam",
"uk": "Готово"
},
"COMMON$BACK": {
"en": "Back",
"ja": "戻る",
@@ -20727,5 +20778,22 @@
"ca": "Enrere",
"tr": "Geri",
"uk": "Назад"
},
"MODAL$CLOSE_BUTTON_LABEL": {
"en": "Close",
"ja": "閉じる",
"zh-CN": "关闭",
"zh-TW": "關閉",
"ko-KR": "닫기",
"no": "Lukk",
"ar": "إغلاق",
"de": "Schließen",
"fr": "Fermer",
"it": "Chiudi",
"pt": "Fechar",
"es": "Cerrar",
"ca": "Tancar",
"tr": "Kapat",
"uk": "Закрити"
}
}