Repository: Vonage/vonage-video-react-app
Pull request #115: VIDCS-3493: Disable change layout button when there is someone pinned
Author: login: behei-vonage
Created: 2025-03-06T21:22:36Z | Merged: 2025-03-10T13:53:55Z

#### What is this PR doing?

Based on our discussion during the sync, this PR disables change layout button when there is someone pinned. 

Simple screenshot:

![image](https://github.com/user-attachments/assets/686c38fb-a0d8-41dc-a7fb-c5e7c6c17fd9)


#### How should this be manually tested?

To test this on desktop:

* Checkout this branch.
* Join a meeting room with two or more users. 
* Screenshare from one of the tabs (testing this since the PR touches on the existing code) - notice that the button to change the layout is grayed out with the tooltip `Cannot switch layout while screen share is active` for all the users in the room. 
* Stop screensharing and pin one of the other tabs in your meeting. Notice that the change layout button is grayed out with the tooltip `Cannont switch layout while a participant is pinned.`
* Unpin the video and notice that the button becomes active again.

* Perform a similar test to the above by starting the screensharing in a meeting with multiple users. While screensharing, also pin another user in your meeting room.
* Stop screensharing.
* Notice that the change layout button remains grayed out but the tooltip changes to `Cannont switch layout while a participant is pinned`.
* Unpin all the tiles
* Try this another way around by pinning first, then screensharing, then unpinning. The results should match as above.

To test this on mobile/responsive:

The steps are pretty much the same above, the only thing is that we cannot screen share from mobile devices; however, the layout button can be pressed from the "kebab" menu.

* Screenshare from a desktop user, notice that for the mobile user the button is grayed out to change the layout in the kebab menu.
* While screensharing from a desktop user, also pin someone on the mobile device. 
* Stop screensharing from the desktop user. Notice that on the mobile user the button remains grayed out to change the layout. 

#### What are the relevant tickets?
A maintainer will add this ticket number.

Resolves [VIDCS-3493](https://jira.vonage.com/browse/VIDCS-3493)

#### Checklist
[ ] Branch is based on `develop` (not `main`).
[ ] Resolves a `Known Issue`.
[ ] If yes, did you remove the item from the `docs/KNOWN_ISSUES.md`? 
[ ] Resolves an item reported in `Issues`.
If yes, which issue? [Issue Number](https://github.com/Vonage/vonage-video-react-app/issues/)?


Diff:
diff --git a/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx b/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx
index 459fc23d9..baf170641 100644
--- a/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx
+++ b/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx
@@ -1,30 +1,61 @@
 import { render, screen } from '@testing-library/react';
 import { describe, expect, it, vi, Mock, beforeEach } from 'vitest';
 import userEvent from '@testing-library/user-event';
+import { SessionContextType } from '../../../Context/SessionProvider/session';
 import LayoutButton from './LayoutButton';
 import useSessionContext from '../../../hooks/useSessionContext';
 
 vi.mock('../../../hooks/useSessionContext');
+const mockUseSessionContext = useSessionContext as Mock<[], SessionContextType>;
+const sessionContext = {
+  setLayoutMode: vi.fn(),
+} as unknown as SessionContextType;
 
 describe('LayoutButton', () => {
   beforeEach(() => {
     vi.clearAllMocks();
+    mockUseSessionContext.mockReturnValue(sessionContext);
   });
   it('should render the sidebar view icon if it is an active speaker layout', () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'active-speaker' });
-    render(<LayoutButton isScreenSharePresent={false} />);
+    sessionContext.layoutMode = 'active-speaker';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
     expect(screen.getByTestId('ViewSidebarIcon')).toBeInTheDocument();
   });
 
+  it('should call the set layout mode function when triggered', async () => {
+    sessionContext.layoutMode = 'active-speaker';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
+    const button = await screen.findByRole('button');
+    await userEvent.click(button);
+    expect(sessionContext.setLayoutMode).toHaveBeenCalled();
+
+    sessionContext.layoutMode = 'grid';
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
+    await userEvent.click(button);
+    expect(sessionContext.setLayoutMode).toHaveBeenCalled();
+  });
+
   it('should render the sidebar window icon if it is a grid layout', () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'grid' });
-    render(<LayoutButton isScreenSharePresent={false} />);
+    sessionContext.layoutMode = 'grid';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
     expect(screen.getByTestId('WindowIcon')).toBeInTheDocument();
   });
 
   it('should render the tooltip title that mentions switching to grid layout', async () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'active-speaker' });
