diff --git a/apps/web/src/pages/home-page.tsx b/apps/web/src/pages/home-page.tsx index dd1121d0..c070cbff 100644 --- a/apps/web/src/pages/home-page.tsx +++ b/apps/web/src/pages/home-page.tsx @@ -13,6 +13,8 @@ import { useSettingsStore } from "@/stores/settings-store"; // Tools shown prominently as "quick actions" at the top const QUICK_ACTION_IDS = ["resize", "compress", "convert", "remove-background"]; +let hasAppliedDefaultRedirect = false; + export function HomePage() { const { setFiles, @@ -37,7 +39,13 @@ export function HomePage() { }, [fetchSettings, fetchFeatures]); useEffect(() => { - if (settingsLoaded && defaultToolView === "fullscreen" && files.length === 0) { + if ( + !hasAppliedDefaultRedirect && + settingsLoaded && + defaultToolView === "fullscreen" && + files.length === 0 + ) { + hasAppliedDefaultRedirect = true; navigate("/fullscreen", { replace: true }); } }, [settingsLoaded, defaultToolView, files.length, navigate]); diff --git a/tests/unit/web/home-page-redirect.test.tsx b/tests/unit/web/home-page-redirect.test.tsx new file mode 100644 index 00000000..3992340c --- /dev/null +++ b/tests/unit/web/home-page-redirect.test.tsx @@ -0,0 +1,158 @@ +// @vitest-environment jsdom +import type React from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +let settingsState = { + fetch: vi.fn(), + defaultToolView: "sidebar" as "sidebar" | "fullscreen", + loaded: false, +}; + +vi.mock("@/stores/settings-store", () => ({ + useSettingsStore: () => settingsState, +})); + +let fileState = { + setFiles: vi.fn(), + files: [] as File[], + reset: vi.fn(), + originalBlobUrl: null as string | null, + selectedFileName: null as string | null, + selectedFileSize: 0, + currentEntry: null, +}; + +vi.mock("@/stores/file-store", () => ({ + useFileStore: () => fileState, +})); + +vi.mock("@/stores/features-store", () => ({ + useFeaturesStore: () => ({ + fetch: vi.fn(), + bundles: [], + installing: {}, + queued: [], + }), +})); + +vi.mock("@/components/layout/app-layout", () => ({ + AppLayout: ({ children }: React.PropsWithChildren) => ( +
{children}
+ ), +})); + +vi.mock("@/components/common/image-viewer", () => ({ + ImageViewer: () => null, +})); + +vi.mock("@/components/common/multi-image-viewer", () => ({ + MultiImageViewer: () => null, +})); + +vi.mock("@/lib/icon-map", () => ({ + ICON_MAP: new Proxy( + {}, + { + get: () => (props: { className?: string }) => , + }, + ), +})); + +import { act, cleanup, render } from "@testing-library/react"; +import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; +import { HomePage } from "@/pages/home-page"; + +let currentPath = "/"; + +function LocationSpy() { + const location = useLocation(); + currentPath = location.pathname; + return null; +} + +function renderInRouter(initialPath = "/") { + currentPath = initialPath; + return render( + + + + } /> + } /> + + , + ); +} + +afterEach(cleanup); + +// Tests are ordered intentionally: the module-level `hasAppliedDefaultRedirect` +// flag starts false and is set true by the "redirects on first mount" test. +// Tests before it verify conditions that independently prevent the redirect +// (so the flag stays false). The final test verifies the flag prevents repeat redirects. +describe("HomePage default view redirect", () => { + beforeEach(() => { + settingsState = { + fetch: vi.fn(), + defaultToolView: "sidebar", + loaded: false, + }; + fileState = { + setFiles: vi.fn(), + files: [], + reset: vi.fn(), + originalBlobUrl: null, + selectedFileName: null, + selectedFileSize: 0, + currentEntry: null, + }; + }); + + it("does NOT redirect when settings have not loaded yet", () => { + settingsState.loaded = false; + settingsState.defaultToolView = "fullscreen"; + + renderInRouter("/"); + + expect(currentPath).toBe("/"); + }); + + it("does NOT redirect when default view is sidebar", () => { + settingsState.loaded = true; + settingsState.defaultToolView = "sidebar"; + + renderInRouter("/"); + + expect(currentPath).toBe("/"); + }); + + it("does NOT redirect when files are loaded even with fullscreen default", () => { + settingsState.loaded = true; + settingsState.defaultToolView = "fullscreen"; + fileState.files = [new File([new ArrayBuffer(1024)], "test.png", { type: "image/png" })]; + fileState.originalBlobUrl = "blob:fake-url"; + fileState.selectedFileName = "test.png"; + fileState.selectedFileSize = 1024; + + renderInRouter("/"); + + expect(currentPath).toBe("/"); + }); + + it("redirects to /fullscreen on first mount when default view is fullscreen", () => { + settingsState.loaded = true; + settingsState.defaultToolView = "fullscreen"; + + renderInRouter("/"); + + expect(currentPath).toBe("/fullscreen"); + }); + + it("does NOT redirect on subsequent mounts after initial redirect (issue #128)", () => { + settingsState.loaded = true; + settingsState.defaultToolView = "fullscreen"; + + renderInRouter("/"); + + expect(currentPath).toBe("/"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 46947e4c..a9c221aa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -108,6 +108,7 @@ export default defineConfig({ sharp: path.join(apiNodeModules, "sharp"), react: path.join(webNodeModules, "react"), "react-dom": path.join(webNodeModules, "react-dom"), + "react-router-dom": path.join(webNodeModules, "react-router-dom"), zustand: path.join(webNodeModules, "zustand"), "posthog-js": path.join(webNodeModules, "posthog-js"), "@sentry/react": path.join(webNodeModules, "@sentry/react"),