From 79e595f36e8059aded706fbaeb1f4b97c5705daf Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Mon, 20 Jul 2026 11:34:34 +0200 Subject: [PATCH] fix(ui): keep provider wizard footer callbacks fresh - Preserve latest footer action closures without redundant renders - Cover callback freshness and semantic accessibility assertions --- .../use-provider-wizard-controller.test.tsx | 46 +++++++++++++++---- .../hooks/use-provider-wizard-controller.ts | 12 ++++- .../wizard/provider-wizard-modal.test.tsx | 6 +-- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx index c733025ac7..9f97379264 100644 --- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx +++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.test.tsx @@ -274,6 +274,12 @@ describe("useProviderWizardController", () => { it("does not rerender when setting a semantically unchanged footer config", () => { // Given const onOpenChange = vi.fn(); + const firstOnBack = vi.fn(); + const firstOnSecondaryAction = vi.fn(); + const firstOnAction = vi.fn(); + const latestOnBack = vi.fn(); + const latestOnSecondaryAction = vi.fn(); + const latestOnAction = vi.fn(); let renderCount = 0; const { result } = renderHook(() => { renderCount += 1; @@ -287,34 +293,58 @@ describe("useProviderWizardController", () => { const firstFooterConfig = { showBack: true, backLabel: "Back", - onBack: vi.fn(), + onBack: firstOnBack, + showSecondaryAction: true, + secondaryActionLabel: "Cancel", + secondaryActionVariant: "outline" as const, + secondaryActionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, + onSecondaryAction: firstOnSecondaryAction, showAction: true, actionLabel: "Next", actionDisabled: false, actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, - onAction: vi.fn(), + onAction: firstOnAction, }; act(() => { result.current.setFooterConfig(firstFooterConfig); }); const renderCountAfterFirstUpdate = renderCount; - const footerConfigAfterFirstUpdate = result.current.resolvedFooterConfig; // When act(() => { result.current.setFooterConfig({ ...firstFooterConfig, - onBack: vi.fn(), - onAction: vi.fn(), + onBack: latestOnBack, + onSecondaryAction: latestOnSecondaryAction, + onAction: latestOnAction, }); + + result.current.resolvedFooterConfig.onBack?.(); + result.current.resolvedFooterConfig.onSecondaryAction?.(); + result.current.resolvedFooterConfig.onAction?.(); }); // Then expect(renderCount).toBe(renderCountAfterFirstUpdate); - expect(result.current.resolvedFooterConfig).toBe( - footerConfigAfterFirstUpdate, - ); + expect(result.current.resolvedFooterConfig).toMatchObject({ + showBack: true, + backLabel: "Back", + showSecondaryAction: true, + secondaryActionLabel: "Cancel", + secondaryActionVariant: "outline", + secondaryActionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, + showAction: true, + actionLabel: "Next", + actionDisabled: false, + actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, + }); + expect(firstOnBack).not.toHaveBeenCalled(); + expect(firstOnSecondaryAction).not.toHaveBeenCalled(); + expect(firstOnAction).not.toHaveBeenCalled(); + expect(latestOnBack).toHaveBeenCalledTimes(1); + expect(latestOnSecondaryAction).toHaveBeenCalledTimes(1); + expect(latestOnAction).toHaveBeenCalledTimes(1); }); it("does not override launch footer config in the controller", () => { diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts index 29381b146d..32d67de9ee 100644 --- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts +++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts @@ -117,6 +117,11 @@ export function useProviderWizardController({ const [footerConfig, setFooterConfigState] = useState(EMPTY_FOOTER_CONFIG); const footerConfigRef = useRef(EMPTY_FOOTER_CONFIG); + const footerActionCallbacksRef = useRef({ + onBack: () => footerConfigRef.current.onBack?.(), + onSecondaryAction: () => footerConfigRef.current.onSecondaryAction?.(), + onAction: () => footerConfigRef.current.onAction?.(), + }); const [providerTypeHint, setProviderTypeHint] = useState( null, ); @@ -268,13 +273,16 @@ export function useProviderWizardController({ typeof nextFooterConfig === "function" ? nextFooterConfig(currentFooterConfig) : nextFooterConfig; + footerConfigRef.current = resolvedNextFooterConfig; if (isSameFooterConfig(currentFooterConfig, resolvedNextFooterConfig)) { return; } - footerConfigRef.current = resolvedNextFooterConfig; - setFooterConfigState(resolvedNextFooterConfig); + setFooterConfigState({ + ...resolvedNextFooterConfig, + ...footerActionCallbacksRef.current, + }); }; const openOrganizationsFlow = () => { diff --git a/ui/components/providers/wizard/provider-wizard-modal.test.tsx b/ui/components/providers/wizard/provider-wizard-modal.test.tsx index 6509de65a4..7c33a6fdd5 100644 --- a/ui/components/providers/wizard/provider-wizard-modal.test.tsx +++ b/ui/components/providers/wizard/provider-wizard-modal.test.tsx @@ -81,12 +81,8 @@ describe("ProviderWizardModal", () => { // Then const dialog = screen.getByRole("dialog", { name: /adding a provider/i }); - const descriptionId = dialog.getAttribute("aria-describedby"); - expect(descriptionId).toBeTruthy(); - expect(document.getElementById(descriptionId ?? "")).toHaveTextContent( - /connect or update a provider/i, - ); + expect(dialog).toHaveAccessibleDescription(/connect or update a provider/i); }); it("shows the launch progress step when update mode reaches launch", async () => {