mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## 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:
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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' })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user