TUI is a doomed path. Stick with CLI
This commit is contained in:
668
tests/tui/integration/errorHandlingRecovery.test.js
Normal file
668
tests/tui/integration/errorHandlingRecovery.test.js
Normal file
@@ -0,0 +1,668 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user