From 1debf7fa3541cde8284a9f06a15e5a677f49d3c6 Mon Sep 17 00:00:00 2001 From: Chris Suich Date: Wed, 18 Jun 2025 11:42:38 -0400 Subject: [PATCH] fix(techdocs): handle undefined defaultPath in techdocs initial redirect When handling the initial redirect for techdocs deep linking an undefined value for defaultPath was being included in the url resulting in an error page. https://github.com/backstage/backstage/issues/30300 Signed-off-by: Chris Suich --- .changeset/chubby-dots-kiss.md | 5 ++ .../TechDocsReaderPageContent/dom.test.tsx | 72 +++++++++++++++++++ .../TechDocsReaderPageContent/dom.tsx | 6 +- 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .changeset/chubby-dots-kiss.md create mode 100644 plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx diff --git a/.changeset/chubby-dots-kiss.md b/.changeset/chubby-dots-kiss.md new file mode 100644 index 0000000000..b23fe2fcef --- /dev/null +++ b/.changeset/chubby-dots-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Handle undefined defaultPath in TechDocs initial redirect. diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx new file mode 100644 index 0000000000..80b19f0308 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2025 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 { render } from '@testing-library/react'; + +// We need to mock react-router-dom hooks used by useInitialRedirect +import { useLocation, useNavigate, useParams } from 'react-router-dom'; + +// Import the module from which the hook is defined +import { useInitialRedirect } from './dom'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), + useNavigate: jest.fn(), + useParams: jest.fn(), +})); + +describe('useInitialRedirect', () => { + const mockNavigate = jest.fn(); + + beforeEach(() => { + // Reset mocks before each test + mockNavigate.mockReset(); + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + (useLocation as jest.Mock).mockReturnValue({ + pathname: '/docs/default/Component/backstage-demo', + }); + // Simulate that no current path is provided + (useParams as jest.Mock).mockReturnValue({ '*': '' }); + }); + + const TestComponent: React.FC<{ defaultPath?: string }> = ({ + defaultPath, + }) => { + // Call hook that should trigger a redirect on mount only if defaultPath is a non-empty string. + useInitialRedirect(defaultPath); + return
Test
; + }; + + it('should not navigate when defaultPath is undefined', () => { + render(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should navigate when defaultPath is a non-empty string', () => { + render(); + expect(mockNavigate).toHaveBeenCalledWith( + '/docs/default/Component/backstage-demo/overview', + { replace: true }, + ); + }); + + it('should not navigate if currPath is non-empty', () => { + // Override useParams to simulate a non-empty currPath + (useParams as jest.Mock).mockReturnValue({ '*': 'existing-path' }); + render(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 0c4d6dee3e..0837710d7a 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -61,15 +61,13 @@ const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)'; // If a defaultPath is specified then we should navigate to that path replacing the // current location in the history. This should only happen on the initial load so // navigating to the root of the docs doesn't also redirect. -const useInitialRedirect = (defaultPath?: string) => { - // const hasRun = useRef(false); - +export const useInitialRedirect = (defaultPath?: string) => { const location = useLocation(); const navigate = useNavigate(); const { '*': currPath = '' } = useParams(); useLayoutEffect(() => { - if (currPath === '' && defaultPath !== '') { + if (currPath === '' && defaultPath && defaultPath !== '') { navigate(`${location.pathname}${defaultPath}`, { replace: true }); } }, []); // eslint-disable-line react-hooks/exhaustive-deps