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 <csuich2@gmail.com>
This commit is contained in:
Chris Suich
2025-06-18 11:42:38 -04:00
parent 9ad11f6555
commit 1debf7fa35
3 changed files with 79 additions and 4 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Handle undefined defaultPath in TechDocs initial redirect.
@@ -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 <div>Test</div>;
};
it('should not navigate when defaultPath is undefined', () => {
render(<TestComponent defaultPath={undefined} />);
expect(mockNavigate).not.toHaveBeenCalled();
});
it('should navigate when defaultPath is a non-empty string', () => {
render(<TestComponent defaultPath="/overview" />);
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(<TestComponent defaultPath={undefined} />);
expect(mockNavigate).not.toHaveBeenCalled();
});
});
@@ -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