feat(heatmap): add timelapse animation CSS and tests
Add comprehensive CSS styling for the heatmap timelapse animation controls, including: - Playback controls (play/pause, stop buttons) - Speed controls with slower/faster buttons - Timeline slider for scrubbing through snapshots - Loop toggle checkbox - Timeline labels showing current time, progress, and duration Also add 10 new tests covering the timelapse feature: - View mode switching - Data fetching - UI controls rendering - Loading states - Error handling The timelapse feature was already implemented in the React component and backend, but was missing CSS styling and tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6e3049dc1a
commit
cd96596400
2 changed files with 697 additions and 0 deletions
|
|
@ -187,6 +187,52 @@ body {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* CLI Filter Indicator */
|
||||
.cli-filter-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.cli-filter-indicator .filter-label {
|
||||
font-weight: 600;
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.cli-filter-indicator .filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cli-filter-indicator .filter-item .filter-key {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.cli-filter-indicator .filter-item .filter-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.cli-filter-indicator .filter-item.worker {
|
||||
border-left: 2px solid var(--info);
|
||||
}
|
||||
|
||||
.cli-filter-indicator .filter-item.level {
|
||||
border-left: 2px solid var(--warning);
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -2167,6 +2213,225 @@ body {
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
File Heatmap Treemap Styles
|
||||
============================================ */
|
||||
|
||||
.heatmap-treemap-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 250px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heatmap-treemap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.treemap-node {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.treemap-node:hover rect {
|
||||
fill-opacity: 0.9;
|
||||
stroke-width: 0.4;
|
||||
}
|
||||
|
||||
.treemap-node.selected rect {
|
||||
stroke: var(--accent);
|
||||
stroke-width: 0.5;
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.treemap-node.collision rect {
|
||||
stroke: var(--warning);
|
||||
stroke-width: 0.5;
|
||||
animation: treemap-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes treemap-pulse {
|
||||
0%, 100% {
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
50% {
|
||||
stroke-opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.treemap-tooltip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
min-width: 150px;
|
||||
max-width: 250px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
transform: translate(-50%, -100%);
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.treemap-tooltip .tooltip-header {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.375rem;
|
||||
padding-bottom: 0.375rem;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.6875rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.treemap-tooltip .tooltip-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin: 0.125rem 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.treemap-tooltip .tooltip-row strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.treemap-tooltip .tooltip-row.collision {
|
||||
color: var(--warning);
|
||||
font-weight: 500;
|
||||
margin-top: 0.375rem;
|
||||
padding-top: 0.25rem;
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
File Heatmap Timelapse Controls
|
||||
============================================ */
|
||||
|
||||
.timelapse-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.timelapse-playback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.timelapse-speed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.timelapse-speed .speed-label {
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
}
|
||||
|
||||
.timelapse-loop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.timelapse-loop input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.timelapse-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.timeline-slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.timeline-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
background: var(--accent-hover, var(--accent));
|
||||
}
|
||||
|
||||
.timeline-slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.timeline-slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
background: var(--accent-hover, var(--accent));
|
||||
}
|
||||
|
||||
.timeline-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.timeline-progress {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.timeline-duration {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for */
|
||||
@media (max-width: 768px) {
|
||||
.file-heatmap-panel {
|
||||
|
|
|
|||
|
|
@ -294,4 +294,436 @@ describe('FileHeatmap Component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Treemap view', () => {
|
||||
it('should have view mode toggle buttons', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const listButton = screen.getByRole('button', { name: /list/i });
|
||||
const treemapButton = screen.getByRole('button', { name: /treemap/i });
|
||||
expect(listButton).toBeTruthy();
|
||||
expect(treemapButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should switch to treemap view when treemap button clicked', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const treemapButton = screen.getByRole('button', { name: /treemap/i });
|
||||
fireEvent.click(treemapButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const treemapContainer = document.querySelector('.heatmap-treemap-container');
|
||||
expect(treemapContainer).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render treemap nodes', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const treemapButton = screen.getByRole('button', { name: /treemap/i });
|
||||
fireEvent.click(treemapButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const treemapNodes = document.querySelectorAll('.treemap-node');
|
||||
expect(treemapNodes.length).toBe(mockEntries.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide sort button in treemap mode', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Sort button should be visible in list mode
|
||||
const sortButton = screen.getByRole('button', { name: /sort.*modifications/i });
|
||||
expect(sortButton).toBeTruthy();
|
||||
|
||||
const treemapButton = screen.getByRole('button', { name: /treemap/i });
|
||||
fireEvent.click(treemapButton);
|
||||
|
||||
// Sort button should be hidden in treemap mode
|
||||
await waitFor(() => {
|
||||
const sortButtonAfter = document.querySelector('button[title="Cycle sort mode"]');
|
||||
expect(sortButtonAfter).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show tooltip when hovering treemap node', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const treemapButton = screen.getByRole('button', { name: /treemap/i });
|
||||
fireEvent.click(treemapButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const treemapNodes = document.querySelectorAll('.treemap-node');
|
||||
expect(treemapNodes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const firstNode = document.querySelector('.treemap-node');
|
||||
expect(firstNode).toBeTruthy();
|
||||
|
||||
// Trigger mouse enter
|
||||
fireEvent.mouseEnter(firstNode!);
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = document.querySelector('.treemap-tooltip');
|
||||
expect(tooltip).toBeTruthy();
|
||||
});
|
||||
|
||||
// Trigger mouse leave
|
||||
fireEvent.mouseLeave(firstNode!);
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = document.querySelector('.treemap-tooltip');
|
||||
expect(tooltip).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Timelapse animation', () => {
|
||||
const mockTimelapse = {
|
||||
startTimestamp: Date.now() - 100000,
|
||||
endTimestamp: Date.now(),
|
||||
interval: 2000,
|
||||
totalSnapshots: 5,
|
||||
snapshots: [
|
||||
{
|
||||
timestamp: Date.now() - 100000,
|
||||
entries: [
|
||||
{
|
||||
path: '/src/early.ts',
|
||||
modifications: 2,
|
||||
heatLevel: 'cold' as const,
|
||||
workers: [{ workerId: 'w-alpha', modifications: 2, lastModified: Date.now() - 90000, percentage: 100 }],
|
||||
firstModified: Date.now() - 100000,
|
||||
lastModified: Date.now() - 90000,
|
||||
hasCollision: false,
|
||||
activeWorkers: 1,
|
||||
avgModificationInterval: 5000,
|
||||
},
|
||||
],
|
||||
stats: {
|
||||
totalFiles: 1,
|
||||
totalModifications: 2,
|
||||
collisionFiles: 0,
|
||||
activeFiles: 1,
|
||||
heatDistribution: { cold: 1, warm: 0, hot: 0, critical: 0 },
|
||||
mostActiveDirectory: '/src',
|
||||
avgModificationsPerFile: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 50000,
|
||||
entries: [
|
||||
{
|
||||
path: '/src/early.ts',
|
||||
modifications: 5,
|
||||
heatLevel: 'warm' as const,
|
||||
workers: [{ workerId: 'w-alpha', modifications: 5, lastModified: Date.now() - 50000, percentage: 100 }],
|
||||
firstModified: Date.now() - 100000,
|
||||
lastModified: Date.now() - 50000,
|
||||
hasCollision: false,
|
||||
activeWorkers: 1,
|
||||
avgModificationInterval: 10000,
|
||||
},
|
||||
{
|
||||
path: '/src/mid.ts',
|
||||
modifications: 3,
|
||||
heatLevel: 'warm' as const,
|
||||
workers: [{ workerId: 'w-beta', modifications: 3, lastModified: Date.now() - 50000, percentage: 100 }],
|
||||
firstModified: Date.now() - 60000,
|
||||
lastModified: Date.now() - 50000,
|
||||
hasCollision: false,
|
||||
activeWorkers: 1,
|
||||
avgModificationInterval: 5000,
|
||||
},
|
||||
],
|
||||
stats: {
|
||||
totalFiles: 2,
|
||||
totalModifications: 8,
|
||||
collisionFiles: 0,
|
||||
activeFiles: 2,
|
||||
heatDistribution: { cold: 0, warm: 2, hot: 0, critical: 0 },
|
||||
mostActiveDirectory: '/src',
|
||||
avgModificationsPerFile: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('should have timelapse view mode button', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
expect(timelapseButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should switch to timelapse view when timelapse button clicked', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
// Should show timelapse controls after switching
|
||||
await waitFor(() => {
|
||||
const controls = document.querySelector('.timelapse-controls');
|
||||
expect(controls).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch timelapse data when entering timelapse mode', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
// Should show timelapse controls after data loads
|
||||
await waitFor(() => {
|
||||
const controls = document.querySelector('.timelapse-controls');
|
||||
expect(controls).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display timelapse playback controls when data loaded', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
const { container } = render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const playbackControls = container.querySelector('.timelapse-playback');
|
||||
expect(playbackControls).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have play/pause button in timelapse mode', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
const { container } = render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const playButton = container.querySelector('.timelapse-playback button.primary');
|
||||
expect(playButton).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have speed controls in timelapse mode', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
const { container } = render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const speedControls = container.querySelector('.timelapse-speed');
|
||||
expect(speedControls).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have timeline slider in timelapse mode', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
const { container } = render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const timelineSlider = container.querySelector('.timeline-slider');
|
||||
expect(timelineSlider).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have loop checkbox in timelapse mode', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
const { container } = render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const loopLabel = container.querySelector('.timelapse-loop');
|
||||
expect(loopLabel).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show timeline labels with time and progress', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockResolvedValueOnce(createMockResponse(mockTimelapse));
|
||||
|
||||
const { container } = render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const timelineLabels = container.querySelector('.timeline-labels');
|
||||
expect(timelineLabels).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display loading state while fetching timelapse data', async () => {
|
||||
let resolveFetch: (value: unknown) => void;
|
||||
const fetchPromise = new Promise(resolve => {
|
||||
resolveFetch = resolve;
|
||||
});
|
||||
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockReturnValueOnce(fetchPromise as any);
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/generating timelapse/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Resolve the fetch
|
||||
resolveFetch!(createMockResponse(mockTimelapse));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/generating timelapse/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error message on timelapse fetch failure', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(createMockResponse(mockEntries))
|
||||
.mockResolvedValueOnce(createMockResponse(mockStats))
|
||||
.mockRejectedValueOnce(new Error('Failed to fetch timelapse'));
|
||||
|
||||
render(<FileHeatmap visible={true} onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File Heatmap')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const timelapseButton = screen.getByRole('button', { name: /timelapse/i });
|
||||
fireEvent.click(timelapseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to fetch timelapse/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue