Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent 64c640b181 fix: add webkit messageHandlers polyfill for Instagram iOS browser
Fixes FORMBRICKS-NK

- Add comprehensive polyfill for window.webkit.messageHandlers
- Prevents TypeError when accessing unregistered handlers in Instagram's iOS in-app browser
- Handles three scenarios: webkit missing, messageHandlers missing, or handlers undefined
- Uses Proxy to safely intercept access and return no-op postMessage for missing handlers
- Includes comprehensive test coverage for all scenarios
2026-02-21 10:17:29 +00:00
23 changed files with 341 additions and 94 deletions
+1
View File
@@ -609,6 +609,7 @@ checksums:
environments/contacts/contacts_table_refresh: 6a959475991dd4ab28ad881bae569a09
environments/contacts/contacts_table_refresh_success: 40951396e88e5c8fdafa0b3bb4fadca8
environments/contacts/create_attribute: 87320615901f95b4f35ee83c290a3a6c
environments/contacts/create_key: 0d385c354af8963acbe35cd646710f86
environments/contacts/create_new_attribute: c17d407dacd0b90f360f9f5e899d662f
environments/contacts/create_new_attribute_description: cc19d76bb6940537bbe3461191f25d26
environments/contacts/custom_attributes: fffc7722742d1291b102dc737cf2fc9e
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Kontakte aktualisieren",
"contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert",
"create_attribute": "Attribut erstellen",
"create_key": "Schlüssel erstellen",
"create_new_attribute": "Neues Attribut erstellen",
"create_new_attribute_description": "Erstellen Sie ein neues Attribut für Segmentierungszwecke.",
"custom_attributes": "Benutzerdefinierte Attribute",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Refresh contacts",
"contacts_table_refresh_success": "Contacts refreshed successfully",
"create_attribute": "Create attribute",
"create_key": "Create Key",
"create_new_attribute": "Create new attribute",
"create_new_attribute_description": "Create a new attribute for segmentation purposes.",
"custom_attributes": "Custom Attributes",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Actualizar contactos",
"contacts_table_refresh_success": "Contactos actualizados correctamente",
"create_attribute": "Crear atributo",
"create_key": "Crear clave",
"create_new_attribute": "Crear atributo nuevo",
"create_new_attribute_description": "Crea un atributo nuevo para fines de segmentación.",
"custom_attributes": "Atributos personalizados",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Actualiser les contacts",
"contacts_table_refresh_success": "Contacts rafraîchis avec succès",
"create_attribute": "Créer un attribut",
"create_key": "Créer une clé",
"create_new_attribute": "Créer un nouvel attribut",
"create_new_attribute_description": "Créez un nouvel attribut à des fins de segmentation.",
"custom_attributes": "Attributs personnalisés",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Partnerek frissítése",
"contacts_table_refresh_success": "A partnerek sikeresen frissítve",
"create_attribute": "Attribútum létrehozása",
"create_key": "Kulcs létrehozása",
"create_new_attribute": "Új attribútum létrehozása",
"create_new_attribute_description": "Új attribútum létrehozása szakaszolási célokhoz.",
"custom_attributes": "Egyéni attribútumok",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "連絡先を更新",
"contacts_table_refresh_success": "連絡先を正常に更新しました",
"create_attribute": "属性を作成",
"create_key": "キーを作成",
"create_new_attribute": "新しい属性を作成",
"create_new_attribute_description": "セグメンテーション用の新しい属性を作成します。",
"custom_attributes": "カスタム属性",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Vernieuw contacten",
"contacts_table_refresh_success": "Contacten zijn vernieuwd",
"create_attribute": "Attribuut aanmaken",
"create_key": "Sleutel aanmaken",
"create_new_attribute": "Nieuw attribuut aanmaken",
"create_new_attribute_description": "Maak een nieuw attribuut aan voor segmentatiedoeleinden.",
"custom_attributes": "Aangepaste kenmerken",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Atualizar contatos",
"contacts_table_refresh_success": "Contatos atualizados com sucesso",
"create_attribute": "Criar atributo",
"create_key": "Criar chave",
"create_new_attribute": "Criar novo atributo",
"create_new_attribute_description": "Crie um novo atributo para fins de segmentação.",
"custom_attributes": "Atributos personalizados",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Atualizar contactos",
"contacts_table_refresh_success": "Contactos atualizados com sucesso",
"create_attribute": "Criar atributo",
"create_key": "Criar chave",
"create_new_attribute": "Criar novo atributo",
"create_new_attribute_description": "Crie um novo atributo para fins de segmentação.",
"custom_attributes": "Atributos personalizados",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Reîmprospătare contacte",
"contacts_table_refresh_success": "Contactele au fost actualizate cu succes",
"create_attribute": "Creează atribut",
"create_key": "Creează cheie",
"create_new_attribute": "Creează atribut nou",
"create_new_attribute_description": "Creează un atribut nou pentru segmentare.",
"custom_attributes": "Atribute personalizate",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Обновить контакты",
"contacts_table_refresh_success": "Контакты успешно обновлены",
"create_attribute": "Создать атрибут",
"create_key": "Создать ключ",
"create_new_attribute": "Создать новый атрибут",
"create_new_attribute_description": "Создайте новый атрибут для целей сегментации.",
"custom_attributes": "Пользовательские атрибуты",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "Uppdatera kontakter",
"contacts_table_refresh_success": "Kontakter uppdaterade",
"create_attribute": "Skapa attribut",
"create_key": "Skapa nyckel",
"create_new_attribute": "Skapa nytt attribut",
"create_new_attribute_description": "Skapa ett nytt attribut för segmenteringsändamål.",
"custom_attributes": "Anpassade attribut",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "刷新 联系人",
"contacts_table_refresh_success": "联系人 已成功刷新",
"create_attribute": "创建属性",
"create_key": "创建键",
"create_new_attribute": "创建新属性",
"create_new_attribute_description": "为细分目的创建新属性。",
"custom_attributes": "自定义属性",
+1
View File
@@ -645,6 +645,7 @@
"contacts_table_refresh": "重新整理聯絡人",
"contacts_table_refresh_success": "聯絡人已成功重新整理",
"create_attribute": "建立屬性",
"create_key": "建立金鑰",
"create_new_attribute": "建立新屬性",
"create_new_attribute_description": "建立新屬性以進行分群用途。",
"custom_attributes": "自訂屬性",
@@ -250,7 +250,7 @@ export function CreateAttributeModal({ environmentId }: Readonly<CreateAttribute
disabled={!formData.key || !formData.name || !!keyError}
loading={isCreating}
type="submit">
{t("environments.contacts.create_attribute")}
{t("environments.contacts.create_key")}
</Button>
</DialogFooter>
</form>
@@ -2,8 +2,6 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { generateWebhookSecret } from "@/lib/crypto";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
@@ -26,7 +24,6 @@ import { ZWebhookInput } from "@/modules/integrations/webhooks/types/webhooks";
const ZCreateWebhookAction = z.object({
environmentId: ZId,
webhookInput: ZWebhookInput,
webhookSecret: z.string().optional(),
});
export const createWebhookAction = authenticatedActionClient.schema(ZCreateWebhookAction).action(
@@ -50,11 +47,7 @@ export const createWebhookAction = authenticatedActionClient.schema(ZCreateWebho
},
],
});
const webhook = await createWebhook(
parsedInput.environmentId,
parsedInput.webhookInput,
parsedInput.webhookSecret
);
const webhook = await createWebhook(parsedInput.environmentId, parsedInput.webhookInput);
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.newObject = parsedInput.webhookInput;
return webhook;
@@ -138,43 +131,10 @@ export const updateWebhookAction = authenticatedActionClient.schema(ZUpdateWebho
const ZTestEndpointAction = z.object({
url: z.string(),
webhookId: ZId.optional(),
secret: z.string().optional(),
});
export const testEndpointAction = authenticatedActionClient
.schema(ZTestEndpointAction)
.action(async ({ ctx, parsedInput }) => {
let secret: string | undefined;
if (parsedInput.webhookId) {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromWebhookId(parsedInput.webhookId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId: await getProjectIdFromWebhookId(parsedInput.webhookId),
},
],
});
const webhookResult = await getWebhook(parsedInput.webhookId);
if (!webhookResult.ok) {
throw new ResourceNotFoundError("Webhook", parsedInput.webhookId);
}
secret = webhookResult.data.secret ?? undefined;
} else {
// New webhook, use the provided secret or generate a new one
secret = parsedInput.secret ?? generateWebhookSecret();
}
await testEndpoint(parsedInput.url, secret);
return { success: true, secret };
.action(async ({ parsedInput }) => {
return testEndpoint(parsedInput.url);
});
@@ -53,22 +53,16 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
const [selectedAllSurveys, setSelectedAllSurveys] = useState(false);
const [creatingWebhook, setCreatingWebhook] = useState(false);
const [createdWebhook, setCreatedWebhook] = useState<Webhook | null>(null);
const [webhookSecret, setWebhookSecret] = useState<string | undefined>();
const handleTestEndpoint = async (
sendSuccessToast: boolean
): Promise<{ success: boolean; secret?: string }> => {
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
try {
const { valid, error } = validWebHookURL(testEndpointInput);
if (!valid) {
toast.error(error ?? t("common.something_went_wrong_please_try_again"));
return { success: false };
return;
}
setHittingEndpoint(true);
const testEndpointActionResult = await testEndpointAction({
url: testEndpointInput,
secret: webhookSecret,
});
const testEndpointActionResult = await testEndpointAction({ url: testEndpointInput });
if (!testEndpointActionResult?.data) {
const errorMessage = getFormattedErrorMessage(testEndpointActionResult);
throw new Error(errorMessage);
@@ -76,10 +70,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
setHittingEndpoint(false);
if (sendSuccessToast) toast.success(t("environments.integrations.webhooks.endpoint_pinged"));
setEndpointAccessible(true);
if (testEndpointActionResult.data.secret) {
setWebhookSecret(testEndpointActionResult.data.secret);
}
return testEndpointActionResult.data;
return true;
} catch (err) {
setHittingEndpoint(false);
toast.error(
@@ -92,7 +83,7 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
);
console.error(t("environments.integrations.webhooks.webhook_test_failed_due_to"), err.message);
setEndpointAccessible(false);
return { success: false };
return false;
}
};
@@ -136,8 +127,8 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
throw new Error(t("environments.integrations.webhooks.discord_webhook_not_supported"));
}
const testResult = await handleTestEndpoint(false);
if (!testResult.success) return;
const endpointHitSuccessfully = await handleTestEndpoint(false);
if (!endpointHitSuccessfully) return;
const updatedData: TWebhookInput = {
name: data.name,
@@ -150,7 +141,6 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
const createWebhookActionResult = await createWebhookAction({
environmentId,
webhookInput: updatedData,
webhookSecret: testResult.secret,
});
if (createWebhookActionResult?.data) {
router.refresh();
@@ -177,7 +167,6 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
setSelectedTriggers([]);
setSelectedAllSurveys(false);
setCreatedWebhook(null);
setWebhookSecret(undefined);
};
// Show success dialog with secret after webhook creation
@@ -58,19 +58,16 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: We
setTimeout(() => setCopied(false), 2000);
};
const handleTestEndpoint = async (sendSuccessToast: boolean): Promise<boolean> => {
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
try {
const { valid, error } = validWebHookURL(testEndpointInput);
if (!valid) {
toast.error(error ?? t("common.something_went_wrong_please_try_again"));
return false;
return;
}
setHittingEndpoint(true);
const testEndpointActionResult = await testEndpointAction({
url: testEndpointInput,
webhookId: webhook.id,
});
if (!testEndpointActionResult?.data?.success) {
const testEndpointActionResult = await testEndpointAction({ url: testEndpointInput });
if (!testEndpointActionResult?.data) {
const errorMessage = getFormattedErrorMessage(testEndpointActionResult);
throw new Error(errorMessage);
}
@@ -223,7 +220,7 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen, isReadOnly }: We
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 transform"
className="absolute top-1/2 right-3 -translate-y-1/2 transform"
onClick={() => setShowSecret(!showSecret)}>
{showSecret ? (
<EyeOff className="h-5 w-5 text-slate-400" />
@@ -61,23 +61,19 @@ export const deleteWebhook = async (id: string): Promise<boolean> => {
}
};
export const createWebhook = async (
environmentId: string,
webhookInput: TWebhookInput,
secret?: string
): Promise<Webhook> => {
export const createWebhook = async (environmentId: string, webhookInput: TWebhookInput): Promise<Webhook> => {
try {
if (isDiscordWebhook(webhookInput.url)) {
throw new UnknownError("Discord webhooks are currently not supported.");
}
const signingSecret = secret ?? generateWebhookSecret();
const secret = generateWebhookSecret();
const webhook = await prisma.webhook.create({
data: {
...webhookInput,
surveyIds: webhookInput.surveyIds || [],
secret: signingSecret,
secret,
environment: {
connect: {
id: environmentId,
@@ -122,7 +118,7 @@ export const getWebhooks = async (environmentId: string): Promise<Webhook[]> =>
}
};
export const testEndpoint = async (url: string, secret?: string): Promise<boolean> => {
export const testEndpoint = async (url: string): Promise<boolean> => {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
@@ -135,25 +131,19 @@ export const testEndpoint = async (url: string, secret?: string): Promise<boolea
const webhookTimestamp = Math.floor(Date.now() / 1000);
const body = JSON.stringify({ event: "testEndpoint" });
const requestHeaders: Record<string, string> = {
"Content-Type": "application/json",
"webhook-id": webhookMessageId,
"webhook-timestamp": webhookTimestamp.toString(),
};
if (secret) {
requestHeaders["webhook-signature"] = generateStandardWebhookSignature(
webhookMessageId,
webhookTimestamp,
body,
secret
);
}
// Generate a temporary test secret and signature for consistency with actual webhooks
const testSecret = generateWebhookSecret();
const signature = generateStandardWebhookSignature(webhookMessageId, webhookTimestamp, body, testSecret);
const response = await fetch(url, {
method: "POST",
body,
headers: requestHeaders,
headers: {
"Content-Type": "application/json",
"webhook-id": webhookMessageId,
"webhook-timestamp": webhookTimestamp.toString(),
"webhook-signature": signature,
},
signal: controller.signal,
});
clearTimeout(timeout);
+1 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
test: {
environment: "node",
environmentMatchGlobs: [["**/*.test.tsx", "jsdom"]],
exclude: ["playwright/**", "node_modules/**", ".next/**"],
exclude: ["playwright/**", "node_modules/**"],
setupFiles: ["./vitestSetup.ts"],
env: loadEnv("", process.cwd(), ""),
coverage: {
+46
View File
@@ -6,6 +6,52 @@ import { FILE_PICK_EVENT } from "@/lib/constants";
import { getI18nLanguage } from "@/lib/i18n-utils";
import { addCustomThemeToDom, addStylesToDom, setStyleNonce } from "@/lib/styles";
// Polyfill for webkit messageHandlers to prevent errors in browsers that don't fully support it
// (e.g., Instagram's iOS in-app browser). This prevents TypeError when accessing unregistered handlers.
if (typeof window !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- WebKit types are not standard
const win = window as any;
// Create a Proxy that safely handles access to potentially undefined message handlers
const createMessageHandlersProxy = (originalHandlers: any = {}) => {
return new Proxy(originalHandlers, {
get(target, prop) {
const handler = target[prop as keyof typeof target];
// If the handler doesn't exist, return a safe mock object with a no-op postMessage
if (!handler) {
return {
postMessage: () => {
// Silently ignore - the message handler is not registered in this environment
console.debug(`WebKit message handler "${String(prop)}" is not available in this environment`);
},
};
}
return handler;
},
});
};
// Handle three scenarios:
// 1. window.webkit doesn't exist at all (Instagram iOS browser)
// 2. window.webkit exists but messageHandlers doesn't
// 3. Both exist but handlers might be missing
if (!win.webkit) {
// Scenario 1: Create the entire webkit object with proxied messageHandlers
win.webkit = {
messageHandlers: createMessageHandlersProxy(),
};
} else if (!win.webkit.messageHandlers) {
// Scenario 2: webkit exists but messageHandlers doesn't
win.webkit.messageHandlers = createMessageHandlersProxy();
} else {
// Scenario 3: Both exist, wrap existing messageHandlers with proxy
const originalMessageHandlers = win.webkit.messageHandlers;
win.webkit.messageHandlers = createMessageHandlersProxy(originalMessageHandlers);
}
}
export const renderSurveyInline = (props: SurveyContainerProps) => {
const inlineProps: SurveyContainerProps = {
...props,
@@ -0,0 +1,250 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("WebKit messageHandlers polyfill", () => {
let originalWebkit: any;
let consoleDebugSpy: any;
beforeEach(() => {
// Save the original webkit object if it exists
originalWebkit = (window as any).webkit;
consoleDebugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
});
afterEach(() => {
// Restore the original webkit object
if (originalWebkit) {
(window as any).webkit = originalWebkit;
} else {
delete (window as any).webkit;
}
consoleDebugSpy.mockRestore();
});
// Helper function to apply the polyfill logic (same as in index.ts)
const applyPolyfill = () => {
const win = window as any;
const createMessageHandlersProxy = (originalHandlers: any = {}) => {
return new Proxy(originalHandlers, {
get(target, prop) {
const handler = target[prop as keyof typeof target];
if (!handler) {
return {
postMessage: () => {
console.debug(
`WebKit message handler "${String(prop)}" is not available in this environment`
);
},
};
}
return handler;
},
});
};
if (!win.webkit) {
win.webkit = {
messageHandlers: createMessageHandlersProxy(),
};
} else if (!win.webkit.messageHandlers) {
win.webkit.messageHandlers = createMessageHandlersProxy();
} else {
const originalMessageHandlers = win.webkit.messageHandlers;
win.webkit.messageHandlers = createMessageHandlersProxy(originalMessageHandlers);
}
};
describe("Scenario 1: window.webkit does not exist (Instagram iOS browser)", () => {
it("should create window.webkit with proxied messageHandlers", () => {
// Setup: Remove webkit completely
delete (window as any).webkit;
// Apply polyfill
applyPolyfill();
// Test: webkit should now exist
expect((window as any).webkit).toBeDefined();
expect((window as any).webkit.messageHandlers).toBeDefined();
});
it("should not throw when accessing undefined messageHandlers", () => {
// Setup
delete (window as any).webkit;
// Apply polyfill
applyPolyfill();
// Test: Accessing an undefined handler should not throw
expect(() => {
(window as any).webkit.messageHandlers.undefinedHandler.postMessage("test");
}).not.toThrow();
// Verify console.debug was called
expect(consoleDebugSpy).toHaveBeenCalledWith(
'WebKit message handler "undefinedHandler" is not available in this environment'
);
});
it("should handle multiple undefined handlers without throwing", () => {
// Setup
delete (window as any).webkit;
// Apply polyfill
applyPolyfill();
// Test: Multiple undefined handlers should not throw
expect(() => {
(window as any).webkit.messageHandlers.handler1.postMessage("test1");
(window as any).webkit.messageHandlers.handler2.postMessage("test2");
(window as any).webkit.messageHandlers.handler3.postMessage("test3");
}).not.toThrow();
expect(consoleDebugSpy).toHaveBeenCalledTimes(3);
});
});
describe("Scenario 2: window.webkit exists but messageHandlers does not", () => {
it("should add proxied messageHandlers to existing webkit", () => {
// Setup: Create webkit without messageHandlers
(window as any).webkit = {};
// Apply polyfill
applyPolyfill();
// Test: messageHandlers should now exist
expect((window as any).webkit.messageHandlers).toBeDefined();
});
it("should not throw when accessing undefined handlers", () => {
// Setup
(window as any).webkit = {};
// Apply polyfill
applyPolyfill();
// Test
expect(() => {
(window as any).webkit.messageHandlers.someHandler.postMessage("test");
}).not.toThrow();
expect(consoleDebugSpy).toHaveBeenCalledWith(
'WebKit message handler "someHandler" is not available in this environment'
);
});
});
describe("Scenario 3: Both webkit and messageHandlers exist", () => {
it("should preserve existing handlers while proxying new ones", () => {
// Setup: Create a webkit object with a real handler
const mockPostMessage = vi.fn();
(window as any).webkit = {
messageHandlers: {
existingHandler: {
postMessage: mockPostMessage,
},
},
};
// Apply polyfill
applyPolyfill();
// Test: Existing handler should still work
(window as any).webkit.messageHandlers.existingHandler.postMessage("test message");
expect(mockPostMessage).toHaveBeenCalledWith("test message");
// Test: Undefined handler should not throw
expect(() => {
(window as any).webkit.messageHandlers.undefinedHandler.postMessage("test");
}).not.toThrow();
expect(consoleDebugSpy).toHaveBeenCalledWith(
'WebKit message handler "undefinedHandler" is not available in this environment'
);
});
it("should work with multiple existing handlers", () => {
// Setup
const mockPostMessage1 = vi.fn();
const mockPostMessage2 = vi.fn();
(window as any).webkit = {
messageHandlers: {
handler1: {
postMessage: mockPostMessage1,
},
handler2: {
postMessage: mockPostMessage2,
},
},
};
// Apply polyfill
applyPolyfill();
// Test: All existing handlers should work
(window as any).webkit.messageHandlers.handler1.postMessage("msg1");
(window as any).webkit.messageHandlers.handler2.postMessage("msg2");
expect(mockPostMessage1).toHaveBeenCalledWith("msg1");
expect(mockPostMessage2).toHaveBeenCalledWith("msg2");
// Test: Undefined handlers should not throw
expect(() => {
(window as any).webkit.messageHandlers.handler3.postMessage("msg3");
}).not.toThrow();
});
});
describe("Edge cases", () => {
it("should handle messageHandlers with empty object", () => {
// Setup
(window as any).webkit = {
messageHandlers: {},
};
// Apply polyfill
applyPolyfill();
// Test
expect(() => {
(window as any).webkit.messageHandlers.anyHandler.postMessage("test");
}).not.toThrow();
});
it("should handle messageHandlers with null prototype", () => {
// Setup
(window as any).webkit = {
messageHandlers: Object.create(null),
};
// Apply polyfill
applyPolyfill();
// Test
expect(() => {
(window as any).webkit.messageHandlers.handler.postMessage("test");
}).not.toThrow();
});
it("should not interfere with handler methods other than postMessage", () => {
// Setup
const customMethod = vi.fn();
(window as any).webkit = {
messageHandlers: {
customHandler: {
postMessage: vi.fn(),
customMethod,
},
},
};
// Apply polyfill
applyPolyfill();
// Test: Custom methods should still be accessible
(window as any).webkit.messageHandlers.customHandler.customMethod("test");
expect(customMethod).toHaveBeenCalledWith("test");
});
});
});