Just a whole lot of crap

This commit is contained in:
2025-08-14 16:36:12 -05:00
parent 66b7e42275
commit 62f6d6f279
144 changed files with 41421 additions and 2458 deletions

View File

@@ -0,0 +1,482 @@
const React = require("react");
const { render } = require("ink-testing-library");
const {
MemoryLeakDetector,
getGlobalDetector,
useMemoryLeakDetection,
MemoryLeakUtils,
} = require("../../../src/tui/utils/memoryLeakDetector.js");
/**
* Memory leak detection tests
* Requirements: 4.2, 4.5
*/
describe("MemoryLeakDetector", () => {
let detector;
beforeEach(() => {
detector = new MemoryLeakDetector({
checkInterval: 100, // Fast interval for testing
sampleSize: 5,
growthThreshold: 1024 * 1024, // 1MB
verbose: false,
});
});
afterEach(() => {
detector.stop();
});
describe("Basic Functionality", () => {
test("should start and stop monitoring", () => {
expect(detector.isMonitoring).toBe(false);
detector.start();
expect(detector.isMonitoring).toBe(true);
detector.stop();
expect(detector.isMonitoring).toBe(false);
});
test("should take memory samples", () => {
// Mock process.memoryUsage
const mockMemoryUsage = jest.fn(() => ({
heapUsed: 50 * 1024 * 1024,
heapTotal: 60 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 70 * 1024 * 1024,
}));
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
detector.takeSample();
expect(detector.samples).toHaveLength(1);
expect(detector.samples[0]).toMatchObject({
heapUsed: 50 * 1024 * 1024,
heapTotal: 60 * 1024 * 1024,
});
process.memoryUsage = originalMemoryUsage;
});
test("should limit sample size", () => {
const mockMemoryUsage = jest.fn(() => ({
heapUsed: Math.random() * 100 * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
}));
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
// Take more samples than the limit
for (let i = 0; i < 10; i++) {
detector.takeSample();
}
expect(detector.samples).toHaveLength(5); // Should be limited to sampleSize
process.memoryUsage = originalMemoryUsage;
});
});
describe("Component Registration", () => {
test("should register and unregister components", () => {
detector.registerComponent("TestComponent", 1);
expect(detector.componentRegistry.has("TestComponent")).toBe(true);
expect(detector.componentRegistry.get("TestComponent").instances).toBe(1);
detector.registerComponent("TestComponent", 1);
expect(detector.componentRegistry.get("TestComponent").instances).toBe(2);
detector.unregisterComponent("TestComponent");
expect(detector.componentRegistry.get("TestComponent").instances).toBe(1);
detector.unregisterComponent("TestComponent");
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
});
test("should detect suspicious components", () => {
detector.registerComponent("LeakyComponent", 1);
// Register many instances
for (let i = 0; i < 10; i++) {
detector.registerComponent("LeakyComponent", 1);
}
const suspicious = detector.getSuspiciousComponents();
expect(suspicious).toHaveLength(1);
expect(suspicious[0].name).toBe("LeakyComponent");
expect(suspicious[0].instances).toBe(11);
expect(suspicious[0].ratio).toBeGreaterThan(2);
});
});
describe("Leak Detection", () => {
test("should detect steady memory growth", () => {
const mockMemoryUsage = jest.fn();
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
// Simulate steady growth
const baseMemory = 50 * 1024 * 1024;
const growthPerSample = 5 * 1024 * 1024;
for (let i = 0; i < 5; i++) {
mockMemoryUsage.mockReturnValue({
heapUsed: baseMemory + i * growthPerSample,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
});
detector.takeSample();
}
const analysis = detector.analyzeLeaks();
expect(analysis.hasLeak).toBe(true);
expect(analysis.analysis.steadyGrowth).toBe(true);
process.memoryUsage = originalMemoryUsage;
});
test("should detect rapid memory growth", () => {
const mockMemoryUsage = jest.fn();
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
// Simulate rapid growth
mockMemoryUsage.mockReturnValueOnce({
heapUsed: 50 * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
});
detector.takeSample();
// Wait a bit then add large growth
setTimeout(() => {
mockMemoryUsage.mockReturnValueOnce({
heapUsed: 60 * 1024 * 1024, // 10MB growth
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 130 * 1024 * 1024,
});
detector.takeSample();
mockMemoryUsage.mockReturnValueOnce({
heapUsed: 70 * 1024 * 1024, // Another 10MB growth
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 140 * 1024 * 1024,
});
detector.takeSample();
const analysis = detector.analyzeLeaks();
expect(analysis.hasLeak).toBe(true);
expect(analysis.analysis.rapidGrowth).toBe(true);
process.memoryUsage = originalMemoryUsage;
}, 10);
});
test("should generate appropriate recommendations", () => {
const mockMemoryUsage = jest.fn();
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
// Simulate component leak
detector.registerComponent("LeakyComponent", 1);
for (let i = 0; i < 5; i++) {
detector.registerComponent("LeakyComponent", 1);
}
// Simulate memory growth
for (let i = 0; i < 3; i++) {
mockMemoryUsage.mockReturnValue({
heapUsed: 50 * 1024 * 1024 + i * 2 * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
});
detector.takeSample();
}
const analysis = detector.analyzeLeaks();
expect(analysis.analysis.recommendations).toBeDefined();
expect(analysis.analysis.recommendations.length).toBeGreaterThan(0);
const hasComponentRecommendation = analysis.analysis.recommendations.some(
(rec) => rec.type === "component-leak"
);
expect(hasComponentRecommendation).toBe(true);
process.memoryUsage = originalMemoryUsage;
});
});
describe("Event Listeners", () => {
test("should notify listeners of events", () => {
const listener = jest.fn();
detector.addListener(listener);
const mockMemoryUsage = jest.fn(() => ({
heapUsed: 50 * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
}));
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
detector.takeSample();
expect(listener).toHaveBeenCalledWith("sample", expect.any(Object));
detector.removeListener(listener);
detector.takeSample();
expect(listener).toHaveBeenCalledTimes(1); // Should not be called again
process.memoryUsage = originalMemoryUsage;
});
test("should handle listener errors gracefully", () => {
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
const badListener = jest.fn(() => {
throw new Error("Listener error");
});
const goodListener = jest.fn();
detector.addListener(badListener);
detector.addListener(goodListener);
const mockMemoryUsage = jest.fn(() => ({
heapUsed: 50 * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
}));
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
detector.takeSample();
expect(badListener).toHaveBeenCalled();
expect(goodListener).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
"[MemoryLeakDetector] Error in listener:",
expect.any(Error)
);
consoleSpy.mockRestore();
process.memoryUsage = originalMemoryUsage;
});
});
describe("Statistics and Reporting", () => {
test("should provide memory statistics", () => {
const mockMemoryUsage = jest.fn();
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
// Take a few samples
for (let i = 0; i < 3; i++) {
mockMemoryUsage.mockReturnValue({
heapUsed: 50 * 1024 * 1024 + i * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
});
detector.takeSample();
}
const stats = detector.getStatistics();
expect(stats).toBeDefined();
expect(stats.current).toBeDefined();
expect(stats.growth).toBeDefined();
expect(stats.trend).toBeDefined();
expect(stats.samples).toBe(3);
process.memoryUsage = originalMemoryUsage;
});
test("should generate comprehensive report", () => {
detector.registerComponent("TestComponent", 1);
const mockMemoryUsage = jest.fn(() => ({
heapUsed: 50 * 1024 * 1024,
heapTotal: 100 * 1024 * 1024,
external: 5 * 1024 * 1024,
rss: 120 * 1024 * 1024,
}));
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = mockMemoryUsage;
detector.takeSample();
const report = detector.generateReport();
expect(report).toMatchObject({
timestamp: expect.any(Number),
monitoring: expect.any(Boolean),
statistics: expect.any(Object),
components: expect.any(Array),
recommendations: expect.any(Array),
});
expect(report.components).toHaveLength(1);
expect(report.components[0].name).toBe("TestComponent");
process.memoryUsage = originalMemoryUsage;
});
});
});
describe("useMemoryLeakDetection Hook", () => {
test("should register and unregister component on mount/unmount", () => {
const TestComponent = () => {
useMemoryLeakDetection("TestComponent");
return React.createElement("div", null, "Test");
};
const detector = getGlobalDetector();
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
const { unmount } = render(React.createElement(TestComponent));
expect(detector.componentRegistry.has("TestComponent")).toBe(true);
unmount();
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
});
test("should provide detector utilities", () => {
let detectorUtils = {};
const TestComponent = () => {
detectorUtils = useMemoryLeakDetection("TestComponent");
return React.createElement("div", null, "Test");
};
render(React.createElement(TestComponent));
expect(typeof detectorUtils.detector).toBe("object");
expect(typeof detectorUtils.forceGC).toBe("function");
expect(typeof detectorUtils.getReport).toBe("function");
expect(typeof detectorUtils.getStats).toBe("function");
});
});
describe("MemoryLeakUtils", () => {
describe("checkObjectForLeaks", () => {
test("should detect circular references", () => {
const obj = { name: "test" };
obj.self = obj; // Create circular reference
const issues = MemoryLeakUtils.checkObjectForLeaks(obj, "testObj");
const circularIssue = issues.find(
(issue) => issue.type === "circular-reference"
);
expect(circularIssue).toBeDefined();
expect(circularIssue.message).toContain("Circular reference");
});
test("should detect large arrays", () => {
const largeArray = new Array(15000).fill("item");
const issues = MemoryLeakUtils.checkObjectForLeaks(
largeArray,
"largeArray"
);
const arrayIssue = issues.find((issue) => issue.type === "large-array");
expect(arrayIssue).toBeDefined();
expect(arrayIssue.length).toBe(15000);
});
test("should detect objects with many properties", () => {
const obj = {};
for (let i = 0; i < 1500; i++) {
obj[`prop${i}`] = i;
}
const issues = MemoryLeakUtils.checkObjectForLeaks(obj, "manyPropsObj");
const propsIssue = issues.find(
(issue) => issue.type === "many-properties"
);
expect(propsIssue).toBeDefined();
expect(propsIssue.count).toBe(1500);
});
test("should handle null and undefined objects", () => {
expect(MemoryLeakUtils.checkObjectForLeaks(null)).toEqual([]);
expect(MemoryLeakUtils.checkObjectForLeaks(undefined)).toEqual([]);
});
});
describe("checkDOMNodeForLeaks", () => {
test("should detect excessive event listeners", () => {
const mockNode = {
_events: {},
};
// Add many event listeners
for (let i = 0; i < 60; i++) {
mockNode._events[`event${i}`] = () => {};
}
const issues = MemoryLeakUtils.checkDOMNodeForLeaks(mockNode);
const listenerIssue = issues.find(
(issue) => issue.type === "excessive-listeners"
);
expect(listenerIssue).toBeDefined();
expect(listenerIssue.count).toBe(60);
});
test("should detect detached DOM nodes", () => {
const mockNode = {
parentNode: null,
};
const issues = MemoryLeakUtils.checkDOMNodeForLeaks(mockNode);
const detachedIssue = issues.find(
(issue) => issue.type === "detached-node"
);
expect(detachedIssue).toBeDefined();
});
test("should handle invalid nodes", () => {
expect(MemoryLeakUtils.checkDOMNodeForLeaks(null)).toEqual([]);
expect(MemoryLeakUtils.checkDOMNodeForLeaks("not an object")).toEqual([]);
});
});
});
describe("Global Detector", () => {
test("should return the same instance", () => {
const detector1 = getGlobalDetector();
const detector2 = getGlobalDetector();
expect(detector1).toBe(detector2);
});
test("should accept options on first call", () => {
// Reset global detector
const MemoryLeakDetectorModule = require("../../../src/tui/utils/memoryLeakDetector.js");
MemoryLeakDetectorModule.globalDetector = null;
const detector = getGlobalDetector({
checkInterval: 5000,
verbose: true,
});
expect(detector.options.checkInterval).toBe(5000);
expect(detector.options.verbose).toBe(true);
});
});

