mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add EmptyState component for Usage Dashboard
- Create EmptyState component with theme-aware styling - Features BarChart3 icon with decorative SVG bars - Supports customizable title and message props - Default message: "No usage data yet. Start using Maestro to see your stats!" - Update UsageDashboardModal to use new component - Add comprehensive test suite (16 tests)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Tests for EmptyState component
|
||||
*
|
||||
* Verifies:
|
||||
* - Renders empty state container with correct test ID
|
||||
* - Displays chart icon illustration
|
||||
* - Shows default title and message
|
||||
* - Supports custom title and message
|
||||
* - Applies theme colors properly
|
||||
* - Theme-aware styling for all elements
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { EmptyState } from '../../../../renderer/components/UsageDashboard/EmptyState';
|
||||
import { THEMES } from '../../../../shared/themes';
|
||||
|
||||
// Test themes
|
||||
const darkTheme = THEMES['dracula'];
|
||||
const lightTheme = THEMES['solarized-light'];
|
||||
|
||||
describe('EmptyState', () => {
|
||||
describe('Rendering', () => {
|
||||
it('renders the empty state container with correct test ID', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
expect(screen.getByTestId('usage-dashboard-empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the chart icon illustration', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
// The container should have the icon (BarChart3 renders as svg)
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
const svgs = container.querySelectorAll('svg');
|
||||
|
||||
// Should have at least 2 svgs: main icon and decorative bars
|
||||
expect(svgs.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('displays the default title', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
expect(screen.getByText('No usage data yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the default message', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
expect(screen.getByText('Start using Maestro to see your stats!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Content', () => {
|
||||
it('supports custom title', () => {
|
||||
render(
|
||||
<EmptyState
|
||||
theme={darkTheme}
|
||||
title="No data for this period"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No data for this period')).toBeInTheDocument();
|
||||
expect(screen.queryByText('No usage data yet')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports custom message', () => {
|
||||
render(
|
||||
<EmptyState
|
||||
theme={darkTheme}
|
||||
message="Try selecting a different time range."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Try selecting a different time range.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Start using Maestro to see your stats!')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports both custom title and message', () => {
|
||||
render(
|
||||
<EmptyState
|
||||
theme={darkTheme}
|
||||
title="Custom Title"
|
||||
message="Custom message for testing."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Custom Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom message for testing.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme Styling', () => {
|
||||
it('applies theme textDim color to container', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
expect(container).toHaveStyle({ color: darkTheme.colors.textDim });
|
||||
});
|
||||
|
||||
it('applies theme textMain color to title', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const title = screen.getByText('No usage data yet');
|
||||
expect(title).toHaveStyle({ color: darkTheme.colors.textMain });
|
||||
});
|
||||
|
||||
it('works with light theme', () => {
|
||||
render(<EmptyState theme={lightTheme} />);
|
||||
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
expect(container).toHaveStyle({ color: lightTheme.colors.textDim });
|
||||
|
||||
const title = screen.getByText('No usage data yet');
|
||||
expect(title).toHaveStyle({ color: lightTheme.colors.textMain });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layout', () => {
|
||||
it('uses flexbox centering layout', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
expect(container).toHaveClass('flex');
|
||||
expect(container).toHaveClass('flex-col');
|
||||
expect(container).toHaveClass('items-center');
|
||||
expect(container).toHaveClass('justify-center');
|
||||
});
|
||||
|
||||
it('has gap between icon and text', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
expect(container).toHaveClass('gap-4');
|
||||
});
|
||||
|
||||
it('has full height container', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
expect(container).toHaveClass('h-full');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Icon Styling', () => {
|
||||
it('renders decorative svg with theme-aware fills', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const container = screen.getByTestId('usage-dashboard-empty');
|
||||
const rects = container.querySelectorAll('rect');
|
||||
|
||||
// The decorative bars should have rect elements
|
||||
expect(rects.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that rects use the theme color
|
||||
rects.forEach((rect) => {
|
||||
expect(rect.getAttribute('fill')).toBe(darkTheme.colors.textDim);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('has a data-testid for testing', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
expect(screen.getByTestId('usage-dashboard-empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('text is visible and readable', () => {
|
||||
render(<EmptyState theme={darkTheme} />);
|
||||
|
||||
const title = screen.getByText('No usage data yet');
|
||||
const message = screen.getByText('Start using Maestro to see your stats!');
|
||||
|
||||
// Text should be in the document and visible
|
||||
expect(title).toBeVisible();
|
||||
expect(message).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
97
src/renderer/components/UsageDashboard/EmptyState.tsx
Normal file
97
src/renderer/components/UsageDashboard/EmptyState.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* EmptyState
|
||||
*
|
||||
* Displays a friendly empty state message when no usage data exists.
|
||||
* Used in the Usage Dashboard to indicate that the user should start
|
||||
* using Maestro to generate stats.
|
||||
*
|
||||
* Features:
|
||||
* - Theme-aware styling with inline styles
|
||||
* - Subtle chart illustration/icon
|
||||
* - Friendly, encouraging message
|
||||
* - Reusable component with customizable message
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
import type { Theme } from '../../types';
|
||||
|
||||
interface EmptyStateProps {
|
||||
/** Current theme for styling */
|
||||
theme: Theme;
|
||||
/** Optional custom title (default: "No usage data yet") */
|
||||
title?: string;
|
||||
/** Optional custom message (default: "Start using Maestro to see your stats!") */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
theme,
|
||||
title = 'No usage data yet',
|
||||
message = 'Start using Maestro to see your stats!',
|
||||
}: EmptyStateProps) {
|
||||
return (
|
||||
<div
|
||||
className="h-full flex flex-col items-center justify-center gap-4"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
data-testid="usage-dashboard-empty"
|
||||
>
|
||||
{/* Subtle chart illustration */}
|
||||
<div
|
||||
className="relative"
|
||||
style={{ opacity: 0.3 }}
|
||||
>
|
||||
{/* Main chart icon */}
|
||||
<BarChart3 className="w-16 h-16" />
|
||||
|
||||
{/* Decorative subtle bars for visual interest */}
|
||||
<svg
|
||||
className="absolute -bottom-1 -right-2 w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
style={{ opacity: 0.5 }}
|
||||
>
|
||||
<rect
|
||||
x="4"
|
||||
y="12"
|
||||
width="4"
|
||||
height="8"
|
||||
rx="1"
|
||||
fill={theme.colors.textDim}
|
||||
/>
|
||||
<rect
|
||||
x="10"
|
||||
y="8"
|
||||
width="4"
|
||||
height="12"
|
||||
rx="1"
|
||||
fill={theme.colors.textDim}
|
||||
/>
|
||||
<rect
|
||||
x="16"
|
||||
y="4"
|
||||
width="4"
|
||||
height="16"
|
||||
rx="1"
|
||||
fill={theme.colors.textDim}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Message text */}
|
||||
<div className="text-center">
|
||||
<p
|
||||
className="text-lg mb-2"
|
||||
style={{ color: theme.colors.textMain }}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmptyState;
|
||||
@@ -21,6 +21,7 @@ import { AgentComparisonChart } from './AgentComparisonChart';
|
||||
import { SourceDistributionChart } from './SourceDistributionChart';
|
||||
import { DurationTrendsChart } from './DurationTrendsChart';
|
||||
import { AutoRunStats } from './AutoRunStats';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import type { Theme } from '../../types';
|
||||
import { useLayerStack } from '../../contexts/LayerStackContext';
|
||||
import { MODAL_PRIORITIES } from '../../constants/modalPriorities';
|
||||
@@ -377,22 +378,8 @@ export function UsageDashboardModal({
|
||||
</button>
|
||||
</div>
|
||||
) : !data || (data.totalQueries === 0 && data.bySource.user === 0 && data.bySource.auto === 0) ? (
|
||||
/* Empty State */
|
||||
<div
|
||||
className="h-full flex flex-col items-center justify-center gap-4"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
data-testid="usage-dashboard-empty"
|
||||
>
|
||||
<BarChart3 className="w-16 h-16 opacity-30" />
|
||||
<div className="text-center">
|
||||
<p className="text-lg mb-2" style={{ color: theme.colors.textMain }}>
|
||||
No usage data yet
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Start using Maestro to see your stats!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
/* Empty State Component */
|
||||
<EmptyState theme={theme} />
|
||||
) : (
|
||||
<div className="space-y-6" data-testid="usage-dashboard-content">
|
||||
{/* View-specific content based on viewMode */}
|
||||
|
||||
Reference in New Issue
Block a user