From 726cab926931966822a78d8e26d679cbbb21c6cc Mon Sep 17 00:00:00 2001 From: Mahendra Suthar Date: Wed, 7 Aug 2024 17:35:35 +0530 Subject: [PATCH 1/5] catalog-graph: fix navigation in catalog graph page Signed-off-by: Mahendra Suthar --- .../CatalogGraphPage/useCatalogGraphPage.ts | 142 ++++++++---------- 1 file changed, 60 insertions(+), 82 deletions(-) diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts index 6e47dedc50..f5f50e25dd 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts @@ -27,8 +27,7 @@ import { useMemo, useState, } from 'react'; -import { useLocation } from 'react-router-dom'; -import usePrevious from 'react-use/esm/usePrevious'; +import { useLocation, useSearchParams } from 'react-router-dom'; import { Direction } from '../EntityRelationsGraph'; export type CatalogGraphPageValue = { @@ -70,6 +69,7 @@ export function useCatalogGraphPage({ }; }): CatalogGraphPageValue { const location = useLocation(); + const [_, setSearchParams] = useSearchParams(); const query = useMemo( () => (qs.parse(location.search, { arrayLimit: 0, ignoreQueryPrefix: true }) || @@ -87,19 +87,53 @@ export function useCatalogGraphPage({ [location.search], ); - // Initial state - const [rootEntityNames, setRootEntityNames] = useState( + const rootEntityNames = useMemo( () => (Array.isArray(query.rootEntityRefs) ? query.rootEntityRefs : initialState?.rootEntityRefs ?? [] ).map(r => parseEntityRef(r)), + [initialState?.rootEntityRefs, query.rootEntityRefs], ); + + const setRootEntityNames = useCallback( + (value: React.SetStateAction) => { + const prev = Array.isArray(query.rootEntityRefs) + ? query.rootEntityRefs + : []; + + const hasChanged = + Array.isArray(value) && + (value.length !== prev.length || + value.some((r, i) => stringifyEntityRef(r) !== prev[i])); + + if (!hasChanged) { + return; + } + + setSearchParams(params => { + const rootEntityRefs = Array.isArray(value) + ? value.map(r => stringifyEntityRef(r)) + : []; + + params.delete('rootEntityRefs[]'); + + rootEntityRefs.forEach(r => { + params.set('rootEntityRefs[]', r); + }); + + return params; + }); + }, + [query.rootEntityRefs, setSearchParams], + ); + const [maxDepth, setMaxDepth] = useState(() => typeof query.maxDepth === 'string' ? parseMaxDepth(query.maxDepth) : initialState?.maxDepth ?? Number.POSITIVE_INFINITY, ); + const [selectedRelations, setSelectedRelations] = useState< string[] | undefined >(() => @@ -107,105 +141,52 @@ export function useCatalogGraphPage({ ? query.selectedRelations : initialState?.selectedRelations, ); + const [selectedKinds, setSelectedKinds] = useState(() => (Array.isArray(query.selectedKinds) ? query.selectedKinds : initialState?.selectedKinds )?.map(k => k.toLocaleLowerCase('en-US')), ); + const [unidirectional, setUnidirectional] = useState(() => typeof query.unidirectional === 'string' ? query.unidirectional === 'true' : initialState?.unidirectional ?? true, ); + const [mergeRelations, setMergeRelations] = useState(() => typeof query.mergeRelations === 'string' ? query.mergeRelations === 'true' : initialState?.mergeRelations ?? true, ); + const [direction, setDirection] = useState(() => typeof query.direction === 'string' ? query.direction : initialState?.direction ?? Direction.LEFT_RIGHT, ); + const [curve, setCurve] = useState<'curveStepBefore' | 'curveMonotoneX'>(() => typeof query.curve === 'string' ? query.curve : initialState?.curve ?? 'curveMonotoneX', ); + const [showFilters, setShowFilters] = useState(() => typeof query.showFilters === 'string' ? query.showFilters === 'true' : initialState?.showFilters ?? true, ); + const toggleShowFilters = useCallback( () => setShowFilters(s => !s), [setShowFilters], ); - // Update from query parameters - const prevQueryParams = usePrevious(location.search); useEffect(() => { - // Only respond to changes to url query params - if (location.search === prevQueryParams) { - return; - } - - if (Array.isArray(query.rootEntityRefs)) { - setRootEntityNames(query.rootEntityRefs.map(r => parseEntityRef(r))); - } - - if (typeof query.maxDepth === 'string') { - setMaxDepth(parseMaxDepth(query.maxDepth)); - } - - if (Array.isArray(query.selectedKinds)) { - setSelectedKinds(query.selectedKinds); - } - - if (Array.isArray(query.selectedRelations)) { - setSelectedRelations(query.selectedRelations); - } - - if (typeof query.unidirectional === 'string') { - setUnidirectional(query.unidirectional === 'true'); - } - - if (typeof query.mergeRelations === 'string') { - setMergeRelations(query.mergeRelations === 'true'); - } - - if (typeof query.direction === 'string') { - setDirection(query.direction); - } - - if (typeof query.showFilters === 'string') { - setShowFilters(query.showFilters === 'true'); - } - }, [ - prevQueryParams, - location.search, - query, - setRootEntityNames, - setMaxDepth, - setSelectedKinds, - setSelectedRelations, - setUnidirectional, - setMergeRelations, - setDirection, - setShowFilters, - ]); - - // Update query parameters - const previousRootEntityRefs = usePrevious( - rootEntityNames.map(e => stringifyEntityRef(e)), - ); - - useEffect(() => { - const rootEntityRefs = rootEntityNames.map(e => stringifyEntityRef(e)); const newParams = qs.stringify( { - rootEntityRefs, maxDepth: isFinite(maxDepth) ? maxDepth : '∞', selectedKinds, selectedRelations, @@ -213,36 +194,33 @@ export function useCatalogGraphPage({ mergeRelations, direction, showFilters, + curve, }, { arrayFormat: 'brackets', addQueryPrefix: true }, ); - const newUrl = `${window.location.pathname}${newParams}`; - // We directly manipulate window history here in order to not re-render - // infinitely (state => location => state => etc). The intention of this - // code is just to ensure the right query/filters are loaded when a user - // clicks the "back" button after clicking a result. - // Only push a new history entry if we switched to another entity, but not - // if we just changed a viewer setting. - if ( - !previousRootEntityRefs || - (rootEntityRefs.length === previousRootEntityRefs.length && - rootEntityRefs.every((v, i) => v === previousRootEntityRefs[i])) - ) { - window.history.replaceState(null, document.title, newUrl); - } else { - window.history.pushState(null, document.title, newUrl); - } + const searchParams = new URLSearchParams(newParams); + + setSearchParams( + params => { + searchParams.forEach((value, key) => { + params.set(key, value); + }); + + return params; + }, + { replace: true }, + ); }, [ - rootEntityNames, maxDepth, + curve, selectedKinds, selectedRelations, unidirectional, mergeRelations, direction, showFilters, - previousRootEntityRefs, + setSearchParams, ]); return { From 530d96f7166bb8db366aec0efb5e2fc94eec5857 Mon Sep 17 00:00:00 2001 From: Mahendra Suthar Date: Wed, 7 Aug 2024 21:55:17 +0530 Subject: [PATCH 2/5] catalog-graph: refactor useCatalogGraphPage hook remove setSearchParam and use qs.stringify and navigate to set search params Signed-off-by: Mahendra Suthar --- .../CatalogGraphPage/useCatalogGraphPage.ts | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts index f5f50e25dd..1f6fb74997 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts @@ -27,12 +27,12 @@ import { useMemo, useState, } from 'react'; -import { useLocation, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { Direction } from '../EntityRelationsGraph'; export type CatalogGraphPageValue = { rootEntityNames: CompoundEntityRef[]; - setRootEntityNames: Dispatch>; + setRootEntityNames: (value: CompoundEntityRef[]) => void; maxDepth: number; setMaxDepth: Dispatch>; selectedRelations: string[] | undefined; @@ -69,7 +69,8 @@ export function useCatalogGraphPage({ }; }): CatalogGraphPageValue { const location = useLocation(); - const [_, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + const query = useMemo( () => (qs.parse(location.search, { arrayLimit: 0, ignoreQueryPrefix: true }) || @@ -97,35 +98,28 @@ export function useCatalogGraphPage({ ); const setRootEntityNames = useCallback( - (value: React.SetStateAction) => { - const prev = Array.isArray(query.rootEntityRefs) - ? query.rootEntityRefs - : []; + (value: CompoundEntityRef[]) => { + const areSame = + rootEntityNames.length === value.length && + rootEntityNames.every( + (r, i) => stringifyEntityRef(r) === stringifyEntityRef(value[i]), + ); - const hasChanged = - Array.isArray(value) && - (value.length !== prev.length || - value.some((r, i) => stringifyEntityRef(r) !== prev[i])); - - if (!hasChanged) { + if (areSame) { return; } - setSearchParams(params => { - const rootEntityRefs = Array.isArray(value) - ? value.map(r => stringifyEntityRef(r)) - : []; + const newSearch = qs.stringify( + { + ...query, + rootEntityRefs: value.map(r => stringifyEntityRef(r)), + }, + { arrayFormat: 'brackets', addQueryPrefix: true }, + ); - params.delete('rootEntityRefs[]'); - - rootEntityRefs.forEach(r => { - params.set('rootEntityRefs[]', r); - }); - - return params; - }); + navigate(newSearch); }, - [query.rootEntityRefs, setSearchParams], + [rootEntityNames, navigate, query], ); const [maxDepth, setMaxDepth] = useState(() => @@ -187,6 +181,7 @@ export function useCatalogGraphPage({ useEffect(() => { const newParams = qs.stringify( { + rootEntityRefs: rootEntityNames.map(stringifyEntityRef), maxDepth: isFinite(maxDepth) ? maxDepth : '∞', selectedKinds, selectedRelations, @@ -199,18 +194,7 @@ export function useCatalogGraphPage({ { arrayFormat: 'brackets', addQueryPrefix: true }, ); - const searchParams = new URLSearchParams(newParams); - - setSearchParams( - params => { - searchParams.forEach((value, key) => { - params.set(key, value); - }); - - return params; - }, - { replace: true }, - ); + navigate(newParams, { replace: true }); }, [ maxDepth, curve, @@ -220,7 +204,8 @@ export function useCatalogGraphPage({ mergeRelations, direction, showFilters, - setSearchParams, + rootEntityNames, + navigate, ]); return { From 8c3639e60a1f31470499bee13bfe3917e2c64da5 Mon Sep 17 00:00:00 2001 From: Mahendra Suthar Date: Fri, 16 Aug 2024 01:07:39 +0530 Subject: [PATCH 3/5] fix useCatalogGraphPage tests update tests to test back button behaviour Signed-off-by: Mahendra Suthar --- .../useCatalogGraphPage.test.tsx | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx new file mode 100644 index 0000000000..85cb606482 --- /dev/null +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.tsx @@ -0,0 +1,182 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RELATION_MEMBER_OF } from '@backstage/catalog-model'; +import { renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { BrowserRouter } from 'react-router-dom'; +import { Direction } from '../EntityRelationsGraph'; +import { useCatalogGraphPage } from './useCatalogGraphPage'; + +const wrapper = ({ children }: { children?: React.ReactNode }) => { + return {children}; +}; + +describe('useCatalogGraphPage', () => { + test('should use initial state', () => { + const { result } = renderHook(props => useCatalogGraphPage(props), { + initialProps: { + initialState: { + rootEntityRefs: ['b:d/c'], + maxDepth: 2, + direction: Direction.RIGHT_LEFT, + mergeRelations: false, + unidirectional: false, + showFilters: false, + selectedKinds: ['API'], + selectedRelations: [RELATION_MEMBER_OF], + }, + }, + wrapper, + }); + + expect(result.current.rootEntityNames).toEqual([ + { kind: 'b', namespace: 'd', name: 'c' }, + ]); + expect(result.current.maxDepth).toEqual(2); + expect(result.current.direction).toEqual(Direction.RIGHT_LEFT); + expect(result.current.mergeRelations).toEqual(false); + expect(result.current.unidirectional).toEqual(false); + expect(result.current.showFilters).toEqual(false); + expect(result.current.selectedKinds).toEqual(['api']); + expect(result.current.selectedRelations).toEqual([RELATION_MEMBER_OF]); + }); + + test('should use state from url', () => { + act(() => { + history.pushState( + {}, + '', + '?rootEntityRefs[]=b:d/c&maxDepth=2&direction=RL&mergeRelations=false&unidirectional=false&showFilters=false&selectedKinds[]=api&selectedRelations[]=memberOf', + ); + }); + + const { result } = renderHook(props => useCatalogGraphPage(props), { + initialProps: {}, + wrapper, + }); + + expect(result.current.rootEntityNames).toEqual([ + { kind: 'b', namespace: 'd', name: 'c' }, + ]); + expect(result.current.maxDepth).toEqual(2); + expect(result.current.direction).toEqual(Direction.RIGHT_LEFT); + expect(result.current.mergeRelations).toEqual(false); + expect(result.current.unidirectional).toEqual(false); + expect(result.current.showFilters).toEqual(false); + expect(result.current.selectedKinds).toEqual(['api']); + expect(result.current.selectedRelations).toEqual([RELATION_MEMBER_OF]); + }); + + test('should update state in url (replace if setting changes)', () => { + const { result } = renderHook(props => useCatalogGraphPage(props), { + wrapper, + initialProps: {}, + }); + + act(() => result.current.setMaxDepth(5)); + + expect(window.location.search).toEqual( + '?rootEntityRefs%5B%5D=b%3Ad%2Fc&maxDepth=5&selectedKinds%5B%5D=api&selectedRelations%5B%5D=memberOf&unidirectional=false&mergeRelations=false&direction=RL&showFilters=false&curve=curveMonotoneX', + ); + + act(() => result.current.setUnidirectional(false)); + + expect(window.location.search).toEqual( + '?rootEntityRefs%5B%5D=b%3Ad%2Fc&maxDepth=5&selectedKinds%5B%5D=api&selectedRelations%5B%5D=memberOf&unidirectional=false&mergeRelations=false&direction=RL&showFilters=false&curve=curveMonotoneX', + ); + }); + + test('should update state in url (only push if different root entity)', () => { + const oldLength = window.history.length; + const { result } = renderHook(props => useCatalogGraphPage(props), { + initialProps: { + initialState: { + rootEntityRefs: ['component:default/first'], + }, + }, + wrapper, + }); + + act(() => + result.current.setRootEntityNames([ + { kind: 'component', namespace: 'default', name: 'my' }, + ]), + ); + + expect(window.history.length).toEqual(oldLength + 1); + expect(window.location.search).toEqual( + '?rootEntityRefs%5B%5D=component%3Adefault%2Fmy&maxDepth=5&selectedKinds%5B%5D=api&selectedRelations%5B%5D=memberOf&unidirectional=false&mergeRelations=false&direction=RL&showFilters=false&curve=curveMonotoneX', + ); + }); + + test('should update state to last state on back', async () => { + const { result } = renderHook(props => useCatalogGraphPage(props), { + wrapper, + initialProps: { + initialState: { + rootEntityRefs: ['component:default/first'], + }, + }, + }); + + act(() => + result.current.setRootEntityNames([ + { kind: 'component', namespace: 'default', name: 'first' }, + ]), + ); + + expect(window.location.search).toEqual( + '?rootEntityRefs%5B%5D=component%3Adefault%2Ffirst&maxDepth=5&selectedKinds%5B%5D=api&selectedRelations%5B%5D=memberOf&unidirectional=false&mergeRelations=false&direction=RL&showFilters=false&curve=curveMonotoneX', + ); + + act(() => + result.current.setRootEntityNames([ + { kind: 'component', namespace: 'default', name: 'second' }, + ]), + ); + + expect(window.location.search).toEqual( + '?rootEntityRefs%5B%5D=component%3Adefault%2Fsecond&maxDepth=5&selectedKinds%5B%5D=api&selectedRelations%5B%5D=memberOf&unidirectional=false&mergeRelations=false&direction=RL&showFilters=false&curve=curveMonotoneX', + ); + + act(() => { + result.current.setRootEntityNames([ + { kind: 'component', namespace: 'default', name: 'third' }, + ]); + }); + + act(() => { + window.history.back(); + }); + + await waitFor(() => { + expect(result.current.rootEntityNames).toEqual([ + { kind: 'component', namespace: 'default', name: 'second' }, + ]); + }); + + act(() => { + window.history.back(); + }); + + await waitFor(() => { + expect(result.current.rootEntityNames).toEqual([ + { kind: 'component', namespace: 'default', name: 'first' }, + ]); + }); + }); +}); From da9107852cd4afef9e7c6c2e59ddeb00ee8c362c Mon Sep 17 00:00:00 2001 From: Mahendra Suthar Date: Fri, 16 Aug 2024 01:15:41 +0530 Subject: [PATCH 4/5] add changesets for patch update Signed-off-by: Mahendra Suthar --- .changeset/quiet-spies-clean.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-spies-clean.md diff --git a/.changeset/quiet-spies-clean.md b/.changeset/quiet-spies-clean.md new file mode 100644 index 0000000000..ba277f4021 --- /dev/null +++ b/.changeset/quiet-spies-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Fixed a bug in the `CatalogGraphPage` component where, after clicking on some nodes, clicking the back button would break the navigation. This issue caused the entire navigation to fail and behaved differently across various browsers. From 8279366558f9f283a1cd73636ef5c16254565564 Mon Sep 17 00:00:00 2001 From: Mahendra Suthar Date: Fri, 16 Aug 2024 14:49:58 +0530 Subject: [PATCH 5/5] remove useCatalogGraphPage.test.ts Signed-off-by: Mahendra Suthar --- .../useCatalogGraphPage.test.ts | 147 ------------------ 1 file changed, 147 deletions(-) delete mode 100644 plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.ts diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.ts b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.ts deleted file mode 100644 index a89fa353d6..0000000000 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { RELATION_MEMBER_OF } from '@backstage/catalog-model'; -import { act, renderHook } from '@testing-library/react'; -import { useLocation as useLocationMocked } from 'react-router-dom'; -import { Direction } from '../EntityRelationsGraph'; -import { useCatalogGraphPage } from './useCatalogGraphPage'; - -jest.mock('react-router-dom', () => ({ - useLocation: jest.fn(), -})); - -jest.spyOn(window.history, 'replaceState'); -jest.spyOn(window.history, 'pushState'); - -const useLocation = useLocationMocked as jest.Mock< - ReturnType ->; -const windowHistoryReplaceState = window.history.replaceState as jest.Mock< - ReturnType ->; -const windowHistoryPushState = window.history.pushState as jest.Mock< - ReturnType ->; - -describe('useCatalogGraphPage', () => { - beforeEach(() => { - useLocation.mockReturnValue({ - search: '?', - state: {}, - key: '', - pathname: '', - hash: '', - }); - }); - - afterEach(() => jest.resetAllMocks()); - - test('should use initial state', () => { - const { result } = renderHook(() => - useCatalogGraphPage({ - initialState: { - rootEntityRefs: ['b:d/c'], - maxDepth: 2, - direction: Direction.RIGHT_LEFT, - mergeRelations: false, - unidirectional: false, - showFilters: false, - selectedKinds: ['API'], - selectedRelations: [RELATION_MEMBER_OF], - }, - }), - ); - - expect(result.current.rootEntityNames).toEqual([ - { kind: 'b', namespace: 'd', name: 'c' }, - ]); - expect(result.current.maxDepth).toEqual(2); - expect(result.current.direction).toEqual(Direction.RIGHT_LEFT); - expect(result.current.mergeRelations).toEqual(false); - expect(result.current.unidirectional).toEqual(false); - expect(result.current.showFilters).toEqual(false); - expect(result.current.selectedKinds).toEqual(['api']); - expect(result.current.selectedRelations).toEqual([RELATION_MEMBER_OF]); - }); - - test('should use state from url', () => { - useLocation.mockReturnValueOnce({ - search: - '?rootEntityRefs[]=b:d/c&maxDepth=2&direction=RL&mergeRelations=false&unidirectional=false&showFilters=false&selectedKinds[]=api&selectedRelations[]=memberOf', - state: {}, - key: '', - pathname: '', - hash: '', - }); - - const { result } = renderHook(() => useCatalogGraphPage({})); - - expect(result.current.rootEntityNames).toEqual([ - { kind: 'b', namespace: 'd', name: 'c' }, - ]); - expect(result.current.maxDepth).toEqual(2); - expect(result.current.direction).toEqual(Direction.RIGHT_LEFT); - expect(result.current.mergeRelations).toEqual(false); - expect(result.current.unidirectional).toEqual(false); - expect(result.current.showFilters).toEqual(false); - expect(result.current.selectedKinds).toEqual(['api']); - expect(result.current.selectedRelations).toEqual([RELATION_MEMBER_OF]); - }); - - test('should update state in url (replace if setting changes)', () => { - const { result } = renderHook(() => useCatalogGraphPage({})); - - act(() => result.current.setMaxDepth(5)); - - expect(windowHistoryReplaceState).toHaveBeenCalledWith( - null, - '', - '/?maxDepth=5&unidirectional=true&mergeRelations=true&direction=LR&showFilters=true', - ); - - act(() => result.current.setUnidirectional(false)); - - expect(windowHistoryReplaceState).toHaveBeenCalledWith( - null, - '', - '/?maxDepth=5&unidirectional=false&mergeRelations=true&direction=LR&showFilters=true', - ); - }); - - test('should update state in url (only push if different root entity)', () => { - const { result, rerender } = renderHook(() => - useCatalogGraphPage({ - initialState: { - rootEntityRefs: ['component:default/first'], - }, - }), - ); - - act(() => - result.current.setRootEntityNames([ - { kind: 'Component', namespace: 'default', name: 'my' }, - ]), - ); - - rerender(); - - expect(windowHistoryPushState).toHaveBeenCalledWith( - null, - '', - '/?rootEntityRefs%5B%5D=component%3Adefault%2Fmy&maxDepth=%E2%88%9E&unidirectional=true&mergeRelations=true&direction=LR&showFilters=true', - ); - }); -});