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
63 changes: 63 additions & 0 deletions src/components/menu/__tests__/menu.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import Menu from "../index";

jest.mock("i18n-react/dist/i18n-react", () => ({
__esModule: true,
default: { translate: (key) => key }
}));

jest.mock("react-router-dom", () => ({
withRouter: (component) => component
}));

jest.mock("../../../models/member", () => jest.fn().mockImplementation(() => ({
hasAccess: () => true
})));

jest.mock("../menu-definition", () => ({
getGlobalItems: () => [{ name: "directory", linkUrl: "directory" }],
getSummitItems: () => [{ name: "summit_dashboard", linkUrl: "dashboard" }]
}));

const mockHistory = {
push: jest.fn(),
location: { pathname: "/app/directory" }
};

const renderMenu = (props = {}) =>
render(<Menu member={{}} history={mockHistory} {...props} />);

describe("Menu", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("renders the general section", () => {
renderMenu();
expect(screen.getByText("menu.general")).toBeInTheDocument();
});

test("does not render summit section when currentSummit is not provided", () => {
renderMenu();
expect(screen.queryByText("Test Summit")).not.toBeInTheDocument();
});

test("renders summit section when currentSummit has a valid id", () => {
renderMenu({ currentSummit: { id: 1, name: "Test Summit" } });
expect(screen.getByText("Test Summit")).toBeInTheDocument();
});

test("does not render summit section when currentSummit.id is 0", () => {
renderMenu({ currentSummit: { id: 0, name: "Test Summit" } });
expect(screen.queryByText("Test Summit")).not.toBeInTheDocument();
});

test("navigates when a menu item is clicked", () => {
renderMenu();
const link = screen.getByText("menu.directory");
fireEvent.click(link);
expect(mockHistory.push).toHaveBeenCalledWith("/app/directory");
});
});
54 changes: 54 additions & 0 deletions src/components/menu/expandable-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2026 OpenStack Foundation
* 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 React, { useState } from "react";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync open when defaultOpen changes.

defaultOpen is route-derived in src/components/menu/sub-menu-item.js:26-33, but useState(defaultOpen) only applies on the first mount. After navigation, the active section can stay collapsed because this component never re-reads the updated prop.

Suggested fix
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
@@
   const [open, setOpen] = useState(defaultOpen);
+
+  useEffect(() => {
+    setOpen(defaultOpen);
+  }, [defaultOpen]);

Also applies to: 25-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/menu/expandable-item.js` at line 14, The ExpandableItem
component currently initializes state with useState(defaultOpen) but never
updates when the prop changes, so add an effect to sync the local open state
when the defaultOpen prop updates: in the ExpandableItem component (where
useState(defaultOpen) is used) import and use useEffect to call
setOpen(defaultOpen) whenever defaultOpen changes; apply the same fix for the
similar instance around lines 25-29 (the other useState(defaultOpen) usage) so
route-driven changes to defaultOpen re-open/collapse the item as expected.

import ListItemButton from "@mui/material/ListItemButton";
import Typography from "@mui/material/Typography";
import Collapse from "@mui/material/Collapse";
import List from "@mui/material/List";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";

function ExpandableItem({
label,
children,
defaultOpen = true,
isHeader = false
}) {
const [open, setOpen] = useState(defaultOpen);

return (
<>
<ListItemButton
onClick={() => setOpen((prev) => !prev)}
sx={{ py: 1, ...(isHeader ? { px: 2 } : { pl: 2 }) }}
>
<Typography
variant="body1"
sx={{
flexGrow: 1,
...(isHeader && { fontWeight: 700, fontSize: "16px" })
}}
>
{label}
</Typography>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
<Collapse in={open} timeout="auto" unmountOnExit>
<List disablePadding>{children}</List>
</Collapse>
</>
);
}

export default ExpandableItem;
Loading
Loading