Compare commits

..

2 Commits

Author SHA1 Message Date
Cursor Agent
520c337748 test: add unit tests for safeFormRequestSubmit utility
- Test requestSubmit method when available
- Test fallback behavior for iOS Safari 15.5
- Test validation failure prevents form submission
- Ensures proper event dispatching with correct properties
2026-01-21 02:54:03 +00:00
Cursor Agent
7f5c93b629 fix: add iOS Safari 15.5 compatibility for form.requestSubmit()
- Created safeFormRequestSubmit utility function with fallback for browsers
  that don't support requestSubmit() method (iOS Safari < 16.0)
- Updated block-conditional.tsx to use safe form submission
- Updated login-form.tsx to use safe form submission
- Fallback uses reportValidity() for validation UI and dispatches submit event

Fixes TypeError: n.requestSubmit is not a function on iOS Safari 15.5
2026-01-21 02:53:04 +00:00
6 changed files with 110 additions and 43 deletions

View File

@@ -19,6 +19,7 @@ import { TwoFactorBackup } from "@/modules/ee/two-factor-auth/components/two-fac
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
import { PasswordInput } from "@/modules/ui/components/password-input";
import { safeFormRequestSubmit } from "@/modules/ui/lib/utils";
const ZLoginForm = z.object({
email: z.string().email(),
@@ -236,7 +237,7 @@ export const LoginForm = ({
// Add a slight delay before focusing the input field to ensure it's visible
setTimeout(() => emailRef.current?.focus(), 100);
} else if (formRef.current) {
formRef.current.requestSubmit();
safeFormRequestSubmit(formRef.current);
}
}}
className="relative w-full justify-center"

View File

@@ -4,3 +4,27 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Safely requests form submission with validation.
* Provides a fallback for browsers that don't support requestSubmit() (iOS Safari < 16.0).
* @param form The form element to submit
*/
export function safeFormRequestSubmit(form: HTMLFormElement): void {
// Check if requestSubmit is supported (iOS Safari 16.0+, all modern browsers)
if (typeof form.requestSubmit === "function") {
form.requestSubmit();
} else {
// Fallback for older browsers (iOS Safari < 16.0)
// reportValidity() triggers native validation UI
if (!form.reportValidity()) {
return;
}
// Dispatch submit event manually to trigger form submission handlers
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true,
});
form.dispatchEvent(submitEvent);
}
}

View File

