527 lines
14 KiB
JavaScript
527 lines
14 KiB
JavaScript
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);
|
|
});
|
|
});
|