const React = require("react"); const { render } = require("ink-testing-library"); const TuiApplication = require("../../../src/tui/TuiApplication.jsx"); // Mock all the services and providers jest.mock("../../../src/tui/providers/AppProvider.jsx"); jest.mock("../../../src/tui/hooks/useServices.js"); jest.mock("../../../src/tui/components/common/LoadingIndicator.jsx"); jest.mock("../../../src/tui/components/common/ErrorDisplay.jsx"); describe("Error Handling and Recovery Integration Tests", () => { let mockAppState; let mockServices; let mockUseInput; beforeEach(() => { // Reset all mocks jest.clearAllMocks(); // Mock AppProvider mockAppState = { currentScreen: "main", navigateTo: jest.fn(), navigateBack: jest.fn(), getScreenState: jest.fn(), saveScreenState: jest.fn(), updateConfiguration: jest.fn(), getConfiguration: jest.fn(() => ({ targetTag: "test-tag", shopDomain: "test-shop.myshopify.com", accessToken: "test-token", })), }; require("../../../src/tui/providers/AppProvider.jsx").useAppState = jest.fn( () => mockAppState ); // Mock Services mockServices = { getAllSchedules: jest.fn(), addSchedule: jest.fn(), updateSchedule: jest.fn(), deleteSchedule: jest.fn(), getLogFiles: jest.fn(), readLogFile: jest.fn(), parseLogContent: jest.fn(), filterLogs: jest.fn(), fetchAllTags: jest.fn(), getTagDetails: jest.fn(), calculateTagStatistics: jest.fn(), searchTags: jest.fn(), }; require("../../../src/tui/hooks/useServices.js").useServices = jest.fn( () => mockServices ); // Mock useInput mockUseInput = jest.fn(); require("ink").useInput = mockUseInput; // Mock common components require("../../../src/tui/components/common/LoadingIndicator.jsx").LoadingIndicator = ({ children }) => React.createElement("div", { "data-testid": "loading" }, children); require("../../../src/tui/components/common/ErrorDisplay.jsx").ErrorDisplay = ({ error, onRetry }) => React.createElement( "div", { "data-testid": "error", onClick: onRetry, }, error?.message || "An error occurred" ); }); describe("Network Error Handling", () => { test("should handle network timeouts gracefully in scheduling screen", async () => { const networkError = new Error("Network timeout"); networkError.code = "NETWORK_TIMEOUT"; mockServices.getAllSchedules.mockRejectedValue(networkError); mockAppState.currentScreen = "scheduling"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Network timeout"); expect(lastFrame()).toContain("Check your internet connection"); }); test("should handle connection refused errors in tag analysis screen", async () => { const connectionError = new Error("Connection refused"); connectionError.code = "ECONNREFUSED"; mockServices.fetchAllTags.mockRejectedValue(connectionError); mockAppState.currentScreen = "tagAnalysis"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Connection refused"); expect(lastFrame()).toContain("Unable to connect to Shopify"); }); test("should provide retry functionality for network errors", async () => { const networkError = new Error("Network error"); mockServices.getLogFiles .mockRejectedValueOnce(networkError) .mockResolvedValueOnce([]); mockAppState.currentScreen = "viewLogs"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Network error"); // Retry operation inputHandler("r"); await new Promise((resolve) => setTimeout(resolve, 100)); expect(mockServices.getLogFiles).toHaveBeenCalledTimes(2); }); test("should implement exponential backoff for repeated network failures", async () => { const networkError = new Error("Network unstable"); mockServices.fetchAllTags .mockRejectedValueOnce(networkError) .mockRejectedValueOnce(networkError) .mockResolvedValueOnce([]); mockAppState.currentScreen = "tagAnalysis"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // First retry inputHandler("r"); await new Promise((resolve) => setTimeout(resolve, 100)); // Second retry (should have longer delay) inputHandler("r"); await new Promise((resolve) => setTimeout(resolve, 200)); expect(mockServices.fetchAllTags).toHaveBeenCalledTimes(3); }); }); describe("API Error Handling", () => { test("should handle Shopify API rate limiting", async () => { const rateLimitError = new Error("Rate limit exceeded"); rateLimitError.code = "RATE_LIMITED"; rateLimitError.retryAfter = 5; mockServices.fetchAllTags.mockRejectedValue(rateLimitError); mockAppState.currentScreen = "tagAnalysis"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Rate limit exceeded"); expect(lastFrame()).toContain("Please wait 5 seconds"); }); test("should handle authentication errors", async () => { const authError = new Error("Invalid access token"); authError.code = "UNAUTHORIZED"; mockServices.fetchAllTags.mockRejectedValue(authError); mockAppState.currentScreen = "tagAnalysis"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Invalid access token"); expect(lastFrame()).toContain("Check your Shopify credentials"); expect(lastFrame()).toContain("Go to Configuration"); }); test("should handle API permission errors", async () => { const permissionError = new Error("Insufficient permissions"); permissionError.code = "FORBIDDEN"; mockServices.getTagDetails.mockRejectedValue(permissionError); mockAppState.currentScreen = "tagAnalysis"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); mockServices.fetchAllTags.mockResolvedValue([ { tag: "test-tag", productCount: 1, variantCount: 1, totalValue: 100 }, ]); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // Try to view tag details inputHandler("", { return: true }); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Insufficient permissions"); expect(lastFrame()).toContain( "Your API token may not have the required permissions" ); }); test("should handle API version compatibility errors", async () => { const versionError = new Error("API version not supported"); versionError.code = "API_VERSION_MISMATCH"; mockServices.fetchAllTags.mockRejectedValue(versionError); mockAppState.currentScreen = "tagAnalysis"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("API version not supported"); expect(lastFrame()).toContain("Please update the application"); }); }); describe("File System Error Handling", () => { test("should handle missing schedules.json file gracefully", async () => { const fileError = new Error("ENOENT: no such file or directory"); fileError.code = "ENOENT"; mockServices.getAllSchedules.mockRejectedValue(fileError); mockAppState.currentScreen = "scheduling"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("No schedules found"); expect(lastFrame()).toContain("Create your first schedule"); }); test("should handle corrupted log files", async () => { const mockLogFiles = [ { filename: "corrupted.md", size: 1024, operationCount: 5 }, ]; mockServices.getLogFiles.mockResolvedValue(mockLogFiles); mockServices.readLogFile.mockResolvedValue("Corrupted content"); mockServices.parseLogContent.mockImplementation(() => { throw new Error("Failed to parse log content"); }); mockAppState.currentScreen = "viewLogs"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // Select corrupted log file inputHandler("", { return: true }); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Failed to parse log content"); expect(lastFrame()).toContain("Showing raw content"); }); test("should handle permission denied errors for file operations", async () => { const permissionError = new Error("Permission denied"); permissionError.code = "EACCES"; mockServices.addSchedule.mockRejectedValue(permissionError); mockAppState.currentScreen = "scheduling"; mockServices.getAllSchedules.mockResolvedValue([]); let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // Try to create new schedule inputHandler("n"); inputHandler("", { return: true }); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Permission denied"); expect(lastFrame()).toContain("Check file permissions"); }); test("should handle disk space errors", async () => { const diskSpaceError = new Error("No space left on device"); diskSpaceError.code = "ENOSPC"; mockServices.addSchedule.mockRejectedValue(diskSpaceError); mockAppState.currentScreen = "scheduling"; mockServices.getAllSchedules.mockResolvedValue([]); let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // Try to create new schedule inputHandler("n"); inputHandler("", { return: true }); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("No space left on device"); expect(lastFrame()).toContain("Free up disk space"); }); }); describe("Validation Error Handling", () => { test("should handle form validation errors in scheduling screen", async () => { mockServices.getAllSchedules.mockResolvedValue([]); const validationError = new Error("Invalid schedule data"); validationError.code = "VALIDATION_ERROR"; validationError.details = { scheduledTime: "Invalid date format", operationType: "Must be 'update' or 'rollback'", }; mockServices.addSchedule.mockRejectedValue(validationError); mockAppState.currentScreen = "scheduling"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // Try to create invalid schedule inputHandler("n"); inputHandler("", { return: true }); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Invalid date format"); expect(lastFrame()).toContain("Must be 'update' or 'rollback'"); }); test("should handle configuration validation errors", async () => { const configError = new Error("Invalid configuration"); configError.code = "CONFIG_INVALID"; mockAppState.updateConfiguration.mockImplementation(() => { throw configError; }); mockServices.fetchAllTags.mockResolvedValue([ { tag: "test-tag", productCount: 1, variantCount: 1, totalValue: 100 }, ]); mockAppState.currentScreen = "tagAnalysis"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); // Try to update configuration with invalid tag inputHandler("c"); inputHandler("y"); expect(lastFrame()).toContain("Invalid configuration"); }); }); describe("Recovery Mechanisms", () => { test("should automatically retry failed operations with exponential backoff", async () => { const transientError = new Error("Temporary service unavailable"); transientError.code = "SERVICE_UNAVAILABLE"; mockServices.fetchAllTags .mockRejectedValueOnce(transientError) .mockRejectedValueOnce(transientError) .mockResolvedValueOnce([]); mockAppState.currentScreen = "tagAnalysis"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); render(React.createElement(TuiApplication)); // Should automatically retry await new Promise((resolve) => setTimeout(resolve, 500)); expect(mockServices.fetchAllTags).toHaveBeenCalledTimes(3); }); test("should provide manual retry option for persistent errors", async () => { const persistentError = new Error("Service down for maintenance"); mockServices.getAllSchedules .mockRejectedValue(persistentError) .mockRejectedValue(persistentError) .mockResolvedValue([]); mockAppState.currentScreen = "scheduling"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Service down for maintenance"); expect(lastFrame()).toContain("Press 'r' to retry"); // Manual retry inputHandler("r"); await new Promise((resolve) => setTimeout(resolve, 100)); expect(mockServices.getAllSchedules).toHaveBeenCalledTimes(2); }); test("should fallback to cached data when available", async () => { const networkError = new Error("Network unavailable"); // Mock cached data mockAppState.getScreenState.mockReturnValue({ cachedTags: [ { tag: "cached-tag", productCount: 5, variantCount: 15, totalValue: 500, }, ], lastFetch: Date.now() - 300000, // 5 minutes ago }); mockServices.fetchAllTags.mockRejectedValue(networkError); mockAppState.currentScreen = "tagAnalysis"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("cached-tag"); expect(lastFrame()).toContain("Using cached data"); expect(lastFrame()).toContain("5 minutes ago"); }); test("should gracefully degrade functionality when services are unavailable", async () => { const serviceError = new Error("All services unavailable"); mockServices.getAllSchedules.mockRejectedValue(serviceError); mockServices.getLogFiles.mockRejectedValue(serviceError); mockServices.fetchAllTags.mockRejectedValue(serviceError); // Test each screen handles degraded mode const screens = ["scheduling", "viewLogs", "tagAnalysis"]; for (const screen of screens) { mockAppState.currentScreen = screen; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Service unavailable"); expect(lastFrame()).toContain("Limited functionality"); } }); }); describe("Error State Management", () => { test("should clear error state when operation succeeds", async () => { const temporaryError = new Error("Temporary error"); mockServices.getAllSchedules .mockRejectedValueOnce(temporaryError) .mockResolvedValueOnce([]); mockAppState.currentScreen = "scheduling"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Temporary error"); // Retry and succeed inputHandler("r"); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).not.toContain("Temporary error"); expect(lastFrame()).toContain("No schedules found"); }); test("should persist error state across screen navigation", async () => { const persistentError = new Error("Configuration error"); mockServices.fetchAllTags.mockRejectedValue(persistentError); mockAppState.currentScreen = "tagAnalysis"; let inputHandler; mockUseInput.mockImplementation((handler) => { inputHandler = handler; }); const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Configuration error"); // Navigate away and back inputHandler("", { escape: true }); mockAppState.currentScreen = "main"; // Navigate back to tag analysis inputHandler("t"); mockAppState.currentScreen = "tagAnalysis"; // Error should be saved in screen state expect(mockAppState.saveScreenState).toHaveBeenCalledWith( "tagAnalysis", expect.objectContaining({ error: expect.any(Object), }) ); }); test("should provide error context and troubleshooting guidance", async () => { const contextualError = new Error("Shop not found"); contextualError.code = "SHOP_NOT_FOUND"; contextualError.context = { shopDomain: "invalid-shop.myshopify.com", suggestion: "Verify your shop domain in configuration", }; mockServices.fetchAllTags.mockRejectedValue(contextualError); mockAppState.currentScreen = "tagAnalysis"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Shop not found"); expect(lastFrame()).toContain("invalid-shop.myshopify.com"); expect(lastFrame()).toContain("Verify your shop domain"); }); }); describe("Critical Error Handling", () => { test("should handle application crashes gracefully", async () => { const criticalError = new Error("Critical system error"); criticalError.code = "CRITICAL"; // Mock a critical error that would crash the app mockServices.getAllSchedules.mockImplementation(() => { throw criticalError; }); mockAppState.currentScreen = "scheduling"; // Should not crash the entire application expect(() => { render(React.createElement(TuiApplication)); }).not.toThrow(); }); test("should provide safe mode when multiple services fail", async () => { const systemError = new Error("System failure"); // All services fail Object.keys(mockServices).forEach((service) => { mockServices[service].mockRejectedValue(systemError); }); mockAppState.currentScreen = "main"; const { lastFrame } = render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(lastFrame()).toContain("Safe mode"); expect(lastFrame()).toContain("Limited functionality available"); }); test("should log critical errors for debugging", async () => { const criticalError = new Error("Memory allocation failed"); criticalError.code = "ENOMEM"; mockServices.fetchAllTags.mockRejectedValue(criticalError); mockAppState.currentScreen = "tagAnalysis"; // Mock console.error to capture error logging const consoleSpy = jest .spyOn(console, "error") .mockImplementation(() => {}); render(React.createElement(TuiApplication)); await new Promise((resolve) => setTimeout(resolve, 100)); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining("Critical error"), expect.any(Error) ); consoleSpy.mockRestore(); }); }); });