diff --git a/packages/ui/src/features/command-center/components/CommandCenterGrid.tsx b/packages/ui/src/features/command-center/components/CommandCenterGrid.tsx index 32254d828e..22dc13ec35 100644 --- a/packages/ui/src/features/command-center/components/CommandCenterGrid.tsx +++ b/packages/ui/src/features/command-center/components/CommandCenterGrid.tsx @@ -1,4 +1,5 @@ import { destroyShellTerminal } from "@posthog/ui/features/terminal/destroyShellTerminal"; +import { ErrorBoundary } from "@posthog/ui/shell/ErrorBoundary"; import { useCallback, useEffect, useRef, useState } from "react"; import { FOCUSABLE_SELECTOR } from "../../../utils/overlay"; import { @@ -132,7 +133,12 @@ function GridCell({ zoom: zoom !== 1 ? zoom : undefined, }} > - + + + {isActive && (
diff --git a/packages/ui/src/primitives/action-selector/useActionSelectorState.test.ts b/packages/ui/src/primitives/action-selector/useActionSelectorState.test.ts new file mode 100644 index 0000000000..48f70d9a11 --- /dev/null +++ b/packages/ui/src/primitives/action-selector/useActionSelectorState.test.ts @@ -0,0 +1,62 @@ +import { act, renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { SelectorOption } from "./types"; +import { useActionSelectorState } from "./useActionSelectorState"; + +function makeProps( + overrides: Partial[0]> = {}, +) { + return { + options: [] as SelectorOption[], + multiSelect: false, + allowCustomInput: false, + hideSubmitButton: false, + currentStep: 0, + steps: undefined, + onSelect: vi.fn(), + onMultiSelect: vi.fn(), + onStepChange: vi.fn(), + onStepAnswer: vi.fn(), + ...overrides, + }; +} + +describe("useActionSelectorState.selectCurrent", () => { + it("does not throw when there are no options (empty allOptions)", () => { + const { result } = renderHook(() => useActionSelectorState(makeProps())); + + // Pressing Enter with an empty option list must be a no-op, not a crash. + expect(() => act(() => result.current.selectCurrent())).not.toThrow(); + }); + + it("does not throw when the highlighted index is stale after options shrink", () => { + const options: SelectorOption[] = [ + { id: "a", label: "A" }, + { id: "b", label: "B" }, + { id: "c", label: "C" }, + ]; + const { result, rerender } = renderHook( + (props: Parameters[0]) => + useActionSelectorState(props), + { initialProps: makeProps({ options }) }, + ); + + // Move the highlight to the last option, then remove every option. + act(() => result.current.moveUp()); + rerender(makeProps({ options: [] })); + + expect(() => act(() => result.current.selectCurrent())).not.toThrow(); + }); + + it("still selects the highlighted option when one exists", () => { + const onSelect = vi.fn(); + const options: SelectorOption[] = [{ id: "approve", label: "Approve" }]; + const { result } = renderHook(() => + useActionSelectorState(makeProps({ options, onSelect })), + ); + + act(() => result.current.selectCurrent()); + + expect(onSelect).toHaveBeenCalledWith("approve"); + }); +}); diff --git a/packages/ui/src/primitives/action-selector/useActionSelectorState.ts b/packages/ui/src/primitives/action-selector/useActionSelectorState.ts index 42c7a21239..d36573fd14 100644 --- a/packages/ui/src/primitives/action-selector/useActionSelectorState.ts +++ b/packages/ui/src/primitives/action-selector/useActionSelectorState.ts @@ -259,6 +259,7 @@ export function useActionSelectorState({ const selectCurrent = useCallback(() => { const selected = allOptions[selectedIndex]; + if (!selected) return; if (isSubmitOption(selected.id)) { if (!showSubmitButton) {