fix(ui): preserve panel actions when session storage is unavailable

This commit is contained in:
Peter Steinberger
2026-08-21 18:07:13 -07:00
parent f6b42ea223
commit e63bbd805f
3 changed files with 94 additions and 2 deletions
+40
View File
@@ -14,6 +14,16 @@ import { ShellChromeOwner, type ShellChromeHost } from "./app-shell-chrome.ts";
import type { ApplicationContext } from "./context.ts";
import type { LazyCustomElementRequestController } from "./lazy-custom-element.ts";
import { persistLazyShellAction, readLazyShellAction } from "./lazy-shell-action.ts";
import { retryStaleChunkReloadWhenReachable } from "./stale-chunk-reload.ts";
vi.mock("./stale-chunk-reload.ts", async () => {
const actual =
await vi.importActual<typeof import("./stale-chunk-reload.ts")>("./stale-chunk-reload.ts");
return {
...actual,
retryStaleChunkReloadWhenReachable: vi.fn(async () => true),
};
});
type ShellPanelToggleState = {
lazyCustomElements: LazyCustomElementRequestController;
@@ -56,6 +66,7 @@ function configureTerminalShell(terminalElement: TestOptionalCustomElement): She
afterEach(() => {
window.history.replaceState(null, "", "/");
resetAppHostTestGlobals();
vi.clearAllMocks();
});
describe("OpenClaw shell panel toggles", () => {
@@ -102,6 +113,35 @@ describe("OpenClaw shell panel toggles", () => {
expect(delivered.detail).toEqual(event.detail);
});
it("retries a stale panel in place when its action cannot survive a document reload", async () => {
vi.stubGlobal("sessionStorage", undefined);
const error = new Error("Failed to fetch dynamically imported module: terminal.js");
const terminalElement = createLazyElementSpec("storage-disabled terminal panel", {
firstError: error,
});
const terminalToggle = vi.fn();
const shell = configureTerminalShell(terminalElement);
const owner = chromeOwner(shell);
const event = new CustomEvent(TERMINAL_PANEL_TOGGLE_EVENT, {
detail: { dock: "right", open: true },
});
window.addEventListener(TERMINAL_PANEL_TOGGLE_EVENT, terminalToggle);
try {
owner.handleDeferredTerminalToggle(event);
await vi.waitFor(() => expect(shell.lazyCustomElements.visibleState?.status).toBe("error"));
shell.lazyCustomElements.retry();
expect(retryStaleChunkReloadWhenReachable).not.toHaveBeenCalled();
await vi.waitFor(() => expect(terminalToggle).toHaveBeenCalledOnce());
const delivered = terminalToggle.mock.calls[0]?.[0] as CustomEvent;
expect(delivered.detail).toEqual(event.detail);
} finally {
window.removeEventListener(TERMINAL_PANEL_TOGGLE_EVENT, terminalToggle);
}
});
it("restores a structured panel event once in a replacement shell", async () => {
vi.stubGlobal("sessionStorage", createStorageMock());
const terminalElement = createLazyElementSpec("restored terminal");
+53 -1
View File
@@ -12,7 +12,7 @@ import {
} from "./app-host.test-support.ts";
import "./app-host.ts";
import { DEBUG_OVERLAY_ELEMENT } from "./lazy-custom-element.ts";
import { readLazyShellAction } from "./lazy-shell-action.ts";
import { hasStoredLazyShellAction, readLazyShellAction } from "./lazy-shell-action.ts";
const storageKey = "openclaw:lazy-event";
@@ -33,6 +33,58 @@ async function withConnectedShell(shell: ShellLifecycle, run: () => void | Promi
afterEach(resetAppHostTestGlobals);
describe("lazy shell action storage", () => {
it("does not report a stored action when session storage is unavailable", () => {
vi.stubGlobal("sessionStorage", undefined);
expect(hasStoredLazyShellAction()).toBe(false);
});
it("does not report a stored action when browser storage access is denied", () => {
const originalVitest = process.env.VITEST;
const originalStorage = Object.getOwnPropertyDescriptor(globalThis, "sessionStorage");
process.env.VITEST = "";
Object.defineProperty(globalThis, "sessionStorage", {
configurable: true,
get() {
throw new DOMException("Access is denied", "SecurityError");
},
});
try {
expect(hasStoredLazyShellAction()).toBe(false);
} finally {
if (originalVitest === undefined) {
Reflect.deleteProperty(process.env, "VITEST");
} else {
process.env.VITEST = originalVitest;
}
if (originalStorage) {
Object.defineProperty(globalThis, "sessionStorage", originalStorage);
} else {
Reflect.deleteProperty(globalThis, "sessionStorage");
}
}
});
it("does not report a stored action when session storage rejects reads", () => {
const storage = createStorageMock();
storage.getItem = () => {
throw new DOMException("Access is denied", "SecurityError");
};
vi.stubGlobal("sessionStorage", storage);
expect(hasStoredLazyShellAction()).toBe(false);
});
it("reports only actions actually present in available session storage", () => {
const storage = createStorageMock();
vi.stubGlobal("sessionStorage", storage);
expect(hasStoredLazyShellAction()).toBe(false);
storage.setItem(storageKey, JSON.stringify({ eventType: COMMAND_PALETTE_OPEN_EVENT }));
expect(hasStoredLazyShellAction()).toBe(true);
});
it.each([
"{",
JSON.stringify({ eventType: COMMAND_PALETTE_OPEN_EVENT, extra: true }),
+1 -1
View File
@@ -44,7 +44,7 @@ export function lazyShellEvent(
export function hasStoredLazyShellAction(): boolean {
try {
return getSafeSessionStorage()?.getItem(STORAGE_KEY) !== null;
return getSafeSessionStorage()?.getItem(STORAGE_KEY) != null;
} catch {
return false;
}