FN-5817: prevent blank dashboard after auto-merge toggle
Ensure viewport mode updates stay in sync so toggling auto-merge no longer blanks the dashboard. - update useViewportMode to initialize safely and react consistently to viewport/media-query changes - add focused hook coverage for initial mode detection and transition behavior - add dashboard/app regression tests to confirm the board remains rendered after auto-merge toggles - add a changeset for @runfusion/fusion patch release Files changed: .changeset/fn-5817-auto-merge-toggle-mobile.md | 5 ++ .../app/components/__tests__/App.test.tsx | 66 ++++++++++++++++++++++ .../__tests__/board-mobile-initial-render.test.tsx | 30 ++++++++++ .../app/hooks/__tests__/useViewportMode.test.ts | 34 +++++++++++ packages/dashboard/app/hooks/useViewportMode.ts | 28 +++++++-- 5 files changed, 159 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-5817 Fusion-Task-Lineage: 2fc044b1-5f37-44de-b70e-c60364a3d69c
This commit is contained in:
5
.changeset/fn-5817-auto-merge-toggle-mobile.md
Normal file
5
.changeset/fn-5817-auto-merge-toggle-mobile.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes a mobile dashboard crash path where toggling the in-review auto-merge switch could blank the UI until refresh on some Android/legacy WebView environments.
|
||||||
@@ -4127,6 +4127,72 @@ describe("App board branch filters", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("FN-5817 mobile auto-merge toggle stability", () => {
|
||||||
|
it("keeps app shell mounted when toggling auto-merge on mobile", async () => {
|
||||||
|
mockUseViewportMode.mockReturnValue("mobile");
|
||||||
|
|
||||||
|
const updateSettingsSpy = vi.mocked(updateSettings);
|
||||||
|
updateSettingsSpy.mockResolvedValue({ ...defaultSettings, autoMerge: false });
|
||||||
|
|
||||||
|
mockUseTasks.mockImplementation(() => ({
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: "FN-5817",
|
||||||
|
title: "In review task",
|
||||||
|
description: "Regression task",
|
||||||
|
column: "in-review",
|
||||||
|
status: "in-review",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
createdAt: "",
|
||||||
|
updatedAt: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isStale: false,
|
||||||
|
createTask: mockCreateTask,
|
||||||
|
moveTask: vi.fn(),
|
||||||
|
pauseTask: vi.fn(),
|
||||||
|
unpauseTask: vi.fn(),
|
||||||
|
deleteTask: vi.fn(),
|
||||||
|
mergeTask: vi.fn(),
|
||||||
|
retryTask: vi.fn(),
|
||||||
|
resetTask: vi.fn(),
|
||||||
|
updateTask: vi.fn(),
|
||||||
|
duplicateTask: vi.fn(),
|
||||||
|
archiveTask: vi.fn(),
|
||||||
|
unarchiveTask: vi.fn(),
|
||||||
|
archiveAllDone: vi.fn(),
|
||||||
|
loadArchivedTasks: vi.fn(),
|
||||||
|
refreshTasks: vi.fn(),
|
||||||
|
ingestCreatedTasks: vi.fn(),
|
||||||
|
lastFetchTimeMs: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
const toggle = await screen.findByRole("checkbox", { name: "Auto-merge" });
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateSettingsSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ autoMerge: expect.any(Boolean) }),
|
||||||
|
DEFAULT_PROJECT_ID,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("checkbox", { name: "Auto-merge" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("App shell connection status plumbing", () => {
|
describe("App shell connection status plumbing", () => {
|
||||||
it("loads shell connection status for native shell host", async () => {
|
it("loads shell connection status for native shell host", async () => {
|
||||||
mockShellHostContextValue.host = { kind: "desktop-shell", mode: "remote", connectionId: "p1", serverUrl: "https://fusion.example.com" };
|
mockShellHostContextValue.host = { kind: "desktop-shell", mode: "remote", connectionId: "p1", serverUrl: "https://fusion.example.com" };
|
||||||
|
|||||||
@@ -141,6 +141,36 @@ describe("Board mobile initial render stabilization (FN-4574)", () => {
|
|||||||
viewportSpy.mockRestore();
|
viewportSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not throw when visualViewport resize listener lacks removeEventListener (Android seam)", () => {
|
||||||
|
const viewportSpy = mockViewport(375);
|
||||||
|
const visualViewportResizeListeners: Array<() => void> = [];
|
||||||
|
|
||||||
|
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
||||||
|
setTimeout(() => cb(0), 0);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
||||||
|
Object.defineProperty(window, "visualViewport", {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
scale: 1,
|
||||||
|
addEventListener: (_event: string, listener: () => void) => {
|
||||||
|
visualViewportResizeListeners.push(listener);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Board {...boardProps} />);
|
||||||
|
|
||||||
|
expect(visualViewportResizeListeners).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
visualViewportResizeListeners[0]();
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
viewportSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it("is a desktop no-op and does not force pageshow re-anchor", () => {
|
it("is a desktop no-op and does not force pageshow re-anchor", () => {
|
||||||
const viewportSpy = mockViewport(1280);
|
const viewportSpy = mockViewport(1280);
|
||||||
const addEventListenerSpy = vi.spyOn(window, "addEventListener");
|
const addEventListenerSpy = vi.spyOn(window, "addEventListener");
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { renderHook } from "@testing-library/react";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { useViewportMode } from "../useViewportMode";
|
||||||
|
|
||||||
|
describe("useViewportMode", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports legacy MediaQueryList listeners without runtime errors", () => {
|
||||||
|
const listeners: Array<() => void> = [];
|
||||||
|
const removeListener = vi.fn((listener: () => void) => {
|
||||||
|
const index = listeners.indexOf(listener);
|
||||||
|
if (index >= 0) listeners.splice(index, 1);
|
||||||
|
});
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
vi.stubGlobal(
|
||||||
|
"matchMedia",
|
||||||
|
vi.fn((query: string) => ({
|
||||||
|
matches: query === "(max-width: 768px)",
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: (listener: () => void) => listeners.push(listener),
|
||||||
|
removeListener,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderHook(() => useViewportMode());
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,11 +34,31 @@ export function useViewportMode(): ViewportMode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mobileQuery.addEventListener("change", updateMode);
|
const addChangeListener = (query: MediaQueryList, listener: () => void) => {
|
||||||
tabletQuery.addEventListener("change", updateMode);
|
if (typeof query.addEventListener === "function") {
|
||||||
|
query.addEventListener("change", listener);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof query.addListener === "function") {
|
||||||
|
query.addListener(listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeChangeListener = (query: MediaQueryList, listener: () => void) => {
|
||||||
|
if (typeof query.removeEventListener === "function") {
|
||||||
|
query.removeEventListener("change", listener);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof query.removeListener === "function") {
|
||||||
|
query.removeListener(listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addChangeListener(mobileQuery, updateMode);
|
||||||
|
addChangeListener(tabletQuery, updateMode);
|
||||||
return () => {
|
return () => {
|
||||||
mobileQuery.removeEventListener("change", updateMode);
|
removeChangeListener(mobileQuery, updateMode);
|
||||||
tabletQuery.removeEventListener("change", updateMode);
|
removeChangeListener(tabletQuery, updateMode);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user