From a7f5939df766b3c62ba7e0811be0a686236efda0 Mon Sep 17 00:00:00 2001 From: logonoff Date: Fri, 20 Mar 2026 19:14:09 -0400 Subject: [PATCH 01/10] OCPBUGS-79000: Fix perspective switcher icon suspending the whole app Replace bare React.lazy() usage with AsyncComponent, which provides its own Suspense boundary and retry logic. The MenuToggle icon was missing a Suspense boundary, causing the lazy-loaded perspective icon to suspend an ancestor boundary and block the entire app content from rendering when concurrent features are enabled via createRoot. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/nav/NavHeader.tsx | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/packages/console-app/src/components/nav/NavHeader.tsx b/frontend/packages/console-app/src/components/nav/NavHeader.tsx index 14641adb0d3..6df73064db4 100644 --- a/frontend/packages/console-app/src/components/nav/NavHeader.tsx +++ b/frontend/packages/console-app/src/components/nav/NavHeader.tsx @@ -1,11 +1,12 @@ import type { FC, MouseEvent, Ref } from 'react'; -import { useMemo, lazy, useState, useCallback, Suspense } from 'react'; +import { useMemo, useState, useCallback } from 'react'; import type { MenuToggleElement } from '@patternfly/react-core'; import { MenuToggle, Select, SelectList, SelectOption, Title } from '@patternfly/react-core'; import { CogsIcon } from '@patternfly/react-icons/dist/esm/icons/cogs-icon'; import { t } from 'i18next'; import type { Perspective } from '@console/dynamic-plugin-sdk'; import { useActivePerspective } from '@console/dynamic-plugin-sdk'; +import { AsyncComponent } from '@console/internal/components/utils/async'; import { usePerspectives } from '@console/shared/src/hooks/usePerspectives'; export type NavHeaderProps = { @@ -19,8 +20,9 @@ type PerspectiveDropdownItemProps = { onClick: (perspective: string) => void; }; +const IconLoadingComponent: FC = () => <> ; + const PerspectiveDropdownItem: FC = ({ perspective, onClick }) => { - const LazyIcon = useMemo(() => lazy(perspective.properties.icon), [perspective.properties.icon]); return ( = ({ perspective onClick(perspective.properties.id); }} icon={ -  }> - - + perspective.properties.icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> } > @@ -75,8 +78,6 @@ const NavHeader: FC<NavHeaderProps> = ({ onPerspectiveSelected }) => { [activePerspective, perspectiveExtensions], ); - const LazyIcon = useMemo(() => icon && lazy(icon), [icon]); - return perspectiveDropdownItems.length > 1 ? ( <div className="oc-nav-header" @@ -94,7 +95,14 @@ const NavHeader: FC<NavHeaderProps> = ({ onPerspectiveSelected }) => { isExpanded={isPerspectiveDropdownOpen} ref={toggleRef} onClick={() => togglePerspectiveOpen()} - icon={<LazyIcon />} + icon={ + icon && ( + <AsyncComponent + loader={() => icon().then((m) => m.default)} + LoadingComponent={IconLoadingComponent} + /> + ) + } > {name && ( <Title headingLevel="h2" size="md"> From 1f5206434c88b3b5fe635bc0912e052975f78536 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Fri, 20 Mar 2026 19:15:14 -0400 Subject: [PATCH 02/10] OCPBUGS-79000: Add suspense to AppContent this allows the blame to be shifted away from "contextProviderExtensions suspense", so we know if it is a contextProviderExtensions suspending or if it is the AppContent as a whole --- frontend/public/components/app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index 22e7caef504..69a4acadc37 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -253,7 +253,7 @@ const App: FC<{ }; const content = ( - <> + <Suspense fallback={<LoadingBox blame="App content suspense" />}> <ConsoleNotifier location="BannerTop" /> <QuickStartDrawer> <CloudShellDrawer> @@ -308,7 +308,7 @@ const App: FC<{ </QuickStartDrawer> <ConsoleNotifier location="BannerBottom" /> <FeatureFlagExtensionLoader /> - </> + </Suspense> ); return ( From 55f5d914890f5b56b691c5d11a24979066164515 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 2 Mar 2026 15:10:43 -0500 Subject: [PATCH 03/10] CONSOLE-4512: Adopt `createRoot` --- frontend/public/components/app.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/public/components/app.tsx b/frontend/public/components/app.tsx index 69a4acadc37..035a80c1b16 100644 --- a/frontend/public/components/app.tsx +++ b/frontend/public/components/app.tsx @@ -11,7 +11,7 @@ import { useMemo, } from 'react'; import type { FC, Provider as ProviderComponent, ReactNode } from 'react'; -import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { linkify } from 'react-linkify'; import { Provider } from 'react-redux'; @@ -352,7 +352,8 @@ const AppWithExtensions: FC = () => { return <LoadingBox blame="AppWithExtensions" />; }; -render(<LoadingBox blame="Init" />, document.getElementById('app')); +const root = createRoot(document.getElementById('app')!); +root.render(<LoadingBox blame="Init" />); const AppRouter: FC = () => { const standaloneRouteExtensions = useExtensions(isStandaloneRoutePage); @@ -525,7 +526,7 @@ graphQLReady.onReady(() => { } } - render( + root.render( <Suspense fallback={<LoadingBox blame="Root suspense" />}> <Provider store={store}> <PluginStoreProvider store={pluginStore}> @@ -542,6 +543,5 @@ graphQLReady.onReady(() => { </PluginStoreProvider> </Provider> </Suspense>, - document.getElementById('app'), ); }); From 1bceda57cbe6965b88c39827109ed84aaa78ad24 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 13:19:47 -0400 Subject: [PATCH 04/10] CONSOLE-4512: Fix tab flicker on UserPreferencePage with createRoot Derive activeTabId directly from URL params instead of syncing state via useEffect. The previous pattern caused a feedback loop under concurrent rendering: clicking a tab set state immediately, but the useEffect reset it to the stale URL value before the navigation took effect, producing a visible flicker. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/components/user-preferences/UserPreferencePage.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx index 5e656d95a8f..f39c4635cb1 100644 --- a/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx +++ b/frontend/packages/console-app/src/components/user-preferences/UserPreferencePage.tsx @@ -47,11 +47,10 @@ const UserPreferencePage: FC = () => { // fetch the default user preference group from the url params if available const { group: groupIdFromUrl } = useParams(); - const initialTabId = + const activeTabId = sortedUserPreferenceGroups.find((extension) => extension.id === groupIdFromUrl)?.id || sortedUserPreferenceGroups[0]?.id || 'general'; - const [activeTabId, setActiveTabId] = useState<string>(initialTabId); const [userPreferenceTabs, userPreferenceTabContents] = useMemo< [ReactElement<TabProps, JSXElementConstructor<TabProps>>[], ReactElement<TabContentProps>[]] @@ -102,12 +101,11 @@ const UserPreferencePage: FC = () => { const [spotlightElement, setSpotlightElement] = useState<Element | null>(null); useEffect(() => { - setActiveTabId(groupIdFromUrl ?? 'general'); if (spotlight) { const element = document.querySelector(spotlight); setSpotlightElement(element); } - }, [groupIdFromUrl, spotlight, userPreferenceItemResolved, userPreferenceTabContents]); + }, [spotlight, userPreferenceItemResolved, userPreferenceTabContents]); // utils and callbacks const handleTabClick = (event: MouseEvent<HTMLElement>, eventKey: string) => { @@ -115,7 +113,6 @@ const UserPreferencePage: FC = () => { return; } event.preventDefault(); - setActiveTabId(eventKey); navigate(`${USER_PREFERENCES_BASE_URL}/${eventKey}`, { replace: true }); }; const activeTab = sortedUserPreferenceGroups.find((group) => group.id === activeTabId)?.label; From 0339b8fc44dffd6f44bd727bd5b81ef9610a7445 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 13:23:11 -0400 Subject: [PATCH 05/10] CONSOLE-4512: Fix namespace switching reverting under createRoot The useEffect syncing activeNamespace from the URL had activeNamespace in its dependency array, creating a feedback loop: switching namespace set state immediately, but the effect re-fired before the URL updated and reset to the old value. Stabilize updateNamespace with useCallback and refs so it does not change identity on every render. This removes all three eslint-disable react-hooks/exhaustive-deps suppressions and provides correct dependency arrays throughout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../components/detect-namespace/namespace.ts | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/frontend/packages/console-app/src/components/detect-namespace/namespace.ts b/frontend/packages/console-app/src/components/detect-namespace/namespace.ts index b5dad0137b5..7cc67e49d98 100644 --- a/frontend/packages/console-app/src/components/detect-namespace/namespace.ts +++ b/frontend/packages/console-app/src/components/detect-namespace/namespace.ts @@ -1,4 +1,4 @@ -import { createContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useState, useEffect, useCallback, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { setActiveNamespace as setActiveNamespaceForStore, @@ -37,17 +37,27 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => const useProjects: boolean = useFlag(FLAGS.OPENSHIFT); const dispatch = useConsoleDispatch(); - const updateNamespace = (ns) => { - if (ns !== activeNamespace) { - setActiveNamespace(ns); - const oldPath = window.location.pathname; - const newPath = formatNamespaceRoute(ns, oldPath, window.location); - if (newPath !== oldPath) { - navigate(newPath); + const activeNamespaceRef = useRef(activeNamespace); + activeNamespaceRef.current = activeNamespace; + const navigateRef = useRef(navigate); + navigateRef.current = navigate; + const lastNamespaceRef = useRef(lastNamespace); + lastNamespaceRef.current = lastNamespace; + + const updateNamespace = useCallback( + (ns: string) => { + if (ns !== activeNamespaceRef.current) { + setActiveNamespace(ns); + const oldPath = window.location.pathname; + const newPath = formatNamespaceRoute(ns, oldPath, window.location); + if (newPath !== oldPath) { + navigateRef.current(newPath); + } } - } - dispatch(setActiveNamespaceForStore(ns)); - }; + dispatch(setActiveNamespaceForStore(ns)); + }, + [dispatch], + ); // Set namespace when all pending namespace infos are loaded. // Automatically check if preferred and last namespace still exist. @@ -55,7 +65,12 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => !flagPending(useProjects) && preferredNamespaceLoaded && lastNamespaceLoaded; useEffect(() => { if (!urlNamespace && resourcesLoaded) { - getValueForNamespace(preferredNamespace, lastNamespace, useProjects, activeNamespace) + getValueForNamespace( + preferredNamespace, + lastNamespace, + useProjects, + activeNamespaceRef.current, + ) .then((ns: string) => { updateNamespace(ns); }) @@ -64,9 +79,14 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => console.warn('Error fetching namespace', e); }); } - // Only run this hook after all resources have loaded. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [resourcesLoaded, navigate]); + }, [ + resourcesLoaded, + urlNamespace, + preferredNamespace, + lastNamespace, + useProjects, + updateNamespace, + ]); // Updates active namespace (in context and redux state) when the url changes. // This updates the redux state also after the initial rendering. @@ -74,18 +94,18 @@ export const useValuesForNamespaceContext: UseValuesForNamespaceContext = () => if (urlNamespace) { updateNamespace(urlNamespace); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [urlNamespace, activeNamespace, dispatch, navigate]); + }, [urlNamespace, updateNamespace]); // Change active namespace (in context and redux state) as well as last used namespace // when a component calls setNamespace, for example via useActiveNamespace() const setNamespace = useCallback( (ns: string) => { - ns !== lastNamespace && setLastNamespace(ns); + if (ns !== lastNamespaceRef.current) { + setLastNamespace(ns); + } updateNamespace(ns); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dispatch, activeNamespace, setActiveNamespace, lastNamespace, setLastNamespace, navigate], + [updateNamespace, setLastNamespace], ); return { From d05c2467051f5821f7b477a163907833a881814e Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 13:26:11 -0400 Subject: [PATCH 06/10] CONSOLE-4512: Fix infinite loop in HealthChecksItem with createRoot setHealthCheck was called directly during render, dispatching to the parent's useReducer and triggering an immediate re-render cycle. React 18's createRoot is stricter about setState during render and detects this as an infinite loop. Move the call into a useEffect so it only runs after render when the computed values actually change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../components/nodes/node-dashboard/NodeHealth.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx index f88b2fccb93..67e5efba05b 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeHealth.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode } from 'react'; -import { useContext } from 'react'; +import { useContext, useEffect } from 'react'; import { Gallery, GalleryItem, Alert, Stack, StackItem } from '@patternfly/react-core'; import i18next from 'i18next'; import * as _ from 'lodash'; @@ -272,10 +272,12 @@ export const HealthChecksItem: FC<HealthChecksItemProps> = ({ disabledAlert }) = return true; }); - setHealthCheck({ - failingHealthCheck, - reboot, - }); + useEffect(() => { + setHealthCheck({ + failingHealthCheck, + reboot, + }); + }, [failingHealthCheck, reboot, setHealthCheck]); return ( <HealthItem From 9eca379a610810ac8200ce6817cd523e990b9b53 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 13:27:37 -0400 Subject: [PATCH 07/10] CONSOLE-4512: Fix infinite loop in UtilizationItem with createRoot setLimitReqState was called directly during render, dispatching to the parent's useReducer and triggering an infinite re-render cycle. Move the call into a useEffect so it only fires when the computed limit/requested states change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../dashboard/utilization-card/UtilizationItem.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx b/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx index 29b1515c7cb..7467b96149c 100644 --- a/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx +++ b/frontend/packages/console-shared/src/components/dashboard/utilization-card/UtilizationItem.tsx @@ -247,10 +247,15 @@ export const UtilizationItem = memo<UtilizationItemProps>( } else if ([limitState, requestedState].includes(LIMIT_STATE.WARN)) { LimitIcon = YellowExclamationTriangleIcon; } - setLimitReqState && setLimitReqState({ limit: limitState, requested: requestedState }); } } + useEffect(() => { + if (setLimitReqState) { + setLimitReqState({ limit: limitState, requested: requestedState }); + } + }, [limitState, requestedState, setLimitReqState]); + const currentHumanized = !_.isNil(current) ? humanizeValue(current).string : null; return ( From 98336c58a83578d72292fb46e50de6725f5af28b Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 13:34:12 -0400 Subject: [PATCH 08/10] CONSOLE-4512: Fix infinite loop in NodeDashboard with createRoot dispatch({ type: ActionType.OBJ }) was called directly during render to sync the obj prop into reducer state. Move it into a useEffect to avoid infinite re-render cycles under concurrent rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../src/components/nodes/node-dashboard/NodeDashboard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx index db04dfb47f5..c03dbf2ce3d 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { useReducer, useCallback } from 'react'; +import { useReducer, useCallback, useEffect } from 'react'; import * as _ from 'lodash'; import type { NodeKind } from '@console/internal/module/k8s'; import Dashboard from '@console/shared/src/components/dashboard/Dashboard'; @@ -77,9 +77,9 @@ export const reducer = (state: NodeDashboardState, action: NodeDashboardAction) const NodeDashboard: FC<NodeDashboardProps> = ({ obj }) => { const [state, dispatch] = useReducer(reducer, initialState(obj)); - if (obj !== state.obj) { + useEffect(() => { dispatch({ type: ActionType.OBJ, payload: obj }); - } + }, [obj]); const setCPULimit = useCallback( (payload: LimitRequested) => dispatch({ type: ActionType.CPU_LIMIT, payload }), From 5eb882e0ae5f74ceda9b8c1051b348b7b77940d1 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 18:57:09 -0400 Subject: [PATCH 09/10] CONSOLE-4512: Fix setState during render in SecretData setHasRevealableContent was called inside useMemo, which is setState during render. With createRoot this caused re-render cycles that prevented the reveal state from stabilizing, so the E2E test read "Value hidden" instead of the decoded secret value. Derive hasRevealableContent as a useMemo instead of useState, since it only depends on the data prop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .../components/configmap-and-secret-data.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/public/components/configmap-and-secret-data.tsx b/frontend/public/components/configmap-and-secret-data.tsx index 51f60c454a7..dd93bbd955c 100644 --- a/frontend/public/components/configmap-and-secret-data.tsx +++ b/frontend/public/components/configmap-and-secret-data.tsx @@ -139,18 +139,25 @@ const SecretDataRevealButton: FC<SecretDataRevealButtonProps> = ({ reveal, onCli export const SecretData: FC<SecretDataProps> = ({ data }) => { const [reveal, setReveal] = useState(false); - const [hasRevealableContent, setHasRevealableContent] = useState(false); const { t } = useTranslation(); + const hasRevealableContent = useMemo( + () => + data + ? Object.keys(data).some((k) => { + const isBinary = ITOB.isBinary(k, Buffer.from(data[k], 'base64')); + return !isBinary && data[k]; + }) + : false, + [data], + ); + const dataDescriptionList = useMemo(() => { return data ? Object.keys(data) .sort() .map((k) => { const isBinary = ITOB.isBinary(k, Buffer.from(data[k], 'base64')); - if (!isBinary && data[k]) { - setHasRevealableContent(hasRevealableContent || !isBinary); - } return ( <DescriptionListGroup key={k}> <DescriptionListTerm i18n-not-translated="true" data-test="secret-data-term"> @@ -167,7 +174,7 @@ export const SecretData: FC<SecretDataProps> = ({ data }) => { ); }) : []; - }, [data, reveal, hasRevealableContent]); + }, [data, reveal]); return ( <> From e30afe2a9be25ea33b096c0da06b4dc8069e27d0 Mon Sep 17 00:00:00 2001 From: logonoff <git@logonoff.co> Date: Mon, 23 Mar 2026 19:14:54 -0400 Subject: [PATCH 10/10] e2e updates --- .../src/components/editor/CodeEditor.tsx | 8 +++++++- .../cluster-settings/alertmanager/alertmanager.cy.ts | 3 +-- .../tests/crud/roles-rolebindings.cy.ts | 3 +++ .../integration-tests-cypress/views/alertmanager.ts | 3 +-- .../integration-tests-cypress/views/common.ts | 6 +++--- .../integration-tests-cypress/views/details-page.ts | 2 +- .../integration-tests-cypress/views/labels.ts | 2 +- .../integration-tests-cypress/views/list-page.ts | 12 ++++++++---- .../integration-tests-cypress/views/secret.ts | 3 +-- .../integration-tests-cypress/views/yaml-editor.ts | 10 +++++----- 10 files changed, 31 insertions(+), 21 deletions(-) diff --git a/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx b/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx index 56022ddbf69..4927a832535 100644 --- a/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx +++ b/frontend/packages/console-shared/src/components/editor/CodeEditor.tsx @@ -23,6 +23,7 @@ export const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref null, ); const [usesValue] = useState<boolean>(value !== undefined); + const [mounted, setMounted] = useState(false); const shortcutPopover = useShortcutPopover(props.shortcutsPopoverProps); @@ -47,6 +48,7 @@ export const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref } onSave && editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, onSave); // eslint-disable-line no-bitwise onEditorDidMount && onEditorDidMount(editor, monaco); + setMounted(true); }, [onSave, usesValue, onEditorDidMount], ); @@ -103,7 +105,11 @@ export const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref }, [handleResize, minHeight, ToolbarLinks]); return ( - <div style={{ minHeight }} className="ocs-yaml-editor"> + <div + style={{ minHeight }} + className="ocs-yaml-editor" + data-test={mounted ? 'code-editor' : 'code-editor-mounting'} + > <BasicCodeEditor {...props} language={props.language ?? Language.yaml} diff --git a/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts b/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts index 9f72300abda..2da7e68a274 100644 --- a/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts @@ -53,8 +53,7 @@ describe('Alertmanager', () => { }); it('displays the Alertmanager YAML page and saves Alertmanager YAML', () => { - alertmanager.visitAlertmanagerPage(); - detailsPage.selectTab('YAML'); + alertmanager.visitYAMLPage(); yamlEditor.isLoaded(); cy.byTestID('alert-success').should('not.exist'); yamlEditor.clickSaveCreateButton(); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts index 6eaf706835d..421005da8e4 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/roles-rolebindings.cy.ts @@ -56,6 +56,7 @@ const createExampleRoles = () => { const createExampleRoleBindings = () => { cy.log('create RoleBindings instance'); nav.sidenav.clickNavLink(['User Management', 'RoleBindings']); + listPage.titleShouldHaveText('RoleBindings'); listPage.dvRows.shouldBeLoaded(); listPage.clickCreateYAMLbutton(); roleBindings.titleShouldHaveText('Create RoleBinding'); @@ -68,6 +69,7 @@ const createExampleRoleBindings = () => { cy.log('create ClusterRoleBindings instance'); nav.sidenav.clickNavLink(['User Management', 'RoleBindings']); + listPage.titleShouldHaveText('RoleBindings'); listPage.dvRows.shouldBeLoaded(); listPage.clickCreateYAMLbutton(); roleBindings.titleShouldHaveText('Create RoleBinding'); @@ -122,6 +124,7 @@ describe('Roles and RoleBindings', () => { nav.sidenav.clickNavLink(['User Management', 'Roles']); listPage.dvRows.shouldBeLoaded(); projectDropdown.selectProject(testName); + listPage.dvRows.shouldBeLoaded(); listPage.dvFilter.byName(roleName); listPage.dvRows.clickRowByName(roleName); detailsPage.isLoaded(); diff --git a/frontend/packages/integration-tests-cypress/views/alertmanager.ts b/frontend/packages/integration-tests-cypress/views/alertmanager.ts index cba660b292b..3a898475e53 100644 --- a/frontend/packages/integration-tests-cypress/views/alertmanager.ts +++ b/frontend/packages/integration-tests-cypress/views/alertmanager.ts @@ -5,7 +5,6 @@ import type { AlertmanagerConfig, AlertmanagerReceiver, } from '@console/internal/components/monitoring/alertmanager/alertmanager-config'; -import { detailsPage } from './details-page'; import { listPage } from './list-page'; import * as yamlEditor from './yaml-editor'; @@ -87,7 +86,7 @@ export const alertmanager = { cy.visit(`/settings/cluster/alertmanagerconfig/receivers/${receiverName}/edit`); }, visitYAMLPage: () => { - detailsPage.selectTab('YAML'); + cy.visit('/settings/cluster/alertmanageryaml'); yamlEditor.isLoaded(); }, }; diff --git a/frontend/packages/integration-tests-cypress/views/common.ts b/frontend/packages/integration-tests-cypress/views/common.ts index 2ec30169af5..ab12b8eb485 100644 --- a/frontend/packages/integration-tests-cypress/views/common.ts +++ b/frontend/packages/integration-tests-cypress/views/common.ts @@ -10,13 +10,13 @@ export const selectActionsMenuOption = (actionsMenuOption: string) => { export const projectDropdown = { shouldExist: () => cy.byLegacyTestID('namespace-bar-dropdown').should('exist'), selectProject: (projectName: string) => { - // TODO - remove and fix properly - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(3000); cy.reload(); cy.byLegacyTestID('namespace-bar-dropdown').contains('Project:').click(); cy.byTestID('showSystemSwitch').check(); cy.byTestID('dropdown-menu-item-link').contains(projectName).click(); + // TODO - remove and fix properly + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000); }, shouldContain: (name: string) => // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/frontend/packages/integration-tests-cypress/views/details-page.ts b/frontend/packages/integration-tests-cypress/views/details-page.ts index 69d700ddd3b..812ed0904d7 100644 --- a/frontend/packages/integration-tests-cypress/views/details-page.ts +++ b/frontend/packages/integration-tests-cypress/views/details-page.ts @@ -19,7 +19,7 @@ export const detailsPage = { }, breadcrumb: (breadcrumbIndex: number) => cy.byLegacyTestID(`breadcrumb-link-${breadcrumbIndex}`), selectTab: (name: string) => { - cy.get(`a[data-test-id="horizontal-link-${name}"]`).should('exist').click(); + cy.byLegacyTestID(`horizontal-link-${name}`).should('exist').click(); }, }; diff --git a/frontend/packages/integration-tests-cypress/views/labels.ts b/frontend/packages/integration-tests-cypress/views/labels.ts index 0f748715635..7e1a73d2fca 100644 --- a/frontend/packages/integration-tests-cypress/views/labels.ts +++ b/frontend/packages/integration-tests-cypress/views/labels.ts @@ -1,5 +1,5 @@ export const labels = { - inputLabel: (label: string) => cy.byTestID('tags-input').type(label), + inputLabel: (label: string) => cy.byTestID('tags-input').type(`${label}{enter}`), confirmDetailsPageLabelExists: (label: string) => cy.byTestID('label-key').contains(label), clickDetailsPageLabel: () => cy.byTestID('label-key').click(), chipExists: (label: string) => cy.get('#search-toolbar').contains(label).should('exist'), diff --git a/frontend/packages/integration-tests-cypress/views/list-page.ts b/frontend/packages/integration-tests-cypress/views/list-page.ts index 7cf97dcb4aa..17bcbe9dadc 100644 --- a/frontend/packages/integration-tests-cypress/views/list-page.ts +++ b/frontend/packages/integration-tests-cypress/views/list-page.ts @@ -62,13 +62,17 @@ export const listPage = { cy.get('.pf-v6-c-menu-toggle').first().click(), ); cy.get('.pf-v6-c-menu__list-item').contains('Name').click(); - cy.get('[aria-label="Filter by name"]').clear().type(name); + cy.get('[aria-label="Filter by name"]').should('be.enabled').clear(); + cy.get('[aria-label="Filter by name"]').type(name); }, by: (checkboxLabel: string) => { + // Wait for list data to settle before opening the filter, otherwise a + // concurrent re-render (e.g. after a project switch) closes the menu. + cy.byTestID('data-view-table').should('be.visible'); cy.get('[data-ouia-component-id="DataViewCheckboxFilter"]').click(); - cy.get( - `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`, - ).click(); + cy.get(`[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`) + .should('be.visible') + .click(); cy.url().should('include', `=${checkboxLabel}`); cy.get('[data-ouia-component-id="DataViewCheckboxFilter"]').click(); }, diff --git a/frontend/packages/integration-tests-cypress/views/secret.ts b/frontend/packages/integration-tests-cypress/views/secret.ts index e020c0ebab3..4145663a0cf 100644 --- a/frontend/packages/integration-tests-cypress/views/secret.ts +++ b/frontend/packages/integration-tests-cypress/views/secret.ts @@ -46,8 +46,7 @@ export const secrets = { clickRevealValues: () => { // Wait for page to fully stabilize cy.byTestID('loading-indicator', { timeout: 5000 }).should('not.exist'); - // Click reveal-values button with force to handle re-renders - cy.byTestID('reveal-values', { timeout: 30000 }).should('be.visible').click({ force: true }); + cy.byTestID('reveal-values', { timeout: 30000 }).should('be.visible').click(); // Wait for data to be revealed cy.byTestID('secret-data', { timeout: 10000 }).should('be.visible'); }, diff --git a/frontend/packages/integration-tests-cypress/views/yaml-editor.ts b/frontend/packages/integration-tests-cypress/views/yaml-editor.ts index 5d460be35fd..938936d4386 100644 --- a/frontend/packages/integration-tests-cypress/views/yaml-editor.ts +++ b/frontend/packages/integration-tests-cypress/views/yaml-editor.ts @@ -20,11 +20,11 @@ export const setEditorContent = (text: string) => { }); }; -// initially yamlEditor loads with all grey text, finished loading when editor is color coded -// class='mtk27' is the light green color of property such as 'apiVersion' -export const isLoaded = () => cy.get("[class='mtk27']").should('exist'); -// since yaml editor class mtk27 is a font class it doesn't work on an import page with no text -// adding a check for the 1st line number, AND providing a wait allowed the load of the full component +// CodeEditor sets data-test="code-editor" once the Monaco editor has mounted. +// Before that it is "code-editor-mounting", so this check waits for the editor to be ready. +export const isLoaded = () => cy.byTestID('code-editor').should('exist'); +// The code editor check doesn't work on an import page with no text, +// so we check for the monaco textarea and wait for the component to fully load export const isImportLoaded = () => { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(5000);