TUI is a doomed path. Stick with CLI

This commit is contained in:
2025-08-17 01:04:10 -05:00
parent f38eff12cd
commit 35960388cf
23 changed files with 6616 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
const PerformanceOptimizer = require("../../../src/tui/utils/PerformanceOptimizer.js");
describe("PerformanceOptimizer", () => {
let optimizer;
beforeEach(() => {
optimizer = new PerformanceOptimizer();
});
afterEach(() => {
optimizer.destroy();
});
describe("debounce", () => {
it("should debounce function calls", (done) => {
let callCount = 0;
const testFunction = () => {
callCount++;
};
const debouncedFunction = optimizer.debounce(testFunction, 100, "test");
// Call multiple times rapidly
debouncedFunction();
debouncedFunction();
debouncedFunction();
// Should not have been called yet
expect(callCount).toBe(0);
// Wait for debounce delay
setTimeout(() => {
expect(callCount).toBe(1);
done();
}, 150);
});
});
describe("throttle", () => {
it("should throttle function calls", (done) => {
let callCount = 0;
const testFunction = () => {
callCount++;
};
const throttledFunction = optimizer.throttle(testFunction, 100, "test");
// Call multiple times rapidly
throttledFunction();
throttledFunction();
throttledFunction();
// Should have been called once immediately
expect(callCount).toBe(1);
// Wait and call again
setTimeout(() => {
throttledFunction();
expect(callCount).toBe(2);
done();
}, 150);
});
});
describe("memoize", () => {
it("should memoize function results", () => {
let callCount = 0;
const expensiveFunction = (x, y) => {
callCount++;
return x + y;
};
const memoizedFunction = optimizer.memoize(expensiveFunction);
// First call
const result1 = memoizedFunction(1, 2);
expect(result1).toBe(3);
expect(callCount).toBe(1);
// Second call with same arguments
const result2 = memoizedFunction(1, 2);
expect(result2).toBe(3);
expect(callCount).toBe(1); // Should not have called function again
// Third call with different arguments
const result3 = memoizedFunction(2, 3);
expect(result3).toBe(5);
expect(callCount).toBe(2);
});
it("should limit cache size", () => {
const testFunction = (x) => x * 2;
const memoizedFunction = optimizer.memoize(testFunction, undefined, 2);
// Fill cache beyond limit
memoizedFunction(1);
memoizedFunction(2);
memoizedFunction(3); // Should evict first entry
// Verify first entry was evicted
let callCount = 0;
const countingFunction = (x) => {
callCount++;
return x * 2;
};
const countingMemoized = optimizer.memoize(
countingFunction,
undefined,
2
);
countingMemoized(1);
countingMemoized(2);
expect(callCount).toBe(2);
countingMemoized(3); // Should evict entry for 1
expect(callCount).toBe(3);
countingMemoized(1); // Should call function again since it was evicted
expect(callCount).toBe(4);
});
});
describe("createVirtualScrolling", () => {
it("should calculate virtual scrolling data correctly", () => {
const items = Array.from({ length: 100 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
const result = optimizer.createVirtualScrolling(items, 300, 30, 150);
expect(result.totalHeight).toBe(3000); // 100 items * 30px each
expect(result.startIndex).toBe(5); // 150px / 30px per item
expect(result.visibleItems.length).toBeGreaterThan(0);
expect(result.visibleItems.length).toBeLessThanOrEqual(12); // Visible count + buffer
});
});
describe("createLazyLoading", () => {
it("should create lazy loading data correctly", () => {
const items = Array.from({ length: 100 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
const result = optimizer.createLazyLoading(items, 10, 5);
expect(result.startIndex).toBe(5); // 10 - 5
expect(result.endIndex).toBe(20); // 10 + 5*2
expect(result.loadedItems.length).toBe(15); // 20 - 5
expect(result.hasMore).toBe(true);
expect(result.hasPrevious).toBe(true);
});
});
describe("memory management", () => {
it("should track memory usage", () => {
const stats = optimizer.getMemoryUsage();
expect(stats).toHaveProperty("estimatedSizeBytes");
expect(stats).toHaveProperty("estimatedSizeMB");
expect(stats).toHaveProperty("cacheEntries");
expect(stats).toHaveProperty("eventListeners");
expect(stats).toHaveProperty("activeTimers");
expect(stats).toHaveProperty("memoryPressure");
});
it("should clean up expired cache entries", () => {
// Add some cache entries
optimizer.componentCache.set("test1", {
data: "test",
timestamp: Date.now() - 10000,
});
optimizer.componentCache.set("test2", {
data: "test",
timestamp: Date.now(),
});
optimizer.cleanupExpiredCache(5000); // 5 second max age
expect(optimizer.componentCache.has("test1")).toBe(false);
expect(optimizer.componentCache.has("test2")).toBe(true);
});
});
describe("event listener management", () => {
it("should register and cleanup event listeners", () => {
const mockTarget = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};
const handler = () => {};
optimizer.registerEventListener(
"component1",
"click",
handler,
mockTarget
);
expect(mockTarget.addEventListener).toHaveBeenCalledWith(
"click",
handler
);
optimizer.cleanupEventListeners("component1");
expect(mockTarget.removeEventListener).toHaveBeenCalledWith(
"click",
handler
);
});
});
describe("batched updates", () => {
it("should batch updates correctly", (done) => {
let batchedUpdates = [];
const updateFunction = async (batch) => {
batchedUpdates.push(batch);
};
const batchedUpdate = optimizer.createBatchedUpdate(
updateFunction,
3,
50
);
// Add updates
batchedUpdate("update1");
batchedUpdate("update2");
batchedUpdate("update3");
batchedUpdate("update4");
// Wait for batching
setTimeout(() => {
expect(batchedUpdates.length).toBeGreaterThan(0);
expect(batchedUpdates[0]).toEqual(["update1", "update2", "update3"]);
done();
}, 100);
});
});
});

View File

@@ -0,0 +1,130 @@
/**
* Input Validator Tests
* Tests for comprehensive input validation
* Requirements: 5.4, 5.6
*/
const inputValidator = require("../../../src/tui/utils/inputValidator");
describe("InputValidator Tests", () => {
test("should validate operation type correctly", () => {
const validResult = inputValidator.validateField("operationType", "update");
expect(validResult.isValid).toBe(true);
expect(validResult.value).toBe("update");
const invalidResult = inputValidator.validateField(
"operationType",
"invalid"
);
expect(invalidResult.isValid).toBe(false);
expect(invalidResult.errors).toContain(
"operationType must be one of: update, rollback"
);
});
test("should validate scheduled time correctly", () => {
const futureDate = new Date(Date.now() + 86400000).toISOString();
const validResult = inputValidator.validateField(
"scheduledTime",
futureDate
);
expect(validResult.isValid).toBe(true);
const pastDate = new Date(Date.now() - 86400000).toISOString();
const invalidResult = inputValidator.validateField(
"scheduledTime",
pastDate
);
expect(invalidResult.isValid).toBe(false);
});
test("should validate shop domain correctly", () => {
const validDomain = "test-store.myshopify.com";
const validResult = inputValidator.validateField("shopDomain", validDomain);
expect(validResult.isValid).toBe(true);
const invalidDomain = "invalid domain";
const invalidResult = inputValidator.validateField(
"shopDomain",
invalidDomain
);
expect(invalidResult.isValid).toBe(false);
});
test("should validate price adjustment correctly", () => {
const validPercentage = 25.5;
const validResult = inputValidator.validateField(
"priceAdjustment",
validPercentage
);
expect(validResult.isValid).toBe(true);
expect(validResult.value).toBe(25.5);
const invalidPercentage = 1500; // Too high
const invalidResult = inputValidator.validateField(
"priceAdjustment",
invalidPercentage
);
expect(invalidResult.isValid).toBe(false);
});
test("should validate multiple fields", () => {
const data = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000).toISOString(),
recurrence: "weekly",
description: "Test schedule",
};
const result = inputValidator.validateFields(data);
expect(result.isValid).toBe(true);
expect(result.data.operationType).toBe("update");
expect(result.data.recurrence).toBe("weekly");
});
test("should handle optional fields correctly", () => {
const data = {
operationType: "update",
scheduledTime: new Date(Date.now() + 86400000).toISOString(),
recurrence: "once",
// description is optional and missing
};
const result = inputValidator.validateFields(data);
expect(result.isValid).toBe(true);
expect(result.data.description).toBeUndefined();
});
test("should convert string numbers to numbers", () => {
const result = inputValidator.validateField("priceAdjustment", "25.5");
expect(result.isValid).toBe(true);
expect(result.value).toBe(25.5);
expect(typeof result.value).toBe("number");
});
test("should sanitize input strings", () => {
const dirtyInput = " test string with \x00 control chars ";
const sanitized = inputValidator.sanitizeInput(dirtyInput, {
trim: true,
removeControlChars: true,
});
expect(sanitized).toBe("test string with control chars");
});
test("should validate string length limits", () => {
const longDescription = "x".repeat(501);
const result = inputValidator.validateField("description", longDescription);
expect(result.isValid).toBe(false);
expect(
result.errors.some((error) => error.includes("500 characters"))
).toBe(true);
});
test("should validate required fields", () => {
const result = inputValidator.validateField("operationType", "");
expect(result.isValid).toBe(false);
expect(result.errors).toContain("operationType is required");
});
});

View File

@@ -0,0 +1,122 @@
/**
* State Manager Tests
* Tests for state management and cleanup functionality
* Requirements: 5.4, 5.6
*/
const stateManager = require("../../../src/tui/utils/stateManager");
describe("StateManager Tests", () => {
beforeEach(() => {
// Clear all states before each test
stateManager.clearAllStates();
});
afterEach(() => {
// Cleanup after each test
stateManager.clearAllStates();
});
test("should register screen handlers", () => {
const mockCleanup = jest.fn();
const mockValidate = jest
.fn()
.mockResolvedValue({ isValid: true, errors: [] });
stateManager.registerScreen("test-screen", {
cleanup: mockCleanup,
validate: mockValidate,
});
expect(stateManager.cleanupHandlers.has("test-screen")).toBe(true);
expect(stateManager.stateValidators.has("test-screen")).toBe(true);
});
test("should save and restore screen state", async () => {
const testState = {
selectedIndex: 5,
formData: { name: "test" },
timestamp: Date.now(),
};
await stateManager.saveScreenState("test-screen", testState);
const restoredState = await stateManager.restoreScreenState("test-screen");
expect(restoredState.selectedIndex).toBe(5);
expect(restoredState.formData.name).toBe("test");
expect(restoredState._metadata).toBeUndefined(); // Metadata should be stripped
});
test("should perform screen transitions with cleanup", async () => {
const mockCleanup = jest.fn().mockResolvedValue();
stateManager.registerScreen("from-screen", {
cleanup: mockCleanup,
});
const currentState = { data: "test" };
await stateManager.switchScreen("from-screen", "to-screen", currentState);
expect(mockCleanup).toHaveBeenCalled();
expect(stateManager.activeScreen).toBe("to-screen");
});
test("should validate states", async () => {
const mockValidator = jest.fn().mockResolvedValue({
isValid: false,
errors: ["Test error"],
});
stateManager.registerScreen("test-screen", {
validate: mockValidator,
});
await stateManager.saveScreenState("test-screen", { data: "test" });
const report = await stateManager.validateAllStates();
expect(report.invalidStates).toBe(1);
expect(report.errors).toHaveLength(1);
expect(mockValidator).toHaveBeenCalled();
});
test("should provide memory statistics", () => {
stateManager.saveScreenState("screen1", { data: "test1" });
stateManager.saveScreenState("screen2", { data: "test2" });
const stats = stateManager.getMemoryStats();
expect(stats.screenCount).toBe(2);
expect(stats.totalSize).toBeGreaterThan(0);
expect(stats.screenSizes).toHaveProperty("screen1");
expect(stats.screenSizes).toHaveProperty("screen2");
});
test("should track navigation history", async () => {
await stateManager.switchScreen("screen1", "screen2", {});
await stateManager.switchScreen("screen2", "screen3", {});
const history = stateManager.getHistory(5);
expect(history).toHaveLength(2);
expect(history[0].from).toBe("screen2");
expect(history[0].to).toBe("screen3");
expect(history[1].from).toBe("screen1");
expect(history[1].to).toBe("screen2");
});
test("should clear screen states", () => {
stateManager.saveScreenState("screen1", { data: "test1" });
stateManager.saveScreenState("screen2", { data: "test2" });
expect(stateManager.screenStates.size).toBe(2);
stateManager.clearScreenState("screen1");
expect(stateManager.screenStates.size).toBe(1);
expect(stateManager.screenStates.has("screen1")).toBe(false);
stateManager.clearAllStates();
expect(stateManager.screenStates.size).toBe(0);
});
});