From ead7b7d538fbd3cf6e65214237b1b64e773152df Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Sun, 18 Jan 2026 14:06:55 -0600 Subject: [PATCH] =?UTF-8?q?##=20CHANGES=20-=20Split=20Git=20status=20into?= =?UTF-8?q?=20file,=20branch,=20and=20detail=20hooks=20for=20cleaner=20UI?= =?UTF-8?q?=20wiring=20=F0=9F=A7=A9=20-=20Supercharged=20panel=20resizing?= =?UTF-8?q?=20by=20updating=20state=20only=20on=20mouseup=20=E2=9A=A1=20-?= =?UTF-8?q?=20Added=20explicit=20tests=20ensuring=20resize=20drags=20avoid?= =?UTF-8?q?=20excessive=20rerenders=20=F0=9F=A7=AA=20-=20Refreshed=20dashb?= =?UTF-8?q?oard=20wording:=20=E2=80=9CAgent=20Comparison=E2=80=9D=20is=20n?= =?UTF-8?q?ow=20=E2=80=9CProvider=20Comparison=E2=80=9D=20=F0=9F=94=81=20-?= =?UTF-8?q?=20Renamed=20=E2=80=9CSource=20Distribution=E2=80=9D=20chart=20?= =?UTF-8?q?to=20clearer=20=E2=80=9CSession=20Type=E2=80=9D=20labeling=20?= =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20-=20Updated=20chart=20accessibility=20l?= =?UTF-8?q?abels=20and=20headings=20to=20match=20new=20terminology=20?= =?UTF-8?q?=E2=99=BF=20-=20Improved=20mobile=20timestamps:=20time-only=20f?= =?UTF-8?q?or=20today,=20date+time=20for=20older=20=F0=9F=95=B0=EF=B8=8F?= =?UTF-8?q?=20-=20Added=20robust=20MessageHistory=20tests=20covering=20tod?= =?UTF-8?q?ay=20vs=20older=20date=20formatting=20=F0=9F=93=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renderer/components/MainPanel.test.tsx | 21 +++++++++++++++ .../renderer/components/RightPanel.test.tsx | 10 ++++++- .../renderer/components/SessionList.test.tsx | 27 +++++++++++++++++-- .../AgentComparisonChart.test.tsx | 6 ++--- .../SourceDistributionChart.test.tsx | 6 ++--- .../chart-accessibility.test.tsx | 14 +++++----- .../colorblind-palette.test.tsx | 8 +++--- .../components/UsageDashboardModal.test.tsx | 4 +-- .../web/mobile/MessageHistory.test.tsx | 20 ++++++++++++-- src/web/mobile/MessageHistory.tsx | 14 +++++++++- 10 files changed, 105 insertions(+), 25 deletions(-) diff --git a/src/__tests__/renderer/components/MainPanel.test.tsx b/src/__tests__/renderer/components/MainPanel.test.tsx index 3b0a3c5a..e26013ed 100644 --- a/src/__tests__/renderer/components/MainPanel.test.tsx +++ b/src/__tests__/renderer/components/MainPanel.test.tsx @@ -175,6 +175,27 @@ vi.mock('../../../renderer/contexts/GitStatusContext', () => ({ getFileCount: (sessionId: string) => mockGitStatusData[sessionId]?.fileCount ?? 0, getStatus: (sessionId: string) => mockGitStatusData[sessionId], }), + useGitFileStatus: () => ({ + getFileCount: (sessionId: string) => mockGitStatusData[sessionId]?.fileCount ?? 0, + hasChanges: (sessionId: string) => (mockGitStatusData[sessionId]?.fileCount ?? 0) > 0, + isLoading: false, + }), + useGitBranch: () => ({ + getBranchInfo: (sessionId: string) => { + const status = mockGitStatusData[sessionId]; + if (!status) return undefined; + return { + branch: status.branch, + remote: status.remote, + ahead: status.ahead || 0, + behind: status.behind || 0, + }; + }, + }), + useGitDetail: () => ({ + getFileDetails: () => undefined, + refreshGitStatus: mockRefreshGitStatus, + }), })); // Import MainPanel after mocks diff --git a/src/__tests__/renderer/components/RightPanel.test.tsx b/src/__tests__/renderer/components/RightPanel.test.tsx index be0d0cba..651a48b0 100644 --- a/src/__tests__/renderer/components/RightPanel.test.tsx +++ b/src/__tests__/renderer/components/RightPanel.test.tsx @@ -356,9 +356,14 @@ describe('RightPanel', () => { // Start resize fireEvent.mouseDown(resizeHandle, { clientX: 500 }); - // Simulate mouse move + // Simulate mouse move (direct DOM update for performance, no state call yet) fireEvent.mouseMove(document, { clientX: 450 }); // 50px to the left (makes panel wider since reversed) + // State is only updated on mouseUp for performance (avoids ~60 re-renders/sec) + expect(setRightPanelWidthState).not.toHaveBeenCalled(); + + // End resize - state is updated + fireEvent.mouseUp(document); expect(setRightPanelWidthState).toHaveBeenCalled(); }); @@ -375,6 +380,9 @@ describe('RightPanel', () => { // Try to make it very wide (delta = 500 - (-500) = 1000) fireEvent.mouseMove(document, { clientX: -500 }); + // End resize - state is updated on mouseUp + fireEvent.mouseUp(document); + // Should be clamped to max 800 const calls = setRightPanelWidthState.mock.calls; const lastCall = calls[calls.length - 1][0]; diff --git a/src/__tests__/renderer/components/SessionList.test.tsx b/src/__tests__/renderer/components/SessionList.test.tsx index 753e2721..fe234a2a 100644 --- a/src/__tests__/renderer/components/SessionList.test.tsx +++ b/src/__tests__/renderer/components/SessionList.test.tsx @@ -76,6 +76,18 @@ vi.mock('../../../renderer/contexts/GitStatusContext', () => ({ getFileCount: () => 0, getStatus: () => undefined, }), + useGitFileStatus: () => ({ + getFileCount: () => 0, + hasChanges: () => false, + isLoading: false, + }), + useGitBranch: () => ({ + getBranchInfo: () => undefined, + }), + useGitDetail: () => ({ + getFileDetails: () => undefined, + refreshGitStatus: vi.fn().mockResolvedValue(undefined), + }), })); // Add tunnel mock to window.maestro @@ -1155,10 +1167,14 @@ describe('SessionList', () => { // Simulate drag fireEvent.mouseDown(resizeHandle!, { clientX: 300 }); - // Move mouse + // Move mouse (direct DOM update for performance, no state call yet) fireEvent.mouseMove(document, { clientX: 350 }); - // Width should be updated + // State is only updated on mouseUp for performance (avoids ~60 re-renders/sec) + expect(setLeftSidebarWidthState).not.toHaveBeenCalled(); + + // End resize - state is updated + fireEvent.mouseUp(document); expect(setLeftSidebarWidthState).toHaveBeenCalled(); }); }); @@ -2618,13 +2634,20 @@ describe('SessionList', () => { // Try to drag beyond max (600px) fireEvent.mouseDown(resizeHandle!, { clientX: 300 }); fireEvent.mouseMove(document, { clientX: 1000 }); + // State is only updated on mouseUp for performance + fireEvent.mouseUp(document); // Should be clamped to 600 expect(setLeftSidebarWidthState).toHaveBeenCalledWith(600); + // Reset mock for next test + setLeftSidebarWidthState.mockClear(); + // Try to drag below min (256px) fireEvent.mouseDown(resizeHandle!, { clientX: 300 }); fireEvent.mouseMove(document, { clientX: 100 }); + // State is only updated on mouseUp for performance + fireEvent.mouseUp(document); // Should be clamped to 256 expect(setLeftSidebarWidthState).toHaveBeenCalledWith(256); diff --git a/src/__tests__/renderer/components/UsageDashboard/AgentComparisonChart.test.tsx b/src/__tests__/renderer/components/UsageDashboard/AgentComparisonChart.test.tsx index c5bb581c..8f813963 100644 --- a/src/__tests__/renderer/components/UsageDashboard/AgentComparisonChart.test.tsx +++ b/src/__tests__/renderer/components/UsageDashboard/AgentComparisonChart.test.tsx @@ -85,7 +85,7 @@ describe('AgentComparisonChart', () => { it('renders the component with title', () => { render(); - expect(screen.getByText('Agent Comparison')).toBeInTheDocument(); + expect(screen.getByText('Provider Comparison')).toBeInTheDocument(); }); it('renders count and duration labels for each agent', () => { @@ -302,7 +302,7 @@ describe('AgentComparisonChart', () => { it('applies theme text colors', () => { render(); - const title = screen.getByText('Agent Comparison'); + const title = screen.getByText('Provider Comparison'); expect(title).toHaveStyle({ color: theme.colors.textMain, }); @@ -313,7 +313,7 @@ describe('AgentComparisonChart', () => { render(); - expect(screen.getByText('Agent Comparison')).toBeInTheDocument(); + expect(screen.getByText('Provider Comparison')).toBeInTheDocument(); }); it('applies border colors from theme', () => { diff --git a/src/__tests__/renderer/components/UsageDashboard/SourceDistributionChart.test.tsx b/src/__tests__/renderer/components/UsageDashboard/SourceDistributionChart.test.tsx index f3f8d195..aa979b17 100644 --- a/src/__tests__/renderer/components/UsageDashboard/SourceDistributionChart.test.tsx +++ b/src/__tests__/renderer/components/UsageDashboard/SourceDistributionChart.test.tsx @@ -88,7 +88,7 @@ describe('SourceDistributionChart', () => { it('renders the component with title', () => { render(); - expect(screen.getByText('Source Distribution')).toBeInTheDocument(); + expect(screen.getByText('Session Type')).toBeInTheDocument(); }); it('renders metric toggle buttons', () => { @@ -376,7 +376,7 @@ describe('SourceDistributionChart', () => { it('applies theme text colors', () => { render(); - const title = screen.getByText('Source Distribution'); + const title = screen.getByText('Session Type'); expect(title).toHaveStyle({ color: theme.colors.textMain, }); @@ -387,7 +387,7 @@ describe('SourceDistributionChart', () => { render(); - expect(screen.getByText('Source Distribution')).toBeInTheDocument(); + expect(screen.getByText('Session Type')).toBeInTheDocument(); }); it('applies border colors from theme', () => { diff --git a/src/__tests__/renderer/components/UsageDashboard/chart-accessibility.test.tsx b/src/__tests__/renderer/components/UsageDashboard/chart-accessibility.test.tsx index 78c1b103..cd9032de 100644 --- a/src/__tests__/renderer/components/UsageDashboard/chart-accessibility.test.tsx +++ b/src/__tests__/renderer/components/UsageDashboard/chart-accessibility.test.tsx @@ -81,8 +81,8 @@ describe('Chart Accessibility - AgentComparisonChart', () => { render(); const figure = screen.getByRole('figure'); expect(figure).toHaveAttribute('aria-label'); - expect(figure.getAttribute('aria-label')).toContain('Agent comparison chart'); - expect(figure.getAttribute('aria-label')).toContain('3 agents displayed'); + expect(figure.getAttribute('aria-label')).toContain('Provider comparison chart'); + expect(figure.getAttribute('aria-label')).toContain('3 providers displayed'); }); it('has proper aria attributes on meter elements', () => { @@ -136,10 +136,10 @@ describe('Chart Accessibility - SourceDistributionChart', () => { expect(figure).toBeInTheDocument(); }); - it('has descriptive aria-label mentioning source distribution', () => { + it('has descriptive aria-label mentioning session type', () => { render(); const figure = screen.getByRole('figure'); - expect(figure.getAttribute('aria-label')).toContain('Source distribution'); + expect(figure.getAttribute('aria-label')).toContain('Session type'); }); it('has aria-pressed on toggle buttons', () => { @@ -353,11 +353,11 @@ describe('Chart Accessibility - General ARIA Patterns', () => { it('all charts have proper heading structure', () => { // Render each chart and verify h3 headings exist const { unmount: u1 } = render(); - expect(screen.getByRole('heading', { level: 3, name: /agent comparison/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 3, name: /provider comparison/i })).toBeInTheDocument(); u1(); const { unmount: u2 } = render(); - expect(screen.getByRole('heading', { level: 3, name: /source distribution/i })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 3, name: /session type/i })).toBeInTheDocument(); u2(); const { unmount: u3 } = render(); @@ -429,7 +429,7 @@ describe('Chart Accessibility - Screen Reader Announcements', () => { const ariaLabel = figure.getAttribute('aria-label') || ''; expect(ariaLabel).toContain('query counts'); expect(ariaLabel).toContain('duration'); - expect(ariaLabel).toContain('agents displayed'); + expect(ariaLabel).toContain('providers displayed'); }); it('SourceDistributionChart provides percentage summary in SVG', () => { diff --git a/src/__tests__/renderer/components/UsageDashboard/colorblind-palette.test.tsx b/src/__tests__/renderer/components/UsageDashboard/colorblind-palette.test.tsx index 2609d0be..8c1935f9 100644 --- a/src/__tests__/renderer/components/UsageDashboard/colorblind-palette.test.tsx +++ b/src/__tests__/renderer/components/UsageDashboard/colorblind-palette.test.tsx @@ -140,12 +140,12 @@ describe('Colorblind Palette Constants', () => { describe('AgentComparisonChart with colorBlindMode', () => { it('renders with colorBlindMode=false by default', () => { render(); - expect(screen.getByText('Agent Comparison')).toBeInTheDocument(); + expect(screen.getByText('Provider Comparison')).toBeInTheDocument(); }); it('renders with colorBlindMode=true', () => { render(); - expect(screen.getByText('Agent Comparison')).toBeInTheDocument(); + expect(screen.getByText('Provider Comparison')).toBeInTheDocument(); }); it('uses colorblind palette colors when colorBlindMode is enabled', () => { @@ -182,12 +182,12 @@ describe('AgentComparisonChart with colorBlindMode', () => { describe('SourceDistributionChart with colorBlindMode', () => { it('renders with colorBlindMode=false by default', () => { render(); - expect(screen.getByText('Source Distribution')).toBeInTheDocument(); + expect(screen.getByText('Session Type')).toBeInTheDocument(); }); it('renders with colorBlindMode=true', () => { render(); - expect(screen.getByText('Source Distribution')).toBeInTheDocument(); + expect(screen.getByText('Session Type')).toBeInTheDocument(); }); it('renders both Interactive and Auto Run labels', () => { diff --git a/src/__tests__/renderer/components/UsageDashboardModal.test.tsx b/src/__tests__/renderer/components/UsageDashboardModal.test.tsx index 4f6cf24d..3d4c3036 100644 --- a/src/__tests__/renderer/components/UsageDashboardModal.test.tsx +++ b/src/__tests__/renderer/components/UsageDashboardModal.test.tsx @@ -1731,10 +1731,10 @@ describe('UsageDashboardModal', () => { expect(screen.getByTestId('section-summary-cards')).toHaveAttribute('aria-label', 'Summary Cards'); expect(screen.getByTestId('section-agent-comparison')).toHaveAttribute('tabIndex', '0'); - expect(screen.getByTestId('section-agent-comparison')).toHaveAttribute('aria-label', 'Agent Comparison Chart'); + expect(screen.getByTestId('section-agent-comparison')).toHaveAttribute('aria-label', 'Provider Comparison Chart'); expect(screen.getByTestId('section-source-distribution')).toHaveAttribute('tabIndex', '0'); - expect(screen.getByTestId('section-source-distribution')).toHaveAttribute('aria-label', 'Source Distribution Chart'); + expect(screen.getByTestId('section-source-distribution')).toHaveAttribute('aria-label', 'Session Type Chart'); expect(screen.getByTestId('section-activity-heatmap')).toHaveAttribute('tabIndex', '0'); expect(screen.getByTestId('section-activity-heatmap')).toHaveAttribute('aria-label', 'Activity Heatmap'); diff --git a/src/__tests__/web/mobile/MessageHistory.test.tsx b/src/__tests__/web/mobile/MessageHistory.test.tsx index 3f9f9b3e..fa67620a 100644 --- a/src/__tests__/web/mobile/MessageHistory.test.tsx +++ b/src/__tests__/web/mobile/MessageHistory.test.tsx @@ -158,8 +158,9 @@ describe('MessageHistory', () => { }); describe('Timestamp Formatting', () => { - it('formats timestamp with hour and minute', () => { - const timestamp = new Date('2024-01-15T14:30:00').getTime(); + it('formats timestamp with hour and minute for today', () => { + // Use current date to ensure it's "today" + const timestamp = Date.now(); const logs: LogEntry[] = [ createLogEntry({ timestamp, text: 'Test', source: 'user' }), ]; @@ -169,6 +170,21 @@ describe('MessageHistory', () => { const messageCard = container.querySelector('[style*="padding: 10px 12px"]'); expect(messageCard?.textContent).toMatch(/\d{1,2}:\d{2}/); }); + + it('shows date and time for messages older than today', () => { + // Use a date from yesterday + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const timestamp = yesterday.getTime(); + const logs: LogEntry[] = [ + createLogEntry({ timestamp, text: 'Old message', source: 'user' }), + ]; + const { container } = render(); + const messageCard = container.querySelector('[style*="padding: 10px 12px"]'); + // Should contain both date (month/day) and time + // Format is like "Jan 15 14:30" - check for month abbreviation pattern + expect(messageCard?.textContent).toMatch(/[A-Z][a-z]{2}\s+\d{1,2}\s+\d{1,2}:\d{2}/); + }); }); describe('Source Type Styling', () => { diff --git a/src/web/mobile/MessageHistory.tsx b/src/web/mobile/MessageHistory.tsx index d5ea1820..2ddd23a7 100644 --- a/src/web/mobile/MessageHistory.tsx +++ b/src/web/mobile/MessageHistory.tsx @@ -40,10 +40,22 @@ export interface MessageHistoryProps { /** * Format timestamp for display + * Shows time only for today's messages, date + time for older messages */ function formatTime(timestamp: number): string { const date = new Date(timestamp); - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const now = new Date(); + const isToday = date.toDateString() === now.toDateString(); + + if (isToday) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } else { + return ( + date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + + ' ' + + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ); + } } /**