## CHANGES

- Split Git status into file, branch, and detail hooks for cleaner UI wiring 🧩
- Supercharged panel resizing by updating state only on mouseup 
- Added explicit tests ensuring resize drags avoid excessive rerenders 🧪
- Refreshed dashboard wording: “Agent Comparison” is now “Provider Comparison” 🔁
- Renamed “Source Distribution” chart to clearer “Session Type” labeling 🏷️
- Updated chart accessibility labels and headings to match new terminology 
- Improved mobile timestamps: time-only for today, date+time for older 🕰️
- Added robust MessageHistory tests covering today vs older date formatting 📅
This commit is contained in:
Pedram Amini
2026-01-18 14:06:55 -06:00
parent fe77591d42
commit ead7b7d538
10 changed files with 105 additions and 25 deletions

View File

@@ -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

View File

@@ -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];

View File

@@ -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);

View File

@@ -85,7 +85,7 @@ describe('AgentComparisonChart', () => {
it('renders the component with title', () => {
render(<AgentComparisonChart data={mockData} theme={theme} />);
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(<AgentComparisonChart data={mockData} theme={theme} />);
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(<AgentComparisonChart data={mockData} theme={lightTheme} />);
expect(screen.getByText('Agent Comparison')).toBeInTheDocument();
expect(screen.getByText('Provider Comparison')).toBeInTheDocument();
});
it('applies border colors from theme', () => {

View File

@@ -88,7 +88,7 @@ describe('SourceDistributionChart', () => {
it('renders the component with title', () => {
render(<SourceDistributionChart data={mockData} theme={theme} />);
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(<SourceDistributionChart data={mockData} theme={theme} />);
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(<SourceDistributionChart data={mockData} theme={lightTheme} />);
expect(screen.getByText('Source Distribution')).toBeInTheDocument();
expect(screen.getByText('Session Type')).toBeInTheDocument();
});
it('applies border colors from theme', () => {

View File

@@ -81,8 +81,8 @@ describe('Chart Accessibility - AgentComparisonChart', () => {
render(<AgentComparisonChart data={mockStatsData} theme={mockTheme} />);
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(<SourceDistributionChart data={mockStatsData} theme={mockTheme} />);
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(<AgentComparisonChart data={mockStatsData} theme={mockTheme} />);
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(<SourceDistributionChart data={mockStatsData} theme={mockTheme} />);
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(<ActivityHeatmap data={mockStatsData} timeRange="week" theme={mockTheme} />);
@@ -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', () => {

View File

@@ -140,12 +140,12 @@ describe('Colorblind Palette Constants', () => {
describe('AgentComparisonChart with colorBlindMode', () => {
it('renders with colorBlindMode=false by default', () => {
render(<AgentComparisonChart data={mockData} theme={theme} />);
expect(screen.getByText('Agent Comparison')).toBeInTheDocument();
expect(screen.getByText('Provider Comparison')).toBeInTheDocument();
});
it('renders with colorBlindMode=true', () => {
render(<AgentComparisonChart data={mockData} theme={theme} colorBlindMode={true} />);
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(<SourceDistributionChart data={mockData} theme={theme} />);
expect(screen.getByText('Source Distribution')).toBeInTheDocument();
expect(screen.getByText('Session Type')).toBeInTheDocument();
});
it('renders with colorBlindMode=true', () => {
render(<SourceDistributionChart data={mockData} theme={theme} colorBlindMode={true} />);
expect(screen.getByText('Source Distribution')).toBeInTheDocument();
expect(screen.getByText('Session Type')).toBeInTheDocument();
});
it('renders both Interactive and Auto Run labels', () => {

View File

@@ -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');

View File

@@ -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(<MessageHistory logs={logs} inputMode="ai" />);
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', () => {

View File

@@ -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' })
);
}
}
/**