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"),