View File

@@ -0,0 +1,526 @@
const React = require("react");
const { render } = require("ink-testing-library");
const {
useEventListener,
useInterval,
useTimeout,
useAsyncOperation,
useMemoryMonitor,
useWeakRef,
useCleanup,
useResourcePool,
} = require("../../../src/tui/hooks/useMemoryManagement.js");
const {
withMemoryManagement,
MemoryOptimizedContainer,
MemoryEfficientList,
AutoCleanupComponent,
} = require("../../../src/tui/components/common/MemoryOptimizedComponent.jsx");
/**
* Memory management tests for TUI components
* Requirements: 4.2, 4.5
*/
describe("Memory Management Hooks", () => {
describe("useCleanup", () => {
test("should execute cleanup functions on unmount", () => {
const cleanupFn1 = jest.fn();
const cleanupFn2 = jest.fn();
const TestComponent = () => {
const { addCleanup } = useCleanup();
React.useEffect(() => {
addCleanup(cleanupFn1);
addCleanup(cleanupFn2);
}, [addCleanup]);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
expect(cleanupFn1).not.toHaveBeenCalled();
expect(cleanupFn2).not.toHaveBeenCalled();
unmount();
expect(cleanupFn1).toHaveBeenCalledTimes(1);
expect(cleanupFn2).toHaveBeenCalledTimes(1);
});
test("should handle cleanup function errors gracefully", () => {
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
const goodCleanup = jest.fn();
const badCleanup = jest.fn(() => {
throw new Error("Cleanup error");
});
const TestComponent = () => {
const { addCleanup } = useCleanup();
React.useEffect(() => {
addCleanup(badCleanup);
addCleanup(goodCleanup);
}, [addCleanup]);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
unmount();
expect(badCleanup).toHaveBeenCalled();
expect(goodCleanup).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
"Error during cleanup:",
expect.any(Error)
);
consoleSpy.mockRestore();
});
});
describe("useAsyncOperation", () => {
test("should cancel operations on unmount", async () => {
let operationCancelled = false;
const asyncOperation = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (operationCancelled) {
reject(new Error("Operation cancelled"));
} else {
resolve("success");
}
}, 100);
});
const TestComponent = () => {
const { executeAsync, cancelAllOperations } = useAsyncOperation();
React.useEffect(() => {
executeAsync(asyncOperation).catch((error) => {
if (error.message === "Operation cancelled") {
operationCancelled = true;
}
});
}, [executeAsync]);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
// Unmount before operation completes
setTimeout(() => unmount(), 50);
// Wait for operation to complete or be cancelled
await new Promise((resolve) => setTimeout(resolve, 150));
expect(operationCancelled).toBe(true);
});
test("should not execute callbacks after unmount", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const TestComponent = () => {
const { executeAsync } = useAsyncOperation();
React.useEffect(() => {
const asyncOp = () => Promise.resolve("success");
executeAsync(asyncOp, onSuccess, onError);
}, [executeAsync]);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
// Unmount immediately
unmount();
// Wait for async operation
await new Promise((resolve) => setTimeout(resolve, 50));
expect(onSuccess).not.toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();
});
});
describe("useInterval", () => {
test("should clear interval on unmount", () => {
jest.useFakeTimers();
const callback = jest.fn();
const TestComponent = () => {
useInterval(callback, 1000);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
// Fast-forward time
jest.advanceTimersByTime(2000);
expect(callback).toHaveBeenCalledTimes(2);
unmount();
// Fast-forward more time after unmount
jest.advanceTimersByTime(2000);
expect(callback).toHaveBeenCalledTimes(2); // Should not increase
jest.useRealTimers();
});
test("should provide manual control over interval", () => {
jest.useFakeTimers();
const callback = jest.fn();
const TestComponent = () => {
const { start, stop, restart } = useInterval(callback, 1000);
React.useEffect(() => {
// Test manual control
setTimeout(() => stop(), 1500);
setTimeout(() => start(), 2500);
setTimeout(() => restart(), 3500);
}, [start, stop, restart]);
return React.createElement("div", null, "Test");
};
render(React.createElement(TestComponent));
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(1000); // 2000ms total, stopped at 1500ms
expect(callback).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(1000); // 3000ms total, restarted at 2500ms
expect(callback).toHaveBeenCalledTimes(2);
jest.useRealTimers();
});
});
describe("useTimeout", () => {
test("should clear timeout on unmount", () => {
jest.useFakeTimers();
const callback = jest.fn();
const TestComponent = () => {
useTimeout(callback, 1000);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
// Unmount before timeout
unmount();
// Fast-forward past timeout
jest.advanceTimersByTime(1500);
expect(callback).not.toHaveBeenCalled();
jest.useRealTimers();
});
});
describe("useMemoryMonitor", () => {
test("should track render count", () => {
let renderCount = 0;
const TestComponent = ({ value }) => {
const { renderCount: currentRenderCount } =
useMemoryMonitor("TestComponent");
renderCount = currentRenderCount;
return React.createElement("div", null, value);
};
const { rerender } = render(
React.createElement(TestComponent, { value: 1 })
);
expect(renderCount).toBe(1);
rerender(React.createElement(TestComponent, { value: 2 }));
expect(renderCount).toBe(2);
rerender(React.createElement(TestComponent, { value: 3 }));
expect(renderCount).toBe(3);
});
test("should provide memory statistics", () => {
let memoryStats = null;
const TestComponent = () => {
const { getMemoryStats } = useMemoryMonitor("TestComponent");
React.useEffect(() => {
// Simulate some memory usage
setTimeout(() => {
memoryStats = getMemoryStats();
}, 100);
}, [getMemoryStats]);
return React.createElement("div", null, "Test");
};
render(React.createElement(TestComponent));
return new Promise((resolve) => {
setTimeout(() => {
// Memory stats might be null in test environment
// but the function should exist
expect(typeof memoryStats).toBeDefined();
resolve();
}, 150);
});
});
});
describe("useWeakRef", () => {
test("should store and retrieve values using weak references", () => {
let getValue, setValue;
const TestComponent = () => {
[getValue, setValue] = useWeakRef();
return React.createElement("div", null, "Test");
};
render(React.createElement(TestComponent));
const testObject = { data: "test" };
setValue(testObject);
expect(getValue()).toBe(testObject);
setValue(null);
expect(getValue()).toBe(null);
});
});
describe("useResourcePool", () => {
test("should manage resource pool efficiently", () => {
let resourcePool;
const createResource = jest.fn(() => ({ id: Math.random() }));
const resetResource = jest.fn();
const TestComponent = () => {
resourcePool = useResourcePool(createResource, resetResource, 3);
return React.createElement("div", null, "Test");
};
render(React.createElement(TestComponent));
// Acquire resources
const resource1 = resourcePool.acquire();
const resource2 = resourcePool.acquire();
expect(createResource).toHaveBeenCalledTimes(2);
expect(resourcePool.activeCount).toBe(2);
expect(resourcePool.poolSize).toBe(0);
// Release resources
resourcePool.release(resource1);
expect(resourcePool.activeCount).toBe(1);
expect(resourcePool.poolSize).toBe(1);
// Acquire again (should reuse)
const resource3 = resourcePool.acquire();
expect(createResource).toHaveBeenCalledTimes(2); // No new creation
expect(resetResource).toHaveBeenCalledTimes(1);
});
});
});
describe("Memory Optimized Components", () => {
describe("withMemoryManagement HOC", () => {
test("should provide memory management props to wrapped component", () => {
let receivedProps = {};
const TestComponent = (props) => {
receivedProps = props;
return React.createElement("div", null, "Test");
};
const MemoryManagedComponent = withMemoryManagement(TestComponent, {
componentName: "TestComponent",
});
render(
React.createElement(MemoryManagedComponent, { testProp: "value" })
);
expect(receivedProps.testProp).toBe("value");
expect(typeof receivedProps.addCleanup).toBe("function");
expect(typeof receivedProps.executeAsync).toBe("function");
expect(typeof receivedProps.getMemoryStats).toBe("function");
expect(typeof receivedProps.renderCount).toBe("number");
});
});
describe("MemoryOptimizedContainer", () => {
test("should display memory warnings when threshold is exceeded", () => {
// Mock process.memoryUsage to return high memory usage
const originalMemoryUsage = process.memoryUsage;
process.memoryUsage = jest.fn(() => ({
heapUsed: 200 * 1024 * 1024, // 200MB
heapTotal: 250 * 1024 * 1024,
external: 10 * 1024 * 1024,
rss: 300 * 1024 * 1024,
}));
const onMemoryWarning = jest.fn();
const { lastFrame } = render(
React.createElement(
MemoryOptimizedContainer,
{
memoryThreshold: 100 * 1024 * 1024, // 100MB threshold
memoryCheckInterval: 100, // Fast check for testing
onMemoryWarning,
},
"Test content"
)
);
// Wait for memory check
return new Promise((resolve) => {
setTimeout(() => {
const output = lastFrame();
expect(output).toContain("Memory Warning");
process.memoryUsage = originalMemoryUsage;
resolve();
}, 150);
});
});
});
describe("MemoryEfficientList", () => {
test("should limit cached items to prevent memory bloat", () => {
const items = Array.from({ length: 200 }, (_, i) => `Item ${i}`);
const renderItem = (item, index) =>
React.createElement("div", { key: index }, item);
const { lastFrame } = render(
React.createElement(MemoryEfficientList, {
items,
renderItem,
maxCachedItems: 50,
})
);
const output = lastFrame();
expect(output).toContain("Cached:");
expect(output).toContain("50"); // Should show cache limit
});
});
describe("AutoCleanupComponent", () => {
test("should cleanup old resources automatically", () => {
jest.useFakeTimers();
let resourceUtils = {};
const TestComponent = (utils) => {
resourceUtils = utils;
return React.createElement("div", null, "Test");
};
render(
React.createElement(
AutoCleanupComponent,
{
cleanupInterval: 1000,
maxAge: 2000,
},
TestComponent
)
);
// Add a resource
const mockResource = {
cleanup: jest.fn(),
};
resourceUtils.addResource("test", mockResource);
expect(resourceUtils.resourceCount).toBe(1);
// Fast-forward past maxAge
jest.advanceTimersByTime(3000);
expect(mockResource.cleanup).toHaveBeenCalled();
expect(resourceUtils.resourceCount).toBe(0);
jest.useRealTimers();
});
});
});
describe("Memory Leak Detection", () => {
test("should detect potential memory leaks in component lifecycle", async () => {
const components = [];
// Create multiple components with potential leaks
for (let i = 0; i < 10; i++) {
const TestComponent = () => {
const [data, setData] = React.useState([]);
React.useEffect(() => {
// Simulate memory leak by accumulating data
const interval = setInterval(() => {
setData((prev) => [...prev, new Array(1000).fill(i)]);
}, 10);
return () => clearInterval(interval);
}, []);
return React.createElement("div", null, `Component ${i}`);
};
const { unmount } = render(React.createElement(TestComponent));
components.push(unmount);
}
// Let components run for a bit
await new Promise((resolve) => setTimeout(resolve, 100));
// Unmount all components
components.forEach((unmount) => unmount());
// Wait for cleanup
await new Promise((resolve) => setTimeout(resolve, 100));
// In a real scenario, we would check memory usage here
// For testing, we just verify that components were created and destroyed
expect(components).toHaveLength(10);
});
test("should properly cleanup event listeners", () => {
const mockAddEventListener = jest.fn();
const mockRemoveEventListener = jest.fn();
// Mock DOM element
const mockElement = {
addEventListener: mockAddEventListener,
removeEventListener: mockRemoveEventListener,
};
const TestComponent = () => {
useEventListener("click", () => {}, mockElement);
return React.createElement("div", null, "Test");
};
const { unmount } = render(React.createElement(TestComponent));
expect(mockAddEventListener).toHaveBeenCalledTimes(1);
expect(mockRemoveEventListener).not.toHaveBeenCalled();
unmount();
expect(mockRemoveEventListener).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,446 @@
const React = require("react");
const { render } = require("ink-testing-library");
const {
PerformanceBenchmark,
PerformanceProfiler,
MemoryMonitor,
} = require("../../../src/tui/utils/performanceUtils.js");
// Import optimized components
const OptimizedMenuList = require("../../../src/tui/components/common/OptimizedMenuList.jsx");
const VirtualScrollableContainer = require("../../../src/tui/components/common/VirtualScrollableContainer.jsx");
const OptimizedProgressBar = require("../../../src/tui/components/common/OptimizedProgressBar.jsx");
// Import original components for comparison
const MenuList = require("../../../src/tui/components/common/MenuList.jsx");
const ScrollableContainer = require("../../../src/tui/components/common/ScrollableContainer.jsx");
const ProgressBar = require("../../../src/tui/components/common/ProgressBar.jsx");
/**
* Performance tests for TUI component rendering
* Requirements: 4.1, 4.3, 4.4
*/
describe("TUI Component Rendering Performance", () => {
let profiler;
let memoryMonitor;
beforeEach(() => {
profiler = new PerformanceProfiler();
memoryMonitor = new MemoryMonitor();
});
afterEach(() => {
profiler.clear();
memoryMonitor.stopMonitoring();
});
describe("MenuList Performance", () => {
const generateMenuItems = (count) => {
return Array.from({ length: count }, (_, i) => ({
label: `Menu Item ${i + 1}`,
shortcut: String.fromCharCode(97 + (i % 26)), // a-z
description: `Description for menu item ${i + 1}`,
}));
};
test("should render small menu lists efficiently", async () => {
const items = generateMenuItems(10);
const benchmark = new PerformanceBenchmark("Small MenuList Rendering");
const testFunction = () => {
const { unmount } = render(
React.createElement(OptimizedMenuList, {
items,
selectedIndex: 0,
onSelect: () => {},
showShortcuts: true,
})
);
unmount();
};
const results = await benchmark.run(testFunction, 100);
expect(results.average).toBeLessThan(10); // Should render in less than 10ms on average
expect(results.p95).toBeLessThan(20); // 95% of renders should be under 20ms
benchmark.logResults();
});
test("should handle large menu lists with virtual scrolling", async () => {
const items = generateMenuItems(1000);
const benchmark = new PerformanceBenchmark("Large MenuList Rendering");
const testFunction = () => {
const { unmount } = render(
React.createElement(OptimizedMenuList, {
items,
selectedIndex: 0,
onSelect: () => {},
showShortcuts: true,
})
);
unmount();
};
const results = await benchmark.run(testFunction, 50);
expect(results.average).toBeLessThan(50); // Should render in less than 50ms on average
expect(results.p95).toBeLessThan(100); // 95% of renders should be under 100ms
benchmark.logResults();
});
test("should show performance improvement over original MenuList", async () => {
const items = generateMenuItems(500);
// Benchmark original MenuList
const originalBenchmark = new PerformanceBenchmark("Original MenuList");
const originalTestFunction = () => {
const { unmount } = render(
React.createElement(MenuList, {
items,
selectedIndex: 0,
onSelect: () => {},
showShortcuts: true,
})
);
unmount();
};
const originalResults = await originalBenchmark.run(
originalTestFunction,
30
);
// Benchmark optimized MenuList
const optimizedBenchmark = new PerformanceBenchmark("Optimized MenuList");
const optimizedTestFunction = () => {
const { unmount } = render(
React.createElement(OptimizedMenuList, {
items,
selectedIndex: 0,
onSelect: () => {},
showShortcuts: true,
})
);
unmount();
};
const optimizedResults = await optimizedBenchmark.run(
optimizedTestFunction,
30
);
// Optimized version should be at least 20% faster
const improvement =
(originalResults.average - optimizedResults.average) /
originalResults.average;
expect(improvement).toBeGreaterThan(0.2);
console.log(
`Performance improvement: ${(improvement * 100).toFixed(1)}%`
);
originalBenchmark.logResults();
optimizedBenchmark.logResults();
});
});
describe("VirtualScrollableContainer Performance", () => {
const generateScrollItems = (count) => {
return Array.from({ length: count }, (_, i) => ({
id: i,
content: `Item ${
i + 1
} - Lorem ipsum dolor sit amet, consectetur adipiscing elit.`,
}));
};
const renderItem = (item, index) => {
return React.createElement("div", { key: index }, item.content);
};
test("should handle large datasets efficiently with virtual scrolling", async () => {
const items = generateScrollItems(10000);
const benchmark = new PerformanceBenchmark(
"Virtual Scrolling Large Dataset"
);
memoryMonitor.startMonitoring(1000);
const testFunction = () => {
const { unmount } = render(
React.createElement(VirtualScrollableContainer, {
items,
renderItem,
itemHeight: 1,
showScrollIndicators: true,
})
);
unmount();
};
const results = await benchmark.run(testFunction, 20);
// Should handle large datasets efficiently
expect(results.average).toBeLessThan(100); // Should render in less than 100ms
expect(results.p95).toBeLessThan(200); // 95% of renders should be under 200ms
// Check memory usage
const memoryStats = memoryMonitor.getStatistics();
const memoryLeak = memoryMonitor.checkForLeaks();
expect(memoryLeak.isLikely).toBe(false); // Should not have memory leaks
benchmark.logResults();
memoryMonitor.logSummary();
});
test("should maintain consistent performance with different scroll positions", async () => {
const items = generateScrollItems(5000);
const scrollPositions = [0, 100, 500, 1000, 2500, 4999];
const results = [];
for (const scrollPosition of scrollPositions) {
const benchmark = new PerformanceBenchmark(
`Virtual Scroll Position ${scrollPosition}`
);
const testFunction = () => {
const { unmount } = render(
React.createElement(VirtualScrollableContainer, {
items,
renderItem,
itemHeight: 1,
initialScrollPosition: scrollPosition,
})
);
unmount();
};
const result = await benchmark.run(testFunction, 20);
results.push(result.average);
}
// Performance should be consistent across different scroll positions
const maxVariation = Math.max(...results) - Math.min(...results);
const averageTime =
results.reduce((sum, time) => sum + time, 0) / results.length;
const variationPercentage = (maxVariation / averageTime) * 100;
expect(variationPercentage).toBeLessThan(50); // Variation should be less than 50%
console.log(
`Scroll position performance variation: ${variationPercentage.toFixed(
1
)}%`
);
});
});
describe("ProgressBar Performance", () => {
test("should handle rapid progress updates efficiently", async () => {
const benchmark = new PerformanceBenchmark("Rapid Progress Updates");
memoryMonitor.startMonitoring(500);
const testFunction = () => {
let progress = 0;
const { rerender, unmount } = render(
React.createElement(OptimizedProgressBar, {
progress,
label: "Test Progress",
animate: true,
debounceDelay: 50,
})
);
// Simulate rapid updates
for (let i = 0; i < 100; i++) {
progress = i;
rerender(
React.createElement(OptimizedProgressBar, {
progress,
label: "Test Progress",
animate: true,
debounceDelay: 50,
})
);
}
unmount();
};
const results = await benchmark.run(testFunction, 10);
expect(results.average).toBeLessThan(200); // Should handle rapid updates efficiently
// Check for memory leaks during rapid updates
const memoryLeak = memoryMonitor.checkForLeaks();
expect(memoryLeak.isLikely).toBe(false);
benchmark.logResults();
memoryMonitor.logSummary();
});
test("should optimize multi-progress bar rendering", async () => {
const progressItems = Array.from({ length: 20 }, (_, i) => ({
key: `progress-${i}`,
label: `Operation ${i + 1}`,
progress: Math.random() * 100,
color: ["blue", "green", "yellow", "cyan", "magenta"][i % 5],
}));
const benchmark = new PerformanceBenchmark(
"Multi-Progress Bar Rendering"
);
const testFunction = () => {
const { unmount } = render(
React.createElement(OptimizedProgressBar.Multi, {
progressItems,
width: 40,
showLabels: true,
showPercentages: true,
animate: false,
})
);
unmount();
};
const results = await benchmark.run(testFunction, 50);
expect(results.average).toBeLessThan(30); // Should render multiple progress bars efficiently
expect(results.p95).toBeLessThan(60);
benchmark.logResults();
});
});
describe("Memory Management", () => {
test("should not leak memory during component lifecycle", async () => {
const items = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
memoryMonitor.startMonitoring(500);
// Create and destroy components multiple times
for (let i = 0; i < 50; i++) {
const { unmount } = render(
React.createElement(OptimizedMenuList, {
items,
selectedIndex: i % items.length,
onSelect: () => {},
showShortcuts: true,
})
);
// Simulate some async operations
await new Promise((resolve) => setTimeout(resolve, 10));
unmount();
}
// Wait for garbage collection
await new Promise((resolve) => setTimeout(resolve, 1000));
const memoryLeak = memoryMonitor.checkForLeaks();
const memoryStats = memoryMonitor.getStatistics();
expect(memoryLeak.isLikely).toBe(false);
expect(memoryStats.growth.heapUsed).toBeLessThan(50 * 1024 * 1024); // Less than 50MB growth
memoryMonitor.logSummary();
});
test("should clean up event listeners and timers", async () => {
const initialHandlers = process.listenerCount("uncaughtException");
// Create components with timers and event listeners
const components = [];
for (let i = 0; i < 10; i++) {
const { unmount } = render(
React.createElement(OptimizedProgressBar, {
progress: 50,
animate: true,
animationSpeed: 100,
})
);
components.push(unmount);
}
// Unmount all components
components.forEach((unmount) => unmount());
// Wait for cleanup
await new Promise((resolve) => setTimeout(resolve, 500));
const finalHandlers = process.listenerCount("uncaughtException");
// Should not have increased the number of event listeners
expect(finalHandlers).toBeLessThanOrEqual(initialHandlers + 1);
});
});
describe("Debouncing and Throttling", () => {
test("should reduce render frequency with debouncing", async () => {
let renderCount = 0;
const TestComponent = () => {
renderCount++;
return React.createElement("div", null, "Test");
};
const { rerender } = render(React.createElement(TestComponent));
// Trigger multiple rapid re-renders
for (let i = 0; i < 100; i++) {
rerender(React.createElement(TestComponent));
}
// With proper debouncing, render count should be significantly less than 100
expect(renderCount).toBeLessThan(50);
});
test("should maintain responsiveness with throttling", async () => {
const updates = [];
let lastUpdate = Date.now();
const TestComponent = ({ value }) => {
const currentTime = Date.now();
updates.push(currentTime - lastUpdate);
lastUpdate = currentTime;
return React.createElement("div", null, value);
};
const { rerender } = render(
React.createElement(TestComponent, { value: 0 })
);
// Simulate rapid updates with throttling
for (let i = 1; i <= 50; i++) {
await new Promise((resolve) => setTimeout(resolve, 10));
rerender(React.createElement(TestComponent, { value: i }));
}
// Updates should be throttled but still responsive
const averageInterval =
updates.reduce((sum, interval) => sum + interval, 0) / updates.length;
expect(averageInterval).toBeGreaterThan(5); // Should be throttled
expect(averageInterval).toBeLessThan(100); // But still responsive
});
});
});
describe("Performance Regression Tests", () => {
test("should maintain performance benchmarks", async () => {
const benchmarks = {
smallMenuList: { maxAverage: 10, maxP95: 20 },
largeMenuList: { maxAverage: 50, maxP95: 100 },
virtualScrolling: { maxAverage: 100, maxP95: 200 },
progressBar: { maxAverage: 30, maxP95: 60 },
};
// This test would be run in CI to ensure performance doesn't regress
// For now, we'll just verify the benchmarks structure
expect(benchmarks).toBeDefined();
expect(Object.keys(benchmarks)).toHaveLength(4);
});
});