@@ -14,42 +14,7 @@ import { SubmitButton } from "@/components/buttons/submit-button";
import { ElementConditional } from "@/components/general/element-conditional";
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
import { getLocalizedValue } from "@/lib/i18n";
import { cn } from "@/lib/utils";
/**
* Safely calls requestSubmit on a form element with fallback for browsers
* that don't support it (e.g., Mobile Safari 15.5)
*/
const safeRequestSubmit = (form: HTMLFormElement | null | undefined): void => {
if (!form) {
return;
}
// Check if requestSubmit is available
if (typeof form.requestSubmit === "function") {
try {
form.requestSubmit();
} catch (error) {
// Fallback if requestSubmit throws an error
console.warn("[Formbricks] form.requestSubmit() failed, using fallback:", error);
dispatchSubmitEvent(form);
}
} else {
// Fallback for browsers that don't support requestSubmit
dispatchSubmitEvent(form);
}
};
/**
* Fallback method to trigger form validation by dispatching a submit event
*/
const dispatchSubmitEvent = (form: HTMLFormElement): void => {
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true,
});
form.dispatchEvent(submitEvent);
};
import { cn, safeFormRequestSubmit } from "@/lib/utils";
interface BlockConditionalProps {
block: TSurveyBlock;
@@ -176,7 +141,7 @@ export function BlockConditional({
response.length < rankingElement.choices.length);
if (hasIncompleteRanking) {
safeRequestSubmit(form);
safeFormRequestSubmit(form);
return false;
}
return true;
@@ -209,7 +174,7 @@ export function BlockConditional({
element.type === TSurveyElementTypeEnum.ContactInfo
) {
if (!form.checkValidity()) {
safeRequestSubmit(form);
safeFormRequestSubmit(form);
return false;
}
return true;
@@ -226,14 +191,14 @@ export function BlockConditional({
response &&
hasUnansweredRows(response, element)
) {
safeRequestSubmit(form);
safeFormRequestSubmit(form);
return false;
}
// For other element types, check if required fields are empty
// CTA elements should not block navigation even if marked required (as they are informational)
if (element.type !== TSurveyElementTypeEnum.CTA && element.required && isEmptyResponse(response)) {
safeRequestSubmit(form);
safeFormRequestSubmit(form);
return false;
}
@@ -264,7 +229,9 @@ export function BlockConditional({
// Call each form's submit method to trigger TTC calculation
block.elements.forEach((element) => {
const form = elementFormRefs.current.get(element.id);
safeRequestSubmit(form);
if (form) {
safeFormRequestSubmit(form);
}
});
// Collect TTC from the ref (populated synchronously by form submissions)

View File

@@ -10,6 +10,7 @@ import {
getMimeType,
getShuffledChoicesIds,
getShuffledRowIndices,
safeFormRequestSubmit,
} from "./utils";
// Mock crypto.getRandomValues for deterministic shuffle tests
@@ -327,3 +328,54 @@ describe("findBlockByElementId", () => {
expect(block).toBeUndefined();
});
});
describe("safeFormRequestSubmit", () => {
let mockForm: HTMLFormElement;
beforeEach(() => {
// Create a mock form element
mockForm = document.createElement("form");
});
test("should call requestSubmit when it's supported", () => {
// Mock requestSubmit as a function
const requestSubmitSpy = vi.fn();
mockForm.requestSubmit = requestSubmitSpy;
safeFormRequestSubmit(mockForm);
expect(requestSubmitSpy).toHaveBeenCalled();
});
test("should use fallback when requestSubmit is not supported", () => {
// Remove requestSubmit to simulate iOS Safari 15.5
mockForm.requestSubmit = undefined as unknown as typeof mockForm.requestSubmit;
const reportValiditySpy = vi.spyOn(mockForm, "reportValidity").mockReturnValue(true);
const dispatchEventSpy = vi.spyOn(mockForm, "dispatchEvent");
safeFormRequestSubmit(mockForm);
expect(reportValiditySpy).toHaveBeenCalled();
expect(dispatchEventSpy).toHaveBeenCalled();
// Verify the submit event was dispatched with correct properties
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchedEvent.type).toBe("submit");
expect(dispatchedEvent.bubbles).toBe(true);
expect(dispatchedEvent.cancelable).toBe(true);
});
test("should not dispatch event when reportValidity returns false", () => {
// Remove requestSubmit to simulate iOS Safari 15.5
mockForm.requestSubmit = undefined as unknown as typeof mockForm.requestSubmit;
const reportValiditySpy = vi.spyOn(mockForm, "reportValidity").mockReturnValue(false);
const dispatchEventSpy = vi.spyOn(mockForm, "dispatchEvent");
safeFormRequestSubmit(mockForm);
expect(reportValiditySpy).toHaveBeenCalled();
expect(dispatchEventSpy).not.toHaveBeenCalled();
});
});

View File

@@ -275,3 +275,27 @@ export const getFirstElementIdInBlock = (
const block = survey.blocks.find((b) => b.id === blockId);
return block?.elements[0]?.id;
};
/**
* Safely requests form submission with validation.
* Provides a fallback for browsers that don't support requestSubmit() (iOS Safari < 16.0).
* @param form The form element to submit
*/
export const safeFormRequestSubmit = (form: HTMLFormElement): void => {
// Check if requestSubmit is supported (iOS Safari 16.0+, all modern browsers)
if (typeof form.requestSubmit === "function") {
form.requestSubmit();
} else {
// Fallback for older browsers (iOS Safari < 16.0)
// reportValidity() triggers native validation UI
if (!form.reportValidity()) {
return;
}
// Dispatch submit event manually to trigger form submission handlers
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true,
});
form.dispatchEvent(submitEvent);
}
};

1
pnpm-lock.yaml generated
View File

@@ -10249,7 +10249,6 @@ packages:
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
tarn@3.0.2:
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}