Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/quickstart/.changeset/clickable-menu-refix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-quickstart': patch
---

Fix Quick start help menu item not responding to clicks in the global-header dropdown
2 changes: 1 addition & 1 deletion workspaces/quickstart/packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@mui/icons-material": "5.18.0",
"@mui/material": "5.18.0",
"@red-hat-developer-hub/backstage-plugin-app-react": "^0.1.0",
"@red-hat-developer-hub/backstage-plugin-global-header": "^1.21.0",
"@red-hat-developer-hub/backstage-plugin-global-header": "^1.21.2",
"@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11",
"material-icons": "^1.13.14",
Expand Down
2 changes: 1 addition & 1 deletion workspaces/quickstart/plugins/quickstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@mui/material": "5.18.0",
"@patternfly/react-icons": "^6.5.0",
"@red-hat-developer-hub/backstage-plugin-app-react": "^0.1.0",
"@red-hat-developer-hub/backstage-plugin-global-header": "^1.21.0",
"@red-hat-developer-hub/backstage-plugin-global-header": "^1.21.2",
"@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11",
"react-use": "^17.6.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright Red Hat, Inc.
*
* 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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderInTestApp } from '@backstage/test-utils';

import { QuickstartHelpMenuItem } from './QuickstartHelpMenuItem';
import { QUICKSTART_DRAWER_ID } from './const';

const mockToggleDrawer = jest.fn();

jest.mock('@red-hat-developer-hub/backstage-plugin-app-react', () => ({
useAppDrawer: () => ({
toggleDrawer: mockToggleDrawer,
isOpen: jest.fn(),
openDrawer: jest.fn(),
closeDrawer: jest.fn(),
}),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] mock-reset-idiom

Uses mockToggleDrawer.mockClear() instead of jest.clearAllMocks(), which is the predominant idiom in sibling test files (7 of 8 use clearAllMocks).

Suggested fix: Replace mockToggleDrawer.mockClear() with jest.clearAllMocks() in the beforeEach block.


jest.mock('@red-hat-developer-hub/backstage-plugin-global-header', () => ({
MenuItemLink: ({ title, icon }: { title?: string; icon?: string }) => (
<span data-testid="menu-item-link" data-icon={icon}>
{title}
</span>
),
}));

describe('QuickstartHelpMenuItem', () => {
beforeEach(() => {
mockToggleDrawer.mockClear();
});

it('renders as a menuitem with Quick start label', async () => {
await renderInTestApp(
<ul role="menu">
<QuickstartHelpMenuItem />
</ul>,
);

expect(
screen.getByRole('menuitem', { name: /Quick start/i }),
).toBeInTheDocument();
});

it('toggles the quickstart drawer and closes the menu on click', async () => {
const handleClose = jest.fn();
const user = userEvent.setup();

await renderInTestApp(
<ul role="menu">
<QuickstartHelpMenuItem handleClose={handleClose} />
</ul>,
);

await user.click(screen.getByRole('menuitem', { name: /Quick start/i }));

expect(mockToggleDrawer).toHaveBeenCalledWith(QUICKSTART_DRAWER_ID);
expect(handleClose).toHaveBeenCalled();
});

it('forwards ref to the underlying MenuItem element', async () => {
const ref = { current: null as HTMLLIElement | null };

await renderInTestApp(
<ul role="menu">
<QuickstartHelpMenuItem ref={ref} />
</ul>,
);

expect(ref.current).toBeInstanceOf(HTMLLIElement);
expect(ref.current).toHaveAttribute('role', 'menuitem');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,50 @@
* limitations under the License.
*/

import { forwardRef } from 'react';
import type { Ref } from 'react';

import { useAppDrawer } from '@red-hat-developer-hub/backstage-plugin-app-react';
import { GlobalHeaderMenuItem } from '@red-hat-developer-hub/backstage-plugin-global-header/alpha';
import { MenuItemLink } from '@red-hat-developer-hub/backstage-plugin-global-header';

import MenuItem from '@mui/material/MenuItem';

import { QUICKSTART_DRAWER_ID } from './const';

export const QuickstartHelpMenuItem = ({
handleClose,
}: {
handleClose?: () => void;
}) => {
const { toggleDrawer } = useAppDrawer();
/**
* Help-dropdown menu item that toggles the Quick start drawer.
*
* Uses `forwardRef` so MUI Menu focus management can attach a ref to the
* underlying `MenuItem`. Avoids `GlobalHeaderMenuItem` without a `to` prop,
* which (in global-header 1.21.0) rendered `MenuItem component={Fragment}` and
* dropped click handlers / menuitem role.
*/
export const QuickstartHelpMenuItem = forwardRef(
function QuickstartHelpMenuItem(
{
handleClose,
}: {
handleClose?: () => void;
},
ref: Ref<HTMLLIElement>,
) {
const { toggleDrawer } = useAppDrawer();

const handleClick = () => {
toggleDrawer(QUICKSTART_DRAWER_ID);
handleClose?.();
};
const handleClick = () => {
toggleDrawer(QUICKSTART_DRAWER_ID);
handleClose?.();
};

return (
<GlobalHeaderMenuItem
title="Quick start"
icon="waving_hand"
onClick={handleClick}
/>
);
};
return (
<MenuItem
ref={ref}
disableRipple
disableTouchRipple
onClick={handleClick}
sx={{ py: 0.5, color: 'inherit', textDecoration: 'none' }}
>
<MenuItemLink to="" title="Quick start" icon="waving_hand" />
</MenuItem>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] click-event-swallowing

MenuItemLink from the main entry point is a createComponentExtension lazy-loaded component. In practice, the global-header plugin chunk is already loaded before any dropdown can be opened (the header renders on every page), so the lazy-load race is not a real concern. The outer MenuItems onClick is independent of the inner components load state.

);
},
);
Loading
Loading