FABRIC/src/tui/components/FileHeatmap.test.ts
jeda 9a015ffa42 fix: ESM/CommonJS blessed import compatibility
Changed all blessed imports from namespace import (import * as blessed)
to default import (import blessed from 'blessed') in 28 files.

This fixes "blessed.screen is not a function" runtime error that occurs
when ESM modules import CommonJS blessed package with namespace syntax.

Also includes WorkerAnalyticsPanel property additions for type compliance.

Fixes: bd-31x

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 23:47:19 +00:00

792 lines
25 KiB
TypeScript

/**
* Tests for FileHeatmap Component
*
* Tests the file heatmap display with mocked blessed elements.
* Tests heatmap calculation, color gradient rendering, and file path truncation.
*/
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import blessed from 'blessed';
// Mock the blessed module before importing FileHeatmap
vi.mock('blessed', () => {
// Create the mock box inside the factory
const mockBoxInstance = {
setContent: vi.fn(),
setLabel: vi.fn(),
focus: vi.fn(),
key: vi.fn(),
screen: {
render: vi.fn(),
},
};
const mockBox = vi.fn(() => mockBoxInstance);
return {
default: {
box: mockBox,
},
box: mockBox,
};
});
// Import after mocking
import { FileHeatmap } from './FileHeatmap.js';
import { FileHeatmapEntry, FileHeatmapStats, HeatmapOptions, HeatLevel } from '../../types.js';
// Helper to create mock FileHeatmapEntry
function createMockEntry(overrides: Partial<FileHeatmapEntry> = {}): FileHeatmapEntry {
return {
path: 'src/test.ts',
modifications: 5,
heatLevel: 'warm',
workers: [
{
workerId: 'w-test123',
modifications: 5,
lastModified: Date.now(),
percentage: 100,
},
],
firstModified: Date.now() - 60000,
lastModified: Date.now(),
hasCollision: false,
activeWorkers: 1,
avgModificationInterval: 12000,
...overrides,
};
}
// Helper to create mock FileHeatmapStats
function createMockStats(overrides: Partial<FileHeatmapStats> = {}): FileHeatmapStats {
return {
totalFiles: 10,
totalModifications: 50,
collisionFiles: 2,
activeFiles: 3,
heatDistribution: {
cold: 5,
warm: 3,
hot: 1,
critical: 1,
},
mostActiveDirectory: 'src/',
avgModificationsPerFile: 5,
...overrides,
};
}
// Helper to create mock screen
function createMockScreen() {
return {
render: vi.fn(),
append: vi.fn(),
key: vi.fn(),
destroy: vi.fn(),
} as unknown as blessed.Widgets.Screen;
}
describe('FileHeatmap', () => {
let fileHeatmap: FileHeatmap;
let mockScreen: blessed.Widgets.Screen;
let mockBoxInstance: any;
beforeEach(() => {
vi.clearAllMocks();
mockScreen = createMockScreen();
// Get the mock box instance from the mock
const blessedMock = blessed as unknown as { box: Mock };
mockBoxInstance = blessedMock.box();
fileHeatmap = new FileHeatmap({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
bottom: 0,
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe('constructor', () => {
it('should create a blessed box with correct options', () => {
const blessedMock = blessed as unknown as { box: Mock };
expect(blessedMock.box).toHaveBeenCalledWith(
expect.objectContaining({
parent: mockScreen,
top: 0,
left: 0,
width: '50%',
bottom: 0,
label: ' File Heatmap ',
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true,
mouse: true,
})
);
});
it('should bind key handlers on construction', () => {
// Key bindings should be registered
expect(mockBoxInstance.key).toHaveBeenCalled();
});
});
describe('updateData', () => {
it('should update entries and render', () => {
const entries = [
createMockEntry({ path: 'src/a.ts', modifications: 10 }),
createMockEntry({ path: 'src/b.ts', modifications: 5 }),
];
const stats = createMockStats();
const getHeatmap = () => entries;
const getStats = () => stats;
fileHeatmap.updateData(getHeatmap, getStats);
expect(mockBoxInstance.setContent).toHaveBeenCalled();
expect(mockBoxInstance.screen.render).toHaveBeenCalled();
});
it('should show "No file modifications detected" when empty', () => {
const getHeatmap = () => [];
const getStats = () => createMockStats({ totalFiles: 0 });
fileHeatmap.updateData(getHeatmap, getStats);
expect(mockBoxInstance.setContent).toHaveBeenCalledWith(
expect.stringContaining('No file modifications detected')
);
});
it('should display statistics header', () => {
const entries = [createMockEntry()];
const stats = createMockStats({
totalFiles: 10,
totalModifications: 50,
activeFiles: 3,
collisionFiles: 2,
});
const getHeatmap = () => entries;
const getStats = () => stats;
fileHeatmap.updateData(getHeatmap, getStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('Files: 10');
expect(content).toContain('Mods: 50');
expect(content).toContain('Active: 3');
expect(content).toContain('⚠ 2');
});
it('should reset selected index if out of bounds', () => {
// First set some entries
const firstEntries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
createMockEntry({ path: 'c.ts' }),
];
fileHeatmap.updateData(() => firstEntries, createMockStats);
// Update to fewer entries
const secondEntries = [createMockEntry({ path: 'a.ts' })];
fileHeatmap.updateData(() => secondEntries, createMockStats);
// Should not throw and selection should be valid
const selected = fileHeatmap.getSelected();
expect(selected).toBeDefined();
expect(selected?.path).toBe('a.ts');
});
});
describe('file path truncation (formatPath)', () => {
it('should not truncate short paths', () => {
const entry = createMockEntry({ path: 'src/test.ts' });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('src/test.ts');
});
it('should truncate long paths while preserving filename', () => {
const longPath = 'src/very/long/directory/structure/that/should/be/truncated/file.ts';
const entry = createMockEntry({ path: longPath });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should contain filename
expect(content).toContain('file.ts');
// Should contain ellipsis for truncation
expect(content).toContain('...');
});
it('should handle paths with very long filenames', () => {
const longFilename = 'src/verylongfilenamethatexceedsmaximumlengthallowed.test.integration.spec.ts';
const entry = createMockEntry({ path: longFilename });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should still contain ellipsis
expect(content).toContain('...');
});
it('should preserve directory context when truncating', () => {
const path = 'src/components/deep/nested/structure/Component.tsx';
const entry = createMockEntry({ path });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should show both start of path and filename with ellipsis
expect(content).toContain('...');
expect(content).toContain('Component.tsx');
});
});
describe('color gradient rendering (getHeatBar)', () => {
it('should render heat bars for cold level', () => {
const entry = createMockEntry({ heatLevel: 'cold', modifications: 2 });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should contain heat bar characters
expect(content).toContain('█');
expect(content).toContain('░');
// Should use blue color for cold
expect(content).toContain('blue-fg');
});
it('should render heat bars for warm level', () => {
const entry = createMockEntry({ heatLevel: 'warm', modifications: 8 });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('█');
expect(content).toContain('░');
// Should use yellow color for warm
expect(content).toContain('yellow-fg');
});
it('should render heat bars for hot level', () => {
const entry = createMockEntry({ heatLevel: 'hot', modifications: 15 });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('█');
expect(content).toContain('░');
// Should use magenta color for hot
expect(content).toContain('magenta-fg');
});
it('should render heat bars for critical level', () => {
const entry = createMockEntry({ heatLevel: 'critical', modifications: 30 });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('█');
// Note: Critical level might have all filled bars, so we just check for bar character
// Should use red color for critical
expect(content).toContain('red-fg');
});
it('should scale heat bar based on modification count', () => {
const lowModEntry = createMockEntry({ heatLevel: 'warm', modifications: 2 });
const highModEntry = createMockEntry({ heatLevel: 'warm', modifications: 20 });
// Test low modifications
fileHeatmap.updateData(() => [lowModEntry], createMockStats);
const lowContent = mockBoxInstance.setContent.mock.calls[0][0];
// Test high modifications
fileHeatmap.updateData(() => [highModEntry], createMockStats);
const highContent = mockBoxInstance.setContent.mock.calls[1][0];
// Both should have heat bars, but we can verify they're present
expect(lowContent).toContain('█');
expect(highContent).toContain('█');
});
});
describe('heatmap calculation from events', () => {
it('should display modification counts correctly', () => {
const entries = [
createMockEntry({ path: 'a.ts', modifications: 5 }),
createMockEntry({ path: 'b.ts', modifications: 15 }),
createMockEntry({ path: 'c.ts', modifications: 100 }),
];
fileHeatmap.updateData(() => entries, createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should show modification counts padded to 3 chars (with bold formatting)
expect(content).toContain('{bold} 5{/}');
expect(content).toContain('{bold} 15{/}');
expect(content).toContain('{bold}100{/}');
});
it('should show worker information', () => {
const entry = createMockEntry({
workers: [
{
workerId: 'w-abc123',
modifications: 5,
lastModified: Date.now(),
percentage: 100,
},
],
});
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should show truncated worker ID
expect(content).toContain('w-abc123');
});
it('should show multiple workers when present', () => {
const entry = createMockEntry({
workers: [
{
workerId: 'w-worker1',
modifications: 3,
lastModified: Date.now(),
percentage: 60,
},
{
workerId: 'w-worker2',
modifications: 2,
lastModified: Date.now(),
percentage: 40,
},
],
});
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should show worker information
expect(content).toContain('w-work');
});
it('should show collision indicator for files with collisions', () => {
const entry = createMockEntry({ hasCollision: true });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('⚠');
});
it('should show active workers indicator', () => {
const entry = createMockEntry({ activeWorkers: 2, hasCollision: false });
fileHeatmap.updateData(() => [entry], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('⚡');
});
it('should not show collision indicator when no collision', () => {
const entry = createMockEntry({ hasCollision: false, activeWorkers: 1 });
const stats = createMockStats({ collisionFiles: 0 }); // No collision files in stats
fileHeatmap.updateData(() => [entry], () => stats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Split by lines and check the entry row (not the stats header)
const lines = content.split('\n');
const entryLine = lines.find((line: string) => line.includes('src/test.ts'));
expect(entryLine).toBeDefined();
expect(entryLine).not.toContain('⚠');
expect(entryLine).not.toContain('⚡');
});
});
describe('selectNext and selectPrevious', () => {
it('should move to next entry', () => {
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
createMockEntry({ path: 'c.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
// Initially selected is first entry
expect(fileHeatmap.getSelected()?.path).toBe('a.ts');
fileHeatmap.selectNext();
expect(fileHeatmap.getSelected()?.path).toBe('b.ts');
});
it('should wrap to first entry when at end', () => {
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
// Move to last
fileHeatmap.selectNext();
expect(fileHeatmap.getSelected()?.path).toBe('b.ts');
// Wrap to first
fileHeatmap.selectNext();
expect(fileHeatmap.getSelected()?.path).toBe('a.ts');
});
it('should move to previous entry', () => {
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
createMockEntry({ path: 'c.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
// Move to second
fileHeatmap.selectNext();
expect(fileHeatmap.getSelected()?.path).toBe('b.ts');
// Move back to first
fileHeatmap.selectPrevious();
expect(fileHeatmap.getSelected()?.path).toBe('a.ts');
});
it('should wrap to last entry when at beginning', () => {
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
// At first, wrap to last
fileHeatmap.selectPrevious();
expect(fileHeatmap.getSelected()?.path).toBe('b.ts');
});
it('should do nothing when no entries', () => {
fileHeatmap.updateData(() => [], createMockStats);
// Should not throw
expect(() => fileHeatmap.selectNext()).not.toThrow();
expect(() => fileHeatmap.selectPrevious()).not.toThrow();
});
it('should show selection marker on selected entry', () => {
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
const firstContent = mockBoxInstance.setContent.mock.calls[0][0];
expect(firstContent).toContain('>'); // Selection marker
});
});
describe('sort mode cycling', () => {
it('should start with modifications sort mode', () => {
expect(fileHeatmap.getSortMode()).toBe('modifications');
});
it('should cycle through sort modes when s key is pressed', () => {
// Find the 's' key handler
const sCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('s')
);
const sHandler = sCall?.[1];
expect(fileHeatmap.getSortMode()).toBe('modifications');
if (sHandler) {
// Clear previous calls
mockBoxInstance.setContent.mockClear();
sHandler();
expect(fileHeatmap.getSortMode()).toBe('recent');
sHandler();
expect(fileHeatmap.getSortMode()).toBe('workers');
sHandler();
expect(fileHeatmap.getSortMode()).toBe('collisions');
// Should wrap back to modifications
sHandler();
expect(fileHeatmap.getSortMode()).toBe('modifications');
}
});
});
describe('collision filter', () => {
it('should start with collision filter disabled', () => {
expect(fileHeatmap.getCollisionFilter()).toBe(false);
});
it('should toggle collision filter when c key is pressed', () => {
// Find the 'c' key handler
const cCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('c')
);
const cHandler = cCall?.[1];
expect(fileHeatmap.getCollisionFilter()).toBe(false);
if (cHandler) {
cHandler();
expect(fileHeatmap.getCollisionFilter()).toBe(true);
cHandler();
expect(fileHeatmap.getCollisionFilter()).toBe(false);
}
});
it('should update label when collision filter is enabled', () => {
const cCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('c')
);
const cHandler = cCall?.[1];
fileHeatmap.updateData(() => [], createMockStats);
if (cHandler) {
cHandler();
expect(mockBoxInstance.setLabel).toHaveBeenCalledWith(' File Heatmap [COLLISIONS] ');
}
});
it('should show help text when collision filter is enabled and no entries', () => {
const cCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('c')
);
const cHandler = cCall?.[1];
if (cHandler) {
cHandler();
fileHeatmap.updateData(() => [], createMockStats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('Press [c] to show all files');
}
});
});
describe('setFilter and clearFilter', () => {
it('should set directory filter', () => {
fileHeatmap.setFilter('src/');
fileHeatmap.updateData(() => [], createMockStats);
// Should trigger render
expect(mockBoxInstance.setContent).toHaveBeenCalled();
});
it('should clear filter and collision-only mode', () => {
// Enable collision filter
const cCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('c')
);
const cHandler = cCall?.[1];
if (cHandler) {
cHandler();
}
fileHeatmap.setFilter('src/');
expect(fileHeatmap.getCollisionFilter()).toBe(true);
fileHeatmap.clearFilter();
expect(fileHeatmap.getCollisionFilter()).toBe(false);
});
});
describe('getSelected', () => {
it('should return currently selected entry', () => {
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
expect(fileHeatmap.getSelected()?.path).toBe('a.ts');
fileHeatmap.selectNext();
expect(fileHeatmap.getSelected()?.path).toBe('b.ts');
});
it('should return undefined when no entries', () => {
fileHeatmap.updateData(() => [], createMockStats);
expect(fileHeatmap.getSelected()).toBeUndefined();
});
});
describe('focus', () => {
it('should focus the box element', () => {
fileHeatmap.focus();
expect(mockBoxInstance.focus).toHaveBeenCalled();
});
});
describe('getElement', () => {
it('should return the box element', () => {
const element = fileHeatmap.getElement();
expect(element).toBe(mockBoxInstance);
});
});
describe('key bindings', () => {
it('should bind up and k keys to selectPrevious', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['up', 'k'], expect.any(Function));
});
it('should bind down and j keys to selectNext', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['down', 'j'], expect.any(Function));
});
it('should bind g key to select first', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['g'], expect.any(Function));
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
createMockEntry({ path: 'c.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
// Move to last entry
fileHeatmap.selectNext();
fileHeatmap.selectNext();
expect(fileHeatmap.getSelected()?.path).toBe('c.ts');
// Find the 'g' handler and call it
const gCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('g')
);
const gHandler = gCall?.[1];
if (gHandler) {
gHandler();
}
expect(fileHeatmap.getSelected()?.path).toBe('a.ts');
});
it('should bind G (shift+g) key to select last', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['G'], expect.any(Function));
const entries = [
createMockEntry({ path: 'a.ts' }),
createMockEntry({ path: 'b.ts' }),
createMockEntry({ path: 'c.ts' }),
];
fileHeatmap.updateData(() => entries, createMockStats);
// Initially at first
expect(fileHeatmap.getSelected()?.path).toBe('a.ts');
// Find the 'G' handler and call it
const GCall = mockBoxInstance.key.mock.calls.find(
(call: unknown[]) => Array.isArray(call?.[0]) && call[0].includes('G')
);
const GHandler = GCall?.[1];
if (GHandler) {
GHandler();
}
expect(fileHeatmap.getSelected()?.path).toBe('c.ts');
});
it('should bind s key to cycle sort mode', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['s'], expect.any(Function));
});
it('should bind c key to toggle collision filter', () => {
expect(mockBoxInstance.key).toHaveBeenCalledWith(['c'], expect.any(Function));
});
});
describe('heat distribution display', () => {
it('should show heat distribution in stats', () => {
const stats = createMockStats({
heatDistribution: {
cold: 5,
warm: 3,
hot: 1,
critical: 1,
},
});
fileHeatmap.updateData(() => [], () => stats);
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('○5'); // cold
expect(content).toContain('◐3'); // warm
expect(content).toContain('●1'); // hot
expect(content).toContain('🔥1'); // critical
});
});
describe('edge cases', () => {
it('should handle entries with no workers', () => {
const entry = createMockEntry({ workers: [] });
// Should not throw
expect(() => fileHeatmap.updateData(() => [entry], createMockStats)).not.toThrow();
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should show dash for no workers
expect(content).toContain('-');
});
it('should handle entries with many workers', () => {
const entry = createMockEntry({
workers: [
{ workerId: 'w-1', modifications: 5, lastModified: Date.now(), percentage: 40 },
{ workerId: 'w-2', modifications: 4, lastModified: Date.now(), percentage: 30 },
{ workerId: 'w-3', modifications: 3, lastModified: Date.now(), percentage: 20 },
{ workerId: 'w-4', modifications: 2, lastModified: Date.now(), percentage: 10 },
],
});
// Should not throw
expect(() => fileHeatmap.updateData(() => [entry], createMockStats)).not.toThrow();
const content = mockBoxInstance.setContent.mock.calls[0][0];
// Should show top workers with count
expect(content).toContain('+2'); // +2 more workers
});
it('should handle very short paths', () => {
const entry = createMockEntry({ path: 'a.ts' });
expect(() => fileHeatmap.updateData(() => [entry], createMockStats)).not.toThrow();
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('a.ts');
});
it('should handle paths with no directory component', () => {
const entry = createMockEntry({ path: 'file.ts' });
expect(() => fileHeatmap.updateData(() => [entry], createMockStats)).not.toThrow();
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain('file.ts');
});
it('should handle zero modifications gracefully', () => {
const entry = createMockEntry({ modifications: 0, heatLevel: 'cold' });
expect(() => fileHeatmap.updateData(() => [entry], createMockStats)).not.toThrow();
const content = mockBoxInstance.setContent.mock.calls[0][0];
expect(content).toContain(' 0'); // Padded to 3 chars
});
});
});