mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-05 19:30:48 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64c640b181 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -645,6 +645,7 @@
|
||||
"contacts_table_refresh": "連絡先を更新",
|
||||
"contacts_table_refresh_success": "連絡先を正常に更新しました",
|
||||
"create_attribute": "属性を作成",
|
||||
"create_key": "キーを作成",
|
||||
"create_new_attribute": "新しい属性を作成",
|
||||
"create_new_attribute_description": "セグメンテーション用の新しい属性を作成します。",
|
||||
"custom_attributes": "カスタム属性",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -645,6 +645,7 @@
|
||||
"contacts_table_refresh": "Обновить контакты",
|
||||
"contacts_table_refresh_success": "Контакты успешно обновлены",
|
||||
"create_attribute": "Создать атрибут",
|
||||
"create_key": "Создать ключ",
|
||||
"create_new_attribute": "Создать новый атрибут",
|
||||
"create_new_attribute_description": "Создайте новый атрибут для целей сегментации.",
|
||||
"custom_attributes": "Пользовательские атрибуты",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -645,6 +645,7 @@
|
||||
"contacts_table_refresh": "刷新 联系人",
|
||||
"contacts_table_refresh_success": "联系人 已成功刷新",
|
||||
"create_attribute": "创建属性",
|
||||
"create_key": "创建键",
|
||||
"create_new_attribute": "创建新属性",
|
||||
"create_new_attribute_description": "为细分目的创建新属性。",
|
||||
"custom_attributes": "自定义属性",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user