mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-01 19:30:29 -06:00
Compare commits
1 Commits
fix/attrib
...
fix-back-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e92e51b030 |
@@ -3,6 +3,7 @@ import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
|||||||
import { SurveyMenuBar } from "@/modules/survey/editor/components/survey-menu-bar";
|
import { SurveyMenuBar } from "@/modules/survey/editor/components/survey-menu-bar";
|
||||||
import { isSurveyValid } from "@/modules/survey/editor/lib/validation";
|
import { isSurveyValid } from "@/modules/survey/editor/lib/validation";
|
||||||
import { Project } from "@prisma/client";
|
import { Project } from "@prisma/client";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
@@ -71,6 +72,23 @@ vi.mock("@formbricks/i18n-utils/src/utils", () => ({
|
|||||||
getLanguageLabel: vi.fn((code) => `Lang(${code})`),
|
getLanguageLabel: vi.fn((code) => `Lang(${code})`),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock Zod schemas to always validate successfully
|
||||||
|
vi.mock("@formbricks/types/surveys/types", async () => {
|
||||||
|
const actual = await vi.importActual("@formbricks/types/surveys/types");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
ZSurvey: {
|
||||||
|
safeParse: vi.fn(() => ({ success: true })),
|
||||||
|
},
|
||||||
|
ZSurveyEndScreenCard: {
|
||||||
|
parse: vi.fn((ending) => ending),
|
||||||
|
},
|
||||||
|
ZSurveyRedirectUrlCard: {
|
||||||
|
parse: vi.fn((ending) => ending),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("react-hot-toast", () => ({
|
vi.mock("react-hot-toast", () => ({
|
||||||
default: {
|
default: {
|
||||||
success: vi.fn(),
|
success: vi.fn(),
|
||||||
@@ -88,15 +106,43 @@ vi.mock("lucide-react", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock next/navigation
|
||||||
const mockRouter = {
|
const mockRouter = {
|
||||||
back: vi.fn(),
|
back: vi.fn(),
|
||||||
push: vi.fn(),
|
push: vi.fn(),
|
||||||
|
refresh: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
vi.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => mockRouter,
|
||||||
|
}));
|
||||||
|
|
||||||
const mockSetLocalSurvey = vi.fn();
|
const mockSetLocalSurvey = vi.fn();
|
||||||
const mockSetActiveId = vi.fn();
|
const mockSetActiveId = vi.fn();
|
||||||
const mockSetInvalidQuestions = vi.fn();
|
const mockSetInvalidQuestions = vi.fn();
|
||||||
const mockSetIsCautionDialogOpen = vi.fn();
|
const mockSetIsCautionDialogOpen = vi.fn();
|
||||||
|
|
||||||
|
// Mock window.history
|
||||||
|
const mockHistoryPushState = vi.fn();
|
||||||
|
Object.defineProperty(window, "history", {
|
||||||
|
value: {
|
||||||
|
pushState: mockHistoryPushState,
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock window event listeners
|
||||||
|
const mockAddEventListener = vi.fn();
|
||||||
|
const mockRemoveEventListener = vi.fn();
|
||||||
|
Object.defineProperty(window, "addEventListener", {
|
||||||
|
value: mockAddEventListener,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(window, "removeEventListener", {
|
||||||
|
value: mockRemoveEventListener,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
const baseSurvey = {
|
const baseSurvey = {
|
||||||
id: "survey-1",
|
id: "survey-1",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -163,6 +209,10 @@ const defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("SurveyMenuBar", () => {
|
describe("SurveyMenuBar", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(updateSurveyAction).mockResolvedValue({ data: { ...baseSurvey, updatedAt: new Date() } }); // Mock successful update
|
vi.mocked(updateSurveyAction).mockResolvedValue({ data: { ...baseSurvey, updatedAt: new Date() } }); // Mock successful update
|
||||||
vi.mocked(isSurveyValid).mockReturnValue(true);
|
vi.mocked(isSurveyValid).mockReturnValue(true);
|
||||||
@@ -171,10 +221,9 @@ describe("SurveyMenuBar", () => {
|
|||||||
} as any);
|
} as any);
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
mockHistoryPushState.mockClear();
|
||||||
|
mockAddEventListener.mockClear();
|
||||||
afterEach(() => {
|
mockRemoveEventListener.mockClear();
|
||||||
cleanup();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders correctly with default props", () => {
|
test("renders correctly with default props", () => {
|
||||||
@@ -186,6 +235,133 @@ describe("SurveyMenuBar", () => {
|
|||||||
expect(screen.getByText("environments.surveys.edit.publish")).toBeInTheDocument();
|
expect(screen.getByText("environments.surveys.edit.publish")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("sets up browser history state and event listeners on mount", () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Check that history state is pushed with inSurveyEditor flag
|
||||||
|
expect(mockHistoryPushState).toHaveBeenCalledWith({ inSurveyEditor: true }, "");
|
||||||
|
|
||||||
|
// Check that event listeners are added
|
||||||
|
expect(mockAddEventListener).toHaveBeenCalledWith("popstate", expect.any(Function));
|
||||||
|
expect(mockAddEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removes event listeners on unmount", () => {
|
||||||
|
const { unmount } = render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Clear the mock to focus on cleanup calls
|
||||||
|
mockRemoveEventListener.mockClear();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
// Check that event listeners are removed
|
||||||
|
expect(mockRemoveEventListener).toHaveBeenCalledWith("popstate", expect.any(Function));
|
||||||
|
expect(mockRemoveEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles popstate event by calling handleBack", () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Get the popstate handler from the addEventListener call
|
||||||
|
const popstateHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "popstate")?.[1];
|
||||||
|
|
||||||
|
expect(popstateHandler).toBeDefined();
|
||||||
|
|
||||||
|
// Simulate popstate event
|
||||||
|
popstateHandler?.(new PopStateEvent("popstate"));
|
||||||
|
|
||||||
|
// Should navigate to home since surveys are equal (no changes)
|
||||||
|
expect(mockRouter.push).toHaveBeenCalledWith("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles keyboard shortcut (Alt + ArrowLeft) by calling handleBack", () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Get the keydown handler from the addEventListener call
|
||||||
|
const keydownHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "keydown")?.[1];
|
||||||
|
|
||||||
|
expect(keydownHandler).toBeDefined();
|
||||||
|
|
||||||
|
// Simulate Alt + ArrowLeft keydown event
|
||||||
|
const keyEvent = new KeyboardEvent("keydown", {
|
||||||
|
altKey: true,
|
||||||
|
key: "ArrowLeft",
|
||||||
|
});
|
||||||
|
|
||||||
|
keydownHandler?.(keyEvent);
|
||||||
|
|
||||||
|
// Should navigate to home since surveys are equal (no changes)
|
||||||
|
expect(mockRouter.push).toHaveBeenCalledWith("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles keyboard shortcut (Cmd + ArrowLeft) by calling handleBack", () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Get the keydown handler from the addEventListener call
|
||||||
|
const keydownHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "keydown")?.[1];
|
||||||
|
|
||||||
|
expect(keydownHandler).toBeDefined();
|
||||||
|
|
||||||
|
// Simulate Cmd + ArrowLeft keydown event
|
||||||
|
const keyEvent = new KeyboardEvent("keydown", {
|
||||||
|
metaKey: true,
|
||||||
|
key: "ArrowLeft",
|
||||||
|
});
|
||||||
|
|
||||||
|
keydownHandler?.(keyEvent);
|
||||||
|
|
||||||
|
// Should navigate to home since surveys are equal (no changes)
|
||||||
|
expect(mockRouter.push).toHaveBeenCalledWith("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores keyboard events without proper modifier keys", () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Get the keydown handler from the addEventListener call
|
||||||
|
const keydownHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "keydown")?.[1];
|
||||||
|
|
||||||
|
expect(keydownHandler).toBeDefined();
|
||||||
|
|
||||||
|
// Simulate ArrowLeft without modifier keys
|
||||||
|
const keyEvent = new KeyboardEvent("keydown", {
|
||||||
|
key: "ArrowLeft",
|
||||||
|
});
|
||||||
|
|
||||||
|
keydownHandler?.(keyEvent);
|
||||||
|
|
||||||
|
// Should not navigate
|
||||||
|
expect(mockRouter.push).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores keyboard events with wrong key", () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
|
||||||
|
// Get the keydown handler from the addEventListener call
|
||||||
|
const keydownHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "keydown")?.[1];
|
||||||
|
|
||||||
|
expect(keydownHandler).toBeDefined();
|
||||||
|
|
||||||
|
// Simulate Alt + ArrowRight (wrong key)
|
||||||
|
const keyEvent = new KeyboardEvent("keydown", {
|
||||||
|
altKey: true,
|
||||||
|
key: "ArrowRight",
|
||||||
|
});
|
||||||
|
|
||||||
|
keydownHandler?.(keyEvent);
|
||||||
|
|
||||||
|
// Should not navigate
|
||||||
|
expect(mockRouter.push).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigates to home page instead of using router.back when handleBack is called with no changes", async () => {
|
||||||
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
|
const backButton = screen.getByText("common.back").closest("button");
|
||||||
|
await userEvent.click(backButton!);
|
||||||
|
|
||||||
|
expect(mockRouter.push).toHaveBeenCalledWith("/");
|
||||||
|
expect(mockRouter.back).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test("updates survey name on input change", async () => {
|
test("updates survey name on input change", async () => {
|
||||||
render(<SurveyMenuBar {...defaultProps} />);
|
render(<SurveyMenuBar {...defaultProps} />);
|
||||||
const input = screen.getByTestId("survey-name-input");
|
const input = screen.getByTestId("survey-name-input");
|
||||||
@@ -198,11 +374,49 @@ describe("SurveyMenuBar", () => {
|
|||||||
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
|
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
|
||||||
const backButton = screen.getByText("common.back").closest("button");
|
const backButton = screen.getByText("common.back").closest("button");
|
||||||
await userEvent.click(backButton!);
|
await userEvent.click(backButton!);
|
||||||
expect(mockRouter.back).not.toHaveBeenCalled();
|
expect(mockRouter.push).not.toHaveBeenCalled();
|
||||||
expect(screen.getByTestId("alert-dialog")).toBeInTheDocument();
|
expect(screen.getByTestId("alert-dialog")).toBeInTheDocument();
|
||||||
expect(screen.getByText("environments.surveys.edit.confirm_survey_changes")).toBeInTheDocument();
|
expect(screen.getByText("environments.surveys.edit.confirm_survey_changes")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("navigates to home page when declining unsaved changes in dialog", async () => {
|
||||||
|
const changedSurvey = { ...baseSurvey, name: "Changed Name" };
|
||||||
|
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
|
||||||
|
const backButton = screen.getByText("common.back").closest("button");
|
||||||
|
await userEvent.click(backButton!);
|
||||||
|
|
||||||
|
const declineButton = screen.getByText("common.discard");
|
||||||
|
await userEvent.click(declineButton);
|
||||||
|
|
||||||
|
expect(mockRouter.push).toHaveBeenCalledWith("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saves and navigates to home page when confirming unsaved changes in dialog", async () => {
|
||||||
|
const changedSurvey = { ...baseSurvey, name: "Changed Name" };
|
||||||
|
|
||||||
|
// Mock successful save response
|
||||||
|
vi.mocked(updateSurveyAction).mockResolvedValueOnce({
|
||||||
|
data: { ...changedSurvey, updatedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
|
||||||
|
const backButton = screen.getByText("common.back").closest("button");
|
||||||
|
await userEvent.click(backButton!);
|
||||||
|
|
||||||
|
// Get the save button specifically from within the alert dialog
|
||||||
|
const dialog = screen.getByTestId("alert-dialog");
|
||||||
|
const confirmButton = dialog.querySelector("button:first-of-type")!;
|
||||||
|
await userEvent.click(confirmButton);
|
||||||
|
|
||||||
|
// Wait for the async operation to complete
|
||||||
|
await vi.waitFor(
|
||||||
|
() => {
|
||||||
|
expect(mockRouter.push).toHaveBeenCalledWith("/");
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("shows caution alert when responseCount > 0", () => {
|
test("shows caution alert when responseCount > 0", () => {
|
||||||
render(<SurveyMenuBar {...defaultProps} responseCount={5} />);
|
render(<SurveyMenuBar {...defaultProps} responseCount={5} />);
|
||||||
expect(screen.getByText("environments.surveys.edit.caution_text")).toBeInTheDocument();
|
expect(screen.getByText("environments.surveys.edit.caution_text")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -91,6 +91,29 @@ export const SurveyMenuBar = ({
|
|||||||
};
|
};
|
||||||
}, [localSurvey, survey, t]);
|
}, [localSurvey, survey, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.history.pushState({ inSurveyEditor: true }, "");
|
||||||
|
|
||||||
|
const handlePopstate = (_: PopStateEvent) => {
|
||||||
|
handleBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const isBackShortcut = (e.altKey || e.metaKey) && e.key === "ArrowLeft";
|
||||||
|
if (isBackShortcut) {
|
||||||
|
handleBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", handlePopstate);
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("popstate", handlePopstate);
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clearSurveyLocalStorage = () => {
|
const clearSurveyLocalStorage = () => {
|
||||||
if (typeof localStorage !== "undefined") {
|
if (typeof localStorage !== "undefined") {
|
||||||
localStorage.removeItem(`${localSurvey.id}-columnOrder`);
|
localStorage.removeItem(`${localSurvey.id}-columnOrder`);
|
||||||
@@ -121,7 +144,7 @@ export const SurveyMenuBar = ({
|
|||||||
if (!isEqual(localSurveyRest, surveyRest)) {
|
if (!isEqual(localSurveyRest, surveyRest)) {
|
||||||
setConfirmDialogOpen(true);
|
setConfirmDialogOpen(true);
|
||||||
} else {
|
} else {
|
||||||
router.back();
|
router.push("/");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,7 +270,6 @@ export const SurveyMenuBar = ({
|
|||||||
if (updatedSurveyResponse?.data) {
|
if (updatedSurveyResponse?.data) {
|
||||||
setLocalSurvey(updatedSurveyResponse.data);
|
setLocalSurvey(updatedSurveyResponse.data);
|
||||||
toast.success(t("environments.surveys.edit.changes_saved"));
|
toast.success(t("environments.surveys.edit.changes_saved"));
|
||||||
router.refresh();
|
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
@@ -266,7 +288,8 @@ export const SurveyMenuBar = ({
|
|||||||
const handleSaveAndGoBack = async () => {
|
const handleSaveAndGoBack = async () => {
|
||||||
const isSurveySaved = await handleSurveySave();
|
const isSurveySaved = await handleSurveySave();
|
||||||
if (isSurveySaved) {
|
if (isSurveySaved) {
|
||||||
router.back();
|
setConfirmDialogOpen(false);
|
||||||
|
router.push("/");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -395,7 +418,7 @@ export const SurveyMenuBar = ({
|
|||||||
declineBtnVariant="destructive"
|
declineBtnVariant="destructive"
|
||||||
onDecline={() => {
|
onDecline={() => {
|
||||||
setConfirmDialogOpen(false);
|
setConfirmDialogOpen(false);
|
||||||
router.back();
|
router.push("/");
|
||||||
}}
|
}}
|
||||||
onConfirm={handleSaveAndGoBack}
|
onConfirm={handleSaveAndGoBack}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user