mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-23 17:21:18 -05:00
Compare commits
3 Commits
fix/1206-r
...
cursor/doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50156d474c | ||
|
|
87b859d02a | ||
|
|
df7e768216 |
@@ -3,6 +3,7 @@ import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { RenderSurvey } from "@/components/general/render-survey";
|
||||
import { I18nProvider } from "@/components/i18n/provider";
|
||||
import { FILE_PICK_EVENT } from "@/lib/constants";
|
||||
import { ensureBodyExists } from "@/lib/dom-utils";
|
||||
import { getI18nLanguage } from "@/lib/i18n-utils";
|
||||
import { addCustomThemeToDom, addStylesToDom, setStyleNonce } from "@/lib/styles";
|
||||
|
||||
@@ -15,7 +16,7 @@ export const renderSurveyInline = (props: SurveyContainerProps) => {
|
||||
renderSurvey(inlineProps);
|
||||
};
|
||||
|
||||
export const renderSurvey = (props: SurveyContainerProps) => {
|
||||
export const renderSurvey = async (props: SurveyContainerProps) => {
|
||||
// render SurveyNew
|
||||
// if survey type is link, we don't pass the placement, overlay, clickOutside, onClose
|
||||
|
||||
@@ -66,6 +67,8 @@ export const renderSurvey = (props: SurveyContainerProps) => {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await ensureBodyExists();
|
||||
|
||||
const modalContainer = document.createElement("div");
|
||||
modalContainer.id = "formbricks-modal-container";
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
118
packages/surveys/src/lib/dom-utils.test.ts
Normal file
118
packages/surveys/src/lib/dom-utils.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ensureBodyExists } from "./dom-utils";
|
||||
|
||||
describe("ensureBodyExists", () => {
|
||||
beforeEach(() => {
|
||||
// Reset DOM
|
||||
document.head.innerHTML = "";
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("resolves immediately when body exists", async () => {
|
||||
const startTime = Date.now();
|
||||
await ensureBodyExists();
|
||||
const endTime = Date.now();
|
||||
|
||||
// Should resolve almost immediately (< 10ms) when body exists
|
||||
expect(endTime - startTime).toBeLessThan(10);
|
||||
});
|
||||
|
||||
test("waits for body to become available via requestAnimationFrame", async () => {
|
||||
const originalBody = document.body;
|
||||
let callCount = 0;
|
||||
|
||||
// Mock document.body to be null initially
|
||||
Object.defineProperty(document, "body", {
|
||||
get: () => {
|
||||
callCount++;
|
||||
// Return null for first 2 calls, then return actual body
|
||||
return callCount <= 2 ? null : originalBody;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await ensureBodyExists();
|
||||
|
||||
// Should have checked multiple times
|
||||
expect(callCount).toBeGreaterThan(2);
|
||||
|
||||
// Restore original body
|
||||
Object.defineProperty(document, "body", {
|
||||
get: () => originalBody,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("waits for DOMContentLoaded when readyState is loading", async () => {
|
||||
const originalBody = document.body;
|
||||
const originalReadyState = document.readyState;
|
||||
|
||||
// Mock readyState to be loading
|
||||
Object.defineProperty(document, "readyState", {
|
||||
get: () => "loading",
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Mock document.body to be null initially
|
||||
Object.defineProperty(document, "body", {
|
||||
get: () => null,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const promise = ensureBodyExists();
|
||||
|
||||
// Simulate DOMContentLoaded event
|
||||
setTimeout(() => {
|
||||
// Restore body before triggering event
|
||||
Object.defineProperty(document, "body", {
|
||||
get: () => originalBody,
|
||||
configurable: true,
|
||||
});
|
||||
document.dispatchEvent(new Event("DOMContentLoaded"));
|
||||
}, 10);
|
||||
|
||||
await promise;
|
||||
|
||||
// Should have resolved after DOMContentLoaded
|
||||
expect(true).toBe(true);
|
||||
|
||||
// Restore original readyState
|
||||
Object.defineProperty(document, "readyState", {
|
||||
get: () => originalReadyState,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("handles edge case where body appears between checks", async () => {
|
||||
const originalBody = document.body;
|
||||
|
||||
// Mock body to appear after first check
|
||||
let firstCheck = true;
|
||||
Object.defineProperty(document, "body", {
|
||||
get: () => {
|
||||
if (firstCheck) {
|
||||
firstCheck = false;
|
||||
return null;
|
||||
}
|
||||
return originalBody;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await ensureBodyExists();
|
||||
|
||||
// Should have resolved successfully
|
||||
expect(firstCheck).toBe(false);
|
||||
|
||||
// Restore original body
|
||||
Object.defineProperty(document, "body", {
|
||||
get: () => originalBody,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
25
packages/surveys/src/lib/dom-utils.ts
Normal file
25
packages/surveys/src/lib/dom-utils.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Ensures document.body is available before proceeding
|
||||
* Returns a promise that resolves when document.body exists
|
||||
*/
|
||||
export const ensureBodyExists = (): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (document.body) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => resolve(), { once: true });
|
||||
} else {
|
||||
const checkBody = () => {
|
||||
if (document.body) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkBody);
|
||||
}
|
||||
};
|
||||
checkBody();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -109,6 +109,28 @@ describe("setStyleNonce and getStyleNonce", () => {
|
||||
expect(getStyleNonce()).toBe(nonce);
|
||||
// Should not throw and should store the nonce for future use
|
||||
});
|
||||
|
||||
test("should not throw error when document.getElementById is not available", () => {
|
||||
const originalGetElementById = document.getElementById;
|
||||
|
||||
// Mock document.getElementById to be undefined
|
||||
Object.defineProperty(document, "getElementById", {
|
||||
get: () => undefined,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const nonce = "test-nonce-safe";
|
||||
|
||||
// Should not throw error
|
||||
expect(() => setStyleNonce(nonce)).not.toThrow();
|
||||
expect(getStyleNonce()).toBe(nonce);
|
||||
|
||||
// Restore original getElementById
|
||||
Object.defineProperty(document, "getElementById", {
|
||||
get: () => originalGetElementById,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("addStylesToDom", () => {
|
||||
@@ -206,6 +228,27 @@ describe("addStylesToDom", () => {
|
||||
// setStyleNonce directly updates the nonce attribute
|
||||
expect(existingElement.getAttribute("nonce")).toBe(newNonce);
|
||||
});
|
||||
|
||||
test("should not throw error when document.head is null", () => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const originalHead = document.head;
|
||||
|
||||
// Mock document.head to be null
|
||||
Object.defineProperty(document, "head", {
|
||||
get: () => null,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Should not throw error
|
||||
expect(() => addStylesToDom()).not.toThrow();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith("addStylesToDom: document.head is not available yet");
|
||||
|
||||
// Restore original head
|
||||
Object.defineProperty(document, "head", {
|
||||
get: () => originalHead,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("addCustomThemeToDom", () => {
|
||||
@@ -606,6 +649,29 @@ describe("addCustomThemeToDom", () => {
|
||||
// setStyleNonce directly updates the nonce attribute
|
||||
expect(existingElement.getAttribute("nonce")).toBe(newNonce);
|
||||
});
|
||||
|
||||
test("should not throw error when document.head is null", () => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const originalHead = document.head;
|
||||
|
||||
// Mock document.head to be null
|
||||
Object.defineProperty(document, "head", {
|
||||
get: () => null,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const styling = getBaseProjectStyling({ brandColor: { light: "#FF0000" } });
|
||||
|
||||
// Should not throw error
|
||||
expect(() => addCustomThemeToDom({ styling })).not.toThrow();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith("addCustomThemeToDom: document.head is not available yet");
|
||||
|
||||
// Restore original head
|
||||
Object.defineProperty(document, "head", {
|
||||
get: () => originalHead,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBaseProjectStyling_Helper", () => {
|
||||
|
||||
@@ -17,6 +17,10 @@ let styleNonce: string | undefined;
|
||||
export const setStyleNonce = (nonce: string | undefined): void => {
|
||||
styleNonce = nonce;
|
||||
|
||||
if (!document?.getElementById) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing style elements if they exist
|
||||
const existingStyleElement = document.getElementById("formbricks__css");
|
||||
if (existingStyleElement && nonce) {
|
||||
@@ -34,6 +38,11 @@ export const getStyleNonce = (): string | undefined => {
|
||||
};
|
||||
|
||||
export const addStylesToDom = () => {
|
||||
if (!document.head) {
|
||||
console.warn("addStylesToDom: document.head is not available yet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.getElementById("formbricks__css") === null) {
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.id = "formbricks__css";
|
||||
@@ -56,6 +65,11 @@ export const addStylesToDom = () => {
|
||||
};
|
||||
|
||||
export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TSurveyStyling }): void => {
|
||||
if (!document.head) {
|
||||
console.warn("addCustomThemeToDom: document.head is not available yet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the style element already exists
|
||||
let styleElement = document.getElementById("formbricks__css__custom") as HTMLStyleElement | null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user