fix: prevent repeated redirect when switching from grid to sidebar view

The default-view redirect in HomePage fired on every mount, not just the
initial page load. A module-level flag now gates the redirect so it only
applies once per session, allowing users to switch to sidebar view when
grid is the default.

Closes #128
This commit is contained in:
SnapOtter
2026-05-07 22:16:26 +08:00
parent ff8a280b45
commit 14810c3c78
3 changed files with 168 additions and 1 deletions
+9 -1
View File
@@ -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]);
+158
View File
@@ -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) => (
<div data-testid="app-layout">{children}</div>
),
}));
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 }) => <span {...props} />,
},
),
}));
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(
<MemoryRouter initialEntries={[initialPath]}>
<LocationSpy />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/fullscreen" element={<div data-testid="fullscreen-page" />} />
</Routes>
</MemoryRouter>,
);
}
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("/");
});
});
+1
View File
@@ -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"),