-    render(<LayoutButton isScreenSharePresent={false} />);
+    sessionContext.layoutMode = 'active-speaker';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
     const button = await screen.findByRole('button');
     await userEvent.hover(button);
     const tooltip = await screen.findByRole('tooltip');
@@ -33,8 +64,11 @@ describe('LayoutButton', () => {
   });
 
   it('should render the tooltip title that mentions switching to active speaker layout', async () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'grid' });
-    render(<LayoutButton isScreenSharePresent={false} />);
+    sessionContext.layoutMode = 'grid';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
     const button = await screen.findByRole('button');
     await userEvent.hover(button);
     const tooltip = await screen.findByRole('tooltip');
@@ -43,8 +77,9 @@ describe('LayoutButton', () => {
   });
 
   it('should render the tooltip title that mentions switching layouts is not allowed when screenshare is present if currently in the grid mode', async () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'grid' });
-    render(<LayoutButton isScreenSharePresent />);
+    sessionContext.layoutMode = 'grid';
+    const { rerender } = render(<LayoutButton isScreenSharePresent isPinningPresent={false} />);
+    rerender(<LayoutButton isScreenSharePresent isPinningPresent={false} />);
     const button = await screen.findByRole('button');
     await userEvent.hover(button);
     const tooltip = await screen.findByRole('tooltip');
@@ -53,12 +88,22 @@ describe('LayoutButton', () => {
   });
 
   it('should render the tooltip title that mentions switching layouts is not allowed when screenshare is present if currently in the active speaker mode', async () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'active-speaker' });
-    render(<LayoutButton isScreenSharePresent />);
+    sessionContext.layoutMode = 'active-speaker';
+    const { rerender } = render(<LayoutButton isScreenSharePresent isPinningPresent={false} />);
+    rerender(<LayoutButton isScreenSharePresent isPinningPresent={false} />);
     const button = await screen.findByRole('button');
     await userEvent.hover(button);
     const tooltip = await screen.findByRole('tooltip');
     expect(tooltip).toBeInTheDocument();
     expect(tooltip.textContent).toBe('Cannot switch layout while screen share is active');
   });
+
+  it('should render the tooltip title that mentions switching layouts is not allowed when a pinned participant is present', async () => {
+    render(<LayoutButton isScreenSharePresent={false} isPinningPresent />);
+    const button = await screen.findByRole('button');
+    await userEvent.hover(button);
+    const tooltip = await screen.findByRole('tooltip');
+    expect(tooltip).toBeInTheDocument();
+    expect(tooltip.textContent).toBe('Cannot switch layout while a participant is pinned');
+  });
 });
diff --git a/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx b/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
index fc5000619..07ee7d821 100644
--- a/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
+++ b/frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
@@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinningPresent: boolean;
 };
 
 /**
  * LayoutButton Component
  *
  * Displays a button to toggle the meeting room layout for the user between `grid` and `active-speaker`.
- * @param {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ * @param {LayoutButtonProps} props - the props for the component.
+ *  @property {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ *  @property {boolean} isPinningPresent - Indicates whether there is a participant currently pinned.
  * @returns {ReactElement} The LayoutButton component.
  */
-const LayoutButton = ({ isScreenSharePresent }: LayoutButtonProps): ReactElement | false => {
+const LayoutButton = ({
+  isScreenSharePresent,
+  isPinningPresent,
+}: LayoutButtonProps): ReactElement | false => {
   const { layoutMode, setLayoutMode } = useSessionContext();
   const isGrid = layoutMode === 'grid';
+  const isDisabled = isScreenSharePresent || isPinningPresent;
 
   const handleClick = () => {
-    if (isScreenSharePresent) {
+    if (isDisabled) {
       return;
     }
     setLayoutMode((prev) => (prev === 'grid' ? 'active-speaker' : 'grid'));
@@ -32,6 +39,9 @@ const LayoutButton = ({ isScreenSharePresent }: LayoutButtonProps): ReactElement
     if (isScreenSharePresent) {
       return 'Cannot switch layout while screen share is active';
     }
+    if (isPinningPresent) {
+      return 'Cannot switch layout while a participant is pinned';
+    }
     return isGrid ? 'Switch to Active Speaker layout' : 'Switch to Grid layout';
   };
 
@@ -44,13 +54,13 @@ const LayoutButton = ({ isScreenSharePresent }: LayoutButtonProps): ReactElement
         data-testid="layout-button"
         icon={
           !isGrid ? (
-            <ViewSidebarIcon className={isScreenSharePresent ? 'text-gray-500' : 'text-white'} />
+            <ViewSidebarIcon className={isDisabled ? 'text-gray-500' : 'text-white'} />
           ) : (
-            <WindowIcon className={isScreenSharePresent ? 'text-gray-500' : 'text-white'} />
+            <WindowIcon className={isDisabled ? 'text-gray-500' : 'text-white'} />
           )
         }
         sx={{
-          cursor: isScreenSharePresent ? 'not-allowed' : 'pointer',
+          cursor: isDisabled ? 'not-allowed' : 'pointer',
           // on the small view port devices we need to align the button
           marginTop: isSmallViewport ? '0px' : '4px',
           marginLeft: isSmallViewport ? '12px' : '0px',
diff --git a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx
index 74433d273..7f06d8e0e 100644
--- a/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx
+++ b/frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx
@@ -60,6 +60,7 @@ const Toolbar = ({
   const isReportIssueEnabled = import.meta.env.VITE_ENABLE_REPORT_ISSUE === 'true';
   const isViewingScreenShare = subscriberWrappers.some((subWrapper) => subWrapper.isScreenshare);
   const isScreenSharePresent = isViewingScreenShare || isSharingScreen;
+  const isPinningPresent = subscriberWrappers.some((subWrapper) => subWrapper.isPinned);
   const isSmallViewport = useIsSmallViewport();
   const handleLeave = useCallback(() => {
     if (!disconnect) {
@@ -86,7 +87,10 @@ const Toolbar = ({
               isSharingScreen={isSharingScreen}
               isViewingScreenShare={isViewingScreenShare}
             />
-            <LayoutButton isScreenSharePresent={isScreenSharePresent} />
+            <LayoutButton
+              isScreenSharePresent={isScreenSharePresent}
+              isPinningPresent={isPinningPresent}
+            />
             <EmojiGridButton
               isEmojiGridOpen={openEmojiGridDesktop}
               setIsEmojiGridOpen={setOpenEmojiGridDesktop}
diff --git a/frontend/src/components/MeetingRoom/ToolbarOverflowMenu/ToolbarOverflowMenu.tsx b/frontend/src/components/MeetingRoom/ToolbarOverflowMenu/ToolbarOverflowMenu.tsx
index 5a97a8cda..7979b1782 100644
--- a/frontend/src/components/MeetingRoom/ToolbarOverflowMenu/ToolbarOverflowMenu.tsx
+++ b/frontend/src/components/MeetingRoom/ToolbarOverflowMenu/ToolbarOverflowMenu.tsx
@@ -47,6 +47,7 @@ const ToolbarOverflowMenu = ({
   const participantCount =
     subscriberWrappers.filter(({ isScreenshare }) => !isScreenshare).length + 1;
   const isReportIssueEnabled = import.meta.env.VITE_ENABLE_REPORT_ISSUE === 'true';
+  const isPinningPresent = subscriberWrappers.some((subWrapper) => subWrapper.isPinned);
 
   const closeMenuWrapper = (onClick?: () => void) => () => {
     if (onClick) {
@@ -88,7 +89,10 @@ const ToolbarOverflowMenu = ({
                   paddingLeft: '12px',
                 }}
               >
-                <LayoutButton isScreenSharePresent={isViewingScreenShare} />
+                <LayoutButton
+                  isScreenSharePresent={isViewingScreenShare}
+                  isPinningPresent={isPinningPresent}
+                />
                 <EmojiGridButton
                   isEmojiGridOpen={isEmojiGridOpen}
                   setIsEmojiGridOpen={setIsEmojiGridOpen}


Reviews:
user: login: behei-vonage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T21:40:53Z
user: login: v-kpheng
body: Looks good! 💪 

That said, left some questions/comments.  Thanks! 🙏 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T21:57:20Z
user: login: behei-vonage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T21:59:07Z
user: login: behei-vonage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T22:43:37Z
user: login: behei-vonage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T22:44:56Z
user: login: cpettet
body: Looks good so far! Just have a few comments/questions. Let me know what you think!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:48:25Z
user: login: behei-vonage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:51:09Z
user: login: cpettet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:53:32Z
user: login: behei-vonage
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T19:32:33Z
user: login: cpettet
body: LGTM Great job! :shipit:
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T20:55:18Z
user: login: v-kpheng
body: LGTM! 💪 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T22:03:39Z

Review comments:
diff_hunk: @@ -86,7 +87,10 @@ const Toolbar = ({
               isSharingScreen={isSharingScreen}
               isViewingScreenShare={isViewingScreenShare}
             />
-            <LayoutButton isScreenSharePresent={isScreenSharePresent} />
+            <LayoutButton
+              isScreenSharePresent={isScreenSharePresent}
+              isPinnedPresent={isPinnedPresent}
+            />
path: frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx
user: login: behei-vonage
body: Note: I consider combining these two into something like this:

```
<LayoutButton
    isDisabled={isScreenSharePresent || isPinnedPresent}
/>
```
however, keeping the props separately helps with handling with cases when there are both screen share and pinning present and it helps with the tooltip title in `LayoutButton` since if I just pass in `isDisabled` I won't be able to differentiate what title to display 🙏 

created_at: 2025-03-06T21:40:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinnedPresent: boolean;
 };
 
 /**
  * LayoutButton Component
  *
  * Displays a button to toggle the meeting room layout for the user between `grid` and `active-speaker`.
- * @param {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ * @param {LayoutButtonProps} props - the props for the component.
+ *  @property {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ *  @property {boolean} isPinnedPresent - Indicates whether there is a participant currently pinned.
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: v-kpheng
body: Nit: `isPinnedPresent` -> `isPinningPresent`?
created_at: 2025-03-06T21:51:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,11 +54,21 @@ describe('LayoutButton', () => {
 
   it('should render the tooltip title that mentions switching layouts is not allowed when screenshare is present if currently in the active speaker mode', async () => {
     (useSessionContext as Mock).mockReturnValue({ layoutMode: 'active-speaker' });
-    render(<LayoutButton isScreenSharePresent />);
+    render(<LayoutButton isScreenSharePresent isPinnedPresent={false} />);
     const button = await screen.findByRole('button');
     await userEvent.hover(button);
     const tooltip = await screen.findByRole('tooltip');
     expect(tooltip).toBeInTheDocument();
     expect(tooltip.textContent).toBe('Cannot switch layout while screen share is active');
   });
+
+  it('should render the tooltip title that mentions switching layouts is not allowed when a pinned participant is present', async () => {
+    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'grid' });
+    render(<LayoutButton isScreenSharePresent={false} isPinnedPresent />);
+    const button = await screen.findByRole('button');
+    await userEvent.hover(button);
+    const tooltip = await screen.findByRole('tooltip');
+    expect(tooltip).toBeInTheDocument();
+    expect(tooltip.textContent).toBe('Cannont switch layout while a participant is pinned');
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx
user: login: v-kpheng
body: Nit: 'Cannont' -> 'Can not' ?
created_at: 2025-03-06T21:52:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinnedPresent: boolean;
 };
 
 /**
  * LayoutButton Component
  *
  * Displays a button to toggle the meeting room layout for the user between `grid` and `active-speaker`.
- * @param {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ * @param {LayoutButtonProps} props - the props for the component.
+ *  @property {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ *  @property {boolean} isPinnedPresent - Indicates whether there is a participant currently pinned.
  * @returns {ReactElement} The LayoutButton component.
  */
-const LayoutButton = ({ isScreenSharePresent }: LayoutButtonProps): ReactElement | false => {
+const LayoutButton = ({
+  isScreenSharePresent,
+  isPinnedPresent,
+}: LayoutButtonProps): ReactElement | false => {
   const { layoutMode, setLayoutMode } = useSessionContext();
   const isGrid = layoutMode === 'grid';
+  const isDisabled = isScreenSharePresent || isPinnedPresent;
 
   const handleClick = () => {
-    if (isScreenSharePresent) {
+    if (isDisabled) {
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: v-kpheng
body: Do we have test coverage for this?  That covers both screen share and pinning?
created_at: 2025-03-06T21:53:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,7 +87,10 @@ const Toolbar = ({
               isSharingScreen={isSharingScreen}
               isViewingScreenShare={isViewingScreenShare}
             />
-            <LayoutButton isScreenSharePresent={isScreenSharePresent} />
+            <LayoutButton
+              isScreenSharePresent={isScreenSharePresent}
+              isPinnedPresent={isPinnedPresent}
+            />
path: frontend/src/components/MeetingRoom/Toolbar/Toolbar.tsx
user: login: v-kpheng
body: I think the combined approach better, since the component should only care about whether it is active or not.  That said, I see that the component uses the the split to handle more accurate error messages.  Even so, we can refactor to decouple the error message from the component too 🤷‍♂️ 

In any case, not asking for any changes.  Let's see how others feel, please.
created_at: 2025-03-06T21:56:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,11 +54,21 @@ describe('LayoutButton', () => {
 
   it('should render the tooltip title that mentions switching layouts is not allowed when screenshare is present if currently in the active speaker mode', async () => {
     (useSessionContext as Mock).mockReturnValue({ layoutMode: 'active-speaker' });
-    render(<LayoutButton isScreenSharePresent />);
+    render(<LayoutButton isScreenSharePresent isPinnedPresent={false} />);
     const button = await screen.findByRole('button');
     await userEvent.hover(button);
     const tooltip = await screen.findByRole('tooltip');
     expect(tooltip).toBeInTheDocument();
     expect(tooltip.textContent).toBe('Cannot switch layout while screen share is active');
   });
+
+  it('should render the tooltip title that mentions switching layouts is not allowed when a pinned participant is present', async () => {
+    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'grid' });
+    render(<LayoutButton isScreenSharePresent={false} isPinnedPresent />);
+    const button = await screen.findByRole('button');
+    await userEvent.hover(button);
+    const tooltip = await screen.findByRole('tooltip');
+    expect(tooltip).toBeInTheDocument();
+    expect(tooltip.textContent).toBe('Cannont switch layout while a participant is pinned');
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx
user: login: behei-vonage
body: ope, typo in both the implementation and the test so that's why the tests were passing 😂 great catch, I'll update to `Cannot`, that's how we currently have it with screen share 
created_at: 2025-03-06T21:59:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,30 +1,61 @@
 import { render, screen } from '@testing-library/react';
 import { describe, expect, it, vi, Mock, beforeEach } from 'vitest';
 import userEvent from '@testing-library/user-event';
+import { SessionContextType } from '../../../Context/SessionProvider/session';
 import LayoutButton from './LayoutButton';
 import useSessionContext from '../../../hooks/useSessionContext';
 
 vi.mock('../../../hooks/useSessionContext');
+const mockUseSessionContext = useSessionContext as Mock<[], SessionContextType>;
+const sessionContext = {
+  setLayoutMode: vi.fn(),
+} as unknown as SessionContextType;
 
 describe('LayoutButton', () => {
   beforeEach(() => {
     vi.clearAllMocks();
+    mockUseSessionContext.mockReturnValue(sessionContext);
   });
   it('should render the sidebar view icon if it is an active speaker layout', () => {
-    (useSessionContext as Mock).mockReturnValue({ layoutMode: 'active-speaker' });
-    render(<LayoutButton isScreenSharePresent={false} />);
+    sessionContext.layoutMode = 'active-speaker';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
     expect(screen.getByTestId('ViewSidebarIcon')).toBeInTheDocument();
   });
 
+  it('should call the set layout mode function when triggered', async () => {
+    sessionContext.layoutMode = 'active-speaker';
+    const { rerender } = render(
+      <LayoutButton isScreenSharePresent={false} isPinningPresent={false} />
+    );
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
+    const button = await screen.findByRole('button');
+    await userEvent.click(button);
+    expect(sessionContext.setLayoutMode).toHaveBeenCalled();
+
+    sessionContext.layoutMode = 'grid';
+    rerender(<LayoutButton isScreenSharePresent={false} isPinningPresent={false} />);
+    await userEvent.click(button);
+    expect(sessionContext.setLayoutMode).toHaveBeenCalled();
+  });
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.spec.tsx
user: login: behei-vonage
body: I added this unit test for the feedback here: https://github.com/Vonage/vonage-video-react-app/pull/115/files#r1984103601

I settled for doing `expect(sessionContext.setLayoutMode).toHaveBeenCalled();` without `toHaveBeenCalledWith` since `setLayoutMode` has a type `Dispatch<SetStateAction<LayoutMode>>` and afaik there aren't any good ways to unit test this kind of objects. I also don't see any existing examples in our codebase when it relates to stuff like this
created_at: 2025-03-06T22:43:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinnedPresent: boolean;
 };
 
 /**
  * LayoutButton Component
  *
  * Displays a button to toggle the meeting room layout for the user between `grid` and `active-speaker`.
- * @param {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ * @param {LayoutButtonProps} props - the props for the component.
+ *  @property {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ *  @property {boolean} isPinnedPresent - Indicates whether there is a participant currently pinned.
  * @returns {ReactElement} The LayoutButton component.
  */
-const LayoutButton = ({ isScreenSharePresent }: LayoutButtonProps): ReactElement | false => {
+const LayoutButton = ({
+  isScreenSharePresent,
+  isPinnedPresent,
+}: LayoutButtonProps): ReactElement | false => {
   const { layoutMode, setLayoutMode } = useSessionContext();
   const isGrid = layoutMode === 'grid';
+  const isDisabled = isScreenSharePresent || isPinnedPresent;
 
   const handleClick = () => {
-    if (isScreenSharePresent) {
+    if (isDisabled) {
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: behei-vonage
body: I left a note here: https://github.com/Vonage/vonage-video-react-app/pull/115#discussion_r1984150059
created_at: 2025-03-06T22:44:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinningPresent: boolean;
 };
 
 /**
  * LayoutButton Component
  *
  * Displays a button to toggle the meeting room layout for the user between `grid` and `active-speaker`.
- * @param {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ * @param {LayoutButtonProps} props - the props for the component.
+ *  @property {boolean} isScreenSharePresent - Indicates whether there is a screenshare currently in the session.
+ *  @property {boolean} isPinnedPresent - Indicates whether there is a participant currently pinned.
  * @returns {ReactElement} The LayoutButton component.
  */
-const LayoutButton = ({ isScreenSharePresent }: LayoutButtonProps): ReactElement | false => {
+const LayoutButton = ({
+  isScreenSharePresent,
+  isPinningPresent,
+}: LayoutButtonProps): ReactElement | false => {
   const { layoutMode, setLayoutMode } = useSessionContext();
   const isGrid = layoutMode === 'grid';
+  const isDisabled = isScreenSharePresent || isPinningPresent;
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: cpettet
body: I like using `isDisabled` as a variable name, it's much more intuitive.
created_at: 2025-03-07T15:37:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinningPresent: boolean;
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: cpettet
body: Could we change this to `isPinnedPresent` to match across components? Otherwise we have `isPinningPresent` and `isPinnedPresent`.
created_at: 2025-03-07T15:39:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinningPresent: boolean;
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: behei-vonage
body: funny, I started off with `isPinnedPresent` but Kongpheng suggested `isPinningPresent` 😃 @v-kpheng, are you okay if I change this back to `isPinnedPresent` to match how we use it across other components? 🙏 
created_at: 2025-03-07T15:51:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinningPresent: boolean;
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: cpettet
body: Or, maybe we could rename the `isPinnedPresent` in the other component to `isPinningPresent`? I'm not tied to either variable name
created_at: 2025-03-07T15:53:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,23 +6,30 @@ import useSessionContext from '../../../hooks/useSessionContext';
 import ToolbarButton from '../ToolbarButton';
 import useIsSmallViewport from '../../../hooks/useIsSmallViewport';
 
-type LayoutButtonProps = {
+export type LayoutButtonProps = {
   isScreenSharePresent: boolean;
+  isPinningPresent: boolean;
path: frontend/src/components/MeetingRoom/LayoutButton/LayoutButton.tsx
user: login: behei-vonage
body: I changed it to `isPinningPresent` in other components 🙏 
created_at: 2025-03-07T19:32:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-07T19:38:11Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Vonage_vonage-video-react-app&pullRequest=115) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=Vonage_vonage-video-react-app&pullRequest=115&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Vonage_vonage-video-react-app&pullRequest=115&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Vonage_vonage-video-react-app&pullRequest=115&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [97.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=Vonage_vonage-video-react-app&pullRequest=115&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Vonage_vonage-video-react-app&pullRequest=115&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Vonage_vonage-video-react-app&pullRequest=115)


author_association: NONE
user: login: DeliaTok
created_at: 2025-03-07T22:29:28Z
body: looks good!
author_association: NONE