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
5 changed files with 108 additions and 7 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,7 +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";
import { cn, safeFormRequestSubmit } from "@/lib/utils";
interface BlockConditionalProps {
block: TSurveyBlock;
@@ -141,7 +141,7 @@ export function BlockConditional({
response.length < rankingElement.choices.length);
if (hasIncompleteRanking) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
return true;
@@ -174,7 +174,7 @@ export function BlockConditional({
element.type === TSurveyElementTypeEnum.ContactInfo
) {
if (!form.checkValidity()) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
return true;
@@ -191,14 +191,14 @@ export function BlockConditional({
response &&
hasUnansweredRows(response, element)
) {
form.requestSubmit();
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)) {
form.requestSubmit();
safeFormRequestSubmit(form);
return false;
}
@@ -230,7 +230,7 @@ export function BlockConditional({
block.elements.forEach((element) => {
const form = elementFormRefs.current.get(element.id);
if (form) {
form.requestSubmit();
safeFormRequestSubmit(form);
}
});

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);
}
};