From 9529d7e59aa663c934869535cda425cd5de5d6f9 Mon Sep 17 00:00:00 2001 From: Brad Groux <3053586+BradGroux@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:26:51 -0500 Subject: [PATCH] fix: toggle active header chat panels (#1317) --- .../__tests__/layout-chrome-mantine.test.tsx | 65 +++++++++++++++++-- web/src/components/layout/Header.tsx | 55 +++++++++++++--- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/web/src/__tests__/layout-chrome-mantine.test.tsx b/web/src/__tests__/layout-chrome-mantine.test.tsx index 37257d22..af20b609 100644 --- a/web/src/__tests__/layout-chrome-mantine.test.tsx +++ b/web/src/__tests__/layout-chrome-mantine.test.tsx @@ -1,10 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { cleanup, screen, waitFor } from '@testing-library/react'; +import { cleanup, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ViewProvider } from '@/contexts/ViewContext'; import { KeyboardProvider } from '@/hooks/useKeyboard'; import { Header } from '@/components/layout/Header'; +import { DesktopBottomPanel } from '@/components/layout/DesktopBottomPanel'; import { DesktopShellProvider } from '@/components/layout/DesktopShellContext'; import { UserMenu } from '@/components/layout/UserMenu'; import { WorkspaceSwitcher } from '@/components/layout/WorkspaceSwitcher'; @@ -137,7 +138,7 @@ function renderHeaderChrome() { ); } -function renderDesktopHeaderChrome() { +function renderDesktopHeaderChrome(options: { withBottomPanel?: boolean } = {}) { Object.defineProperty(window, 'veritasDesktop', { configurable: true, value: { @@ -151,6 +152,7 @@ function renderDesktopHeaderChrome() {
+ {options.withBottomPanel && } @@ -159,7 +161,22 @@ function renderDesktopHeaderChrome() { describe('layout chrome Mantine migration', () => { beforeEach(() => { + const storage = new Map(); + Object.defineProperty(window, 'localStorage', { + configurable: true, + value: { + get length() { + return storage.size; + }, + clear: () => storage.clear(), + getItem: (key: string) => storage.get(key) ?? null, + key: (index: number) => Array.from(storage.keys())[index] ?? null, + removeItem: (key: string) => storage.delete(key), + setItem: (key: string, value: string) => storage.set(key, value), + } satisfies Storage, + }); vi.clearAllMocks(); + window.localStorage.clear(); window.history.replaceState({}, '', '/'); window.HTMLElement.prototype.scrollIntoView = vi.fn(); }); @@ -257,12 +274,52 @@ describe('layout chrome Mantine migration', () => { expect(screen.getByRole('button', { name: 'Collapse left sidebar' })).toBeDefined(); expect(screen.getByRole('button', { name: 'Expand right sidebar' })).toBeDefined(); expect(screen.getByRole('button', { name: 'Open chat dock' })).toBeDefined(); - expect(screen.getByRole('button', { name: 'Board Chat' })).toBeDefined(); - expect(screen.getByRole('button', { name: 'Squad Chat' })).toBeDefined(); + expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined(); + expect(screen.getByRole('button', { name: 'Open Squad Chat' })).toBeDefined(); const brandIcon = screen .getByRole('button', { name: 'Refresh page' }) .querySelector('img[src="/icons/pwa-icon-192.png"]'); expect(brandIcon).toBeDefined(); }); + + it.each(['right', 'bottom'] as const)( + 'toggles and switches the header chat controls with the %s dock', + async (dockPosition) => { + window.localStorage.setItem('veritas.workbench.dockPosition', dockPosition); + const user = userEvent.setup(); + renderDesktopHeaderChrome({ withBottomPanel: true }); + + const openBoard = screen.getByRole('button', { name: 'Open Board Chat' }); + const openSquad = screen.getByRole('button', { name: 'Open Squad Chat' }); + expect(openBoard.getAttribute('aria-pressed')).toBe('false'); + expect(openSquad.getAttribute('aria-pressed')).toBe('false'); + + await user.click(openBoard); + const closeBoard = screen.getByRole('button', { name: 'Close Board Chat' }); + expect(closeBoard.getAttribute('aria-pressed')).toBe('true'); + expect(screen.getByRole('button', { name: 'Switch to Squad Chat' })).toBeDefined(); + + await user.click(closeBoard); + expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined(); + + await user.click(screen.getByRole('button', { name: 'Open Board Chat' })); + await user.click(screen.getByRole('button', { name: 'Switch to Squad Chat' })); + const closeSquad = screen.getByRole('button', { name: 'Close Squad Chat' }); + expect(closeSquad.getAttribute('aria-pressed')).toBe('true'); + expect(screen.getByRole('button', { name: 'Switch to Board Chat' })).toBeDefined(); + + await user.click(closeSquad); + await waitFor(() => { + const board = screen.getByRole('button', { name: 'Open Board Chat' }); + const squad = screen.getByRole('button', { name: 'Open Squad Chat' }); + expect([board, squad]).toContain(document.activeElement); + }); + + await user.click(screen.getByRole('button', { name: 'Open Board Chat' })); + const dock = screen.getByRole('region', { name: `Workbench ${dockPosition} dock` }); + await user.click(within(dock).getByRole('button', { name: `Close ${dockPosition} dock` })); + expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined(); + } + ); }); diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 0cdcf22a..ce0eeaf3 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -166,6 +166,23 @@ export function Header() { openBottomPanel, toggleBottomPanel, } = useDesktopShell(); + const usesWorkbenchChat = isDesktopClient || supportsWorkbenchPanel; + const boardChatActive = usesWorkbenchChat && bottomPanel === 'board-chat'; + const squadChatActive = usesWorkbenchChat && bottomPanel === 'squad-chat'; + const boardChatAction = !usesWorkbenchChat + ? 'Open Board Chat' + : boardChatActive + ? 'Close Board Chat' + : bottomPanel + ? 'Switch to Board Chat' + : 'Open Board Chat'; + const squadChatAction = !usesWorkbenchChat + ? 'Open Squad Chat' + : squadChatActive + ? 'Close Squad Chat' + : bottomPanel + ? 'Switch to Squad Chat' + : 'Open Squad Chat'; const toggleView = useCallback( (nextView: NavigationItem['view']) => setView(view === nextView ? 'board' : nextView), @@ -196,6 +213,14 @@ export function Header() { setChatOpen(true); }, [isDesktopClient, markPanelLoaded, openBottomPanel, supportsWorkbenchPanel]); + const toggleChatPanel = useCallback(() => { + if (usesWorkbenchChat) { + toggleBottomPanel('board-chat'); + return; + } + openChatPanel(); + }, [openChatPanel, toggleBottomPanel, usesWorkbenchChat]); + const openSearchDialog = useCallback( (preset?: SearchPreset) => { markPanelLoaded('search'); @@ -214,6 +239,14 @@ export function Header() { setSquadChatOpen(true); }, [isDesktopClient, markPanelLoaded, openBottomPanel, supportsWorkbenchPanel]); + const toggleSquadChatPanel = useCallback(() => { + if (usesWorkbenchChat) { + toggleBottomPanel('squad-chat'); + return; + } + openSquadChatPanel(); + }, [openSquadChatPanel, toggleBottomPanel, usesWorkbenchChat]); + const openSettingsDialog = useCallback( (section?: string) => { markPanelLoaded('settings'); @@ -504,22 +537,24 @@ export function Header() { {!isCompactHeader && ( <>