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>
792 lines
25 KiB
TypeScript
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
|
|
});
|
|
});
|
|
});
|