mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 21:50:39 -06:00
Compare commits
1 Commits
docs/link-
...
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 { isSurveyValid } from "@/modules/survey/editor/lib/validation";
|
||||
import { Project } from "@prisma/client";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
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})`),
|
||||
}));
|
||||
|
||||
// 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", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
@@ -88,15 +106,43 @@ vi.mock("lucide-react", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock next/navigation
|
||||
const mockRouter = {
|
||||
back: vi.fn(),
|
||||
push: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => mockRouter,
|
||||
}));
|
||||
|
||||
const mockSetLocalSurvey = vi.fn();
|
||||
const mockSetActiveId = vi.fn();
|
||||
const mockSetInvalidQuestions = 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 = {
|
||||
id: "survey-1",
|
||||
createdAt: new Date(),
|
||||
@@ -163,6 +209,10 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
describe("SurveyMenuBar", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(updateSurveyAction).mockResolvedValue({ data: { ...baseSurvey, updatedAt: new Date() } }); // Mock successful update
|
||||
vi.mocked(isSurveyValid).mockReturnValue(true);
|
||||
@@ -171,10 +221,9 @@ describe("SurveyMenuBar", () => {
|
||||
} as any);
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mockHistoryPushState.mockClear();
|
||||
mockAddEventListener.mockClear();
|
||||
mockRemoveEventListener.mockClear();
|
||||
});
|
||||
|
||||
test("renders correctly with default props", () => {
|
||||
@@ -186,6 +235,133 @@ describe("SurveyMenuBar", () => {
|
||||
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 () => {
|
||||
render(<SurveyMenuBar {...defaultProps} />);
|
||||
const input = screen.getByTestId("survey-name-input");
|
||||
@@ -198,11 +374,49 @@ describe("SurveyMenuBar", () => {
|
||||
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
|
||||
const backButton = screen.getByText("common.back").closest("button");
|
||||
await userEvent.click(backButton!);
|
||||
expect(mockRouter.back).not.toHaveBeenCalled();
|
||||
expect(mockRouter.push).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId("alert-dialog")).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", () => {
|
||||
render(<SurveyMenuBar {...defaultProps} responseCount={5} />);
|
||||
expect(screen.getByText("environments.surveys.edit.caution_text")).toBeInTheDocument();
|
||||
|
||||
@@ -91,6 +91,29 @@ export const SurveyMenuBar = ({
|
||||
};
|
||||
}, [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 = () => {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.removeItem(`${localSurvey.id}-columnOrder`);
|
||||
@@ -121,7 +144,7 @@ export const SurveyMenuBar = ({
|
||||
if (!isEqual(localSurveyRest, surveyRest)) {
|
||||
setConfirmDialogOpen(true);
|
||||
} else {
|
||||
router.back();
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -247,7 +270,6 @@ export const SurveyMenuBar = ({
|
||||
if (updatedSurveyResponse?.data) {
|
||||
setLocalSurvey(updatedSurveyResponse.data);
|
||||
toast.success(t("environments.surveys.edit.changes_saved"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
@@ -266,7 +288,8 @@ export const SurveyMenuBar = ({
|
||||
const handleSaveAndGoBack = async () => {
|
||||
const isSurveySaved = await handleSurveySave();
|
||||
if (isSurveySaved) {
|
||||
router.back();
|
||||
setConfirmDialogOpen(false);
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -395,7 +418,7 @@ export const SurveyMenuBar = ({
|
||||
declineBtnVariant="destructive"
|
||||
onDecline={() => {
|
||||
setConfirmDialogOpen(false);
|
||||
router.back();
|
||||
router.push("/");
|
||||
}}
|
||||
onConfirm={handleSaveAndGoBack}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user