From 40556c0788b5384023f093147e71a4ec035a7f63 Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Tue, 19 May 2026 14:37:26 +0200 Subject: [PATCH] OCPBUGS-85545: Fix guided tour modal flash on page reload Prevent the "Welcome to the new OpenShift experience" modal from briefly appearing on every page reload for users who have already completed or skipped the tour. The stale-frame race between useReducer initialization and useEffect-based sync with user preferences caused startTour to default to true before the loaded completion state was applied. Co-Authored-By: Claude Opus 4.6 --- .../tour/__tests__/tour-context.spec.ts | 15 +++++++++++++++ .../src/components/tour/tour-context.ts | 8 ++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/frontend/packages/console-app/src/components/tour/__tests__/tour-context.spec.ts b/frontend/packages/console-app/src/components/tour/__tests__/tour-context.spec.ts index 6105b92713a..bb52e069d58 100644 --- a/frontend/packages/console-app/src/components/tour/__tests__/tour-context.spec.ts +++ b/frontend/packages/console-app/src/components/tour/__tests__/tour-context.spec.ts @@ -139,6 +139,21 @@ describe('guided-tour-context', () => { expect(tour).toEqual(null); expect(totalSteps).toEqual(undefined); }); + + it('should not flash startTour when loaded transitions to true with completed tour', () => { + useSelectorMock.mockReturnValue({ A: true, B: false }); + useResolvedExtensionsMock.mockReturnValue(mockTourExtension); + // Start with loaded: false (async ConfigMap fetch in progress) + useUserPreferenceMock.mockReturnValue([{ dev: { completed: false } }, () => null, false]); + const { result, rerender } = renderHook(() => useTourValuesForContext()); + expect(result.current.tour).toEqual(null); + + // Simulate ConfigMap load completing with completed: true + useUserPreferenceMock.mockReturnValue([{ dev: { completed: true } }, () => null, true]); + rerender(); + const { tourState } = result.current; + expect(tourState?.startTour).not.toBe(true); + }); }); describe('useTourStatePerspective', () => { diff --git a/frontend/packages/console-app/src/components/tour/tour-context.ts b/frontend/packages/console-app/src/components/tour/tour-context.ts index 62ca051888b..4f5badc7542 100644 --- a/frontend/packages/console-app/src/components/tour/tour-context.ts +++ b/frontend/packages/console-app/src/components/tour/tour-context.ts @@ -1,5 +1,5 @@ import type { Reducer, Dispatch, ReducerAction } from 'react'; -import { createContext, useReducer, useState, useEffect, useCallback } from 'react'; +import { createContext, useReducer, useRef, useState, useEffect, useCallback } from 'react'; import { pick, union, isEqual } from 'lodash'; import { createSelector } from 'reselect'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; @@ -151,14 +151,18 @@ export const useTourValuesForContext = (): TourContextType => { startTour: !completed, }); + const initializedWithLoadedData = useRef(false); useEffect(() => { tourDispatch({ type: TourActions.initialize, payload: { completed } }); setPerspective(activePerspective); + if (loaded) { + initializedWithLoadedData.current = true; + } // only run effect when the active perspective changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [activePerspective, loaded]); - if (!tour || !loaded) return { tour: null }; + if (!tour || !loaded || !initializedWithLoadedData.current) return { tour: null }; const { properties: { tour: { intro, steps: unfilteredSteps, end },