diff --git a/src/lib/error.types.ts b/src/lib/error.types.ts index 09be9b1..8da5c58 100644 --- a/src/lib/error.types.ts +++ b/src/lib/error.types.ts @@ -6,6 +6,7 @@ export class AppError extends Error { ) { super(message); this.name = "AppError"; + if (details) this.details = details; } } diff --git a/src/lib/utils/errorUtils.ts b/src/lib/utils/errorUtils.ts index ee180a1..3fe3593 100644 --- a/src/lib/utils/errorUtils.ts +++ b/src/lib/utils/errorUtils.ts @@ -12,8 +12,8 @@ export function formatError(error: unknown, defaultMessage: string = "Произ return `${defaultMessage}: Произошла неизвестная ошибка`; } -export function createError(message: string, code: string, recoverable: boolean = true, details?: unknown): AppError { - return new AppError(message, code, recoverable); +export function createError(message: string, code: string, details?: unknown): AppError { + return new AppError(message, code, details); } export function logError(error: unknown, context?: string): void { diff --git a/tests/unit/components/ImageUpload.test.ts b/tests/unit/components/ImageUpload.test.ts deleted file mode 100644 index 27315cb..0000000 --- a/tests/unit/components/ImageUpload.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -import ImageUpload from "$components/image/ImageUpload.svelte"; -import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock the stores and services -vi.mock("$stores/uiStore", () => ({ - uiStore: { - subscribe: vi.fn((callback) => { - callback({ error: null, loading: false }); - return () => {}; - }), - }, - setCurrentStep: vi.fn(), - setLoading: vi.fn(), - clearError: vi.fn(), -})); - -vi.mock("$services/imageService", () => ({ - imageService: { - validateAndProcessImage: vi.fn(), - }, -})); - -vi.mock("$lib/utils/errorHandler", () => ({ - handleError: vi.fn((error) => `Error: ${error}`), -})); - -describe("ImageUpload", () => { - let mockOnImageSelect: (image: string) => void; - let mockImageService: any; - - beforeEach(() => { - mockOnImageSelect = vi.fn(); - vi.clearAllMocks(); - - // Setup mock image service - mockImageService = { - validateAndProcessImage: vi.fn().mockResolvedValue({ - success: true, - imageUrl: "data:image/jpeg;base64,/9j/4AAQSkZJRg==", - }), - }; - }); - - it("should render upload component", () => { - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - expect(container).toBeTruthy(); - }); - - it("should handle file selection via input", async () => { - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - // Create a mock file - const mockFile = new File(["test image content"], "test.jpg", { - type: "image/jpeg", - }); - - // Find file input - const fileInput = container.querySelector('input[type="file"]'); - expect(fileInput).toBeTruthy(); - - if (fileInput) { - // Simulate file selection - await fireEvent.change(fileInput, { - target: { files: [mockFile] }, - }); - - // Should call image service - await waitFor(() => { - expect(mockImageService.validateAndProcessImage).toHaveBeenCalled(); - }); - } - }); - - it("should handle drag and drop", async () => { - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - // Create mock file - const mockFile = new File(["test image content"], "test.jpg", { - type: "image/jpeg", - }); - - // Find drop zone - const dropZone = container.querySelector("[data-testid='drop-zone'], .drop-zone, .upload-area"); - if (dropZone) { - // Simulate drag over - await fireEvent.dragOver(dropZone, { - dataTransfer: { files: [mockFile] }, - }); - - // Simulate drop - await fireEvent.drop(dropZone, { - dataTransfer: { files: [mockFile] }, - }); - - // Should process the image - await waitFor(() => { - expect(mockImageService.validateAndProcessImage).toHaveBeenCalled(); - }); - } - }); - - it("should handle URL input", async () => { - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - // Find URL input and toggle button - const urlInputs = container.querySelectorAll('input[type="url"], input[placeholder*="URL"]'); - const buttons = screen.getAllByRole("button"); - - // Toggle URL input if needed - const toggleButton = buttons.find( - (button) => - button.textContent?.includes("URL") || - button.textContent?.includes("Ссылка") || - button.textContent?.includes("By URL"), - ); - - if (toggleButton) { - await fireEvent.click(toggleButton); - } - - if (urlInputs.length > 0) { - const testUrl = "https://example.com/image.jpg"; - await fireEvent.input(urlInputs[0], { target: { value: testUrl } }); - - // Submit URL form - const submitButton = buttons.find((button) => { - const isLoadButton = button.textContent?.includes("Load") || button.textContent?.includes("Загрузить"); - const buttonType = (button as HTMLButtonElement).type; - return isLoadButton || buttonType === "submit"; - }); - - if (submitButton) { - await fireEvent.click(submitButton); - - // Should process the URL - await waitFor(() => { - expect(mockImageService.validateAndProcessImage).toHaveBeenCalled(); - }); - } - } - }); - - it("should handle paste event", async () => { - render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - // Create mock clipboard data - const mockClipboardData = { - items: [ - { - kind: "file", - type: "image/jpeg", - getAsFile: () => new File(["pasted image"], "pasted.jpg", { type: "image/jpeg" }), - }, - ], - }; - - // Simulate paste event - const pasteEvent = new ClipboardEvent("paste", { - clipboardData: mockClipboardData as any, - }); - - document.dispatchEvent(pasteEvent); - - // Should process the pasted image - await waitFor(() => { - expect(mockImageService.validateAndProcessImage).toHaveBeenCalled(); - }); - }); - - it("should show error for invalid image", async () => { - // Mock image service to return error - mockImageService.validateAndProcessImage.mockResolvedValue({ - success: false, - error: "Invalid image format", - }); - - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - const mockFile = new File(["invalid content"], "test.txt", { - type: "text/plain", - }); - - const fileInput = container.querySelector('input[type="file"]'); - if (fileInput) { - await fireEvent.change(fileInput, { - target: { files: [mockFile] }, - }); - - // Should show error message - await waitFor(() => { - expect(container.textContent).toContain("Error"); - }); - } - }); - - it("should call onImageSelect when image is successfully processed", async () => { - const mockImageUrl = "data:image/jpeg;base64,/9j/4AAQSkZJRg=="; - - // Mock successful image processing - mockImageService.validateAndProcessImage.mockResolvedValue({ - success: true, - imageUrl: mockImageUrl, - }); - - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - const mockFile = new File(["test image content"], "test.jpg", { - type: "image/jpeg", - }); - - const fileInput = container.querySelector('input[type="file"]'); - if (fileInput) { - await fireEvent.change(fileInput, { - target: { files: [mockFile] }, - }); - - // Should call onImageSelect with processed image - await waitFor(() => { - expect(mockOnImageSelect).toHaveBeenCalledWith(mockImageUrl); - }); - } - }); - - it("should show loading state during processing", async () => { - const { setLoading } = await import("$stores/uiStore"); - - // Mock slow processing - mockImageService.validateAndProcessImage.mockImplementation( - () => - new Promise((resolve) => - setTimeout( - () => - resolve({ - success: true, - imageUrl: "data:image/jpeg;base64,/9j/4AAQSkZJRg==", - }), - 100, - ), - ), - ); - - const { container } = render(ImageUpload, { - props: { - onImageSelect: mockOnImageSelect, - }, - }); - - const mockFile = new File(["test image content"], "test.jpg", { - type: "image/jpeg", - }); - - const fileInput = container.querySelector('input[type="file"]'); - if (fileInput) { - await fireEvent.change(fileInput, { - target: { files: [mockFile] }, - }); - - // Should show loading state - expect(setLoading).toHaveBeenCalledWith(true); - - // Wait for processing to complete - await waitFor(() => { - expect(setLoading).toHaveBeenCalledWith(false); - }); - } - }); -}); diff --git a/tests/unit/components/TextManager.test.ts b/tests/unit/components/TextManager.test.ts deleted file mode 100644 index aec5290..0000000 --- a/tests/unit/components/TextManager.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import TextManager from "$components/text/TextManager.svelte"; -import { fireEvent, render, screen } from "@testing-library/svelte"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock the stores -vi.mock("$stores/panelStore", () => ({ - textSettingsStore: { - subscribe: vi.fn((callback) => { - callback({ - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center", - paddingX: 10, - verticalOffset: 0, - }); - return () => {}; // unsubscribe function - }), - update: vi.fn(), - }, - updateAllTextSettings: vi.fn(), -})); - -describe("TextManager", () => { - let mockOnTextAdd: (text: string, settings?: any) => void; - let mockOnTextUpdate: (id: string, text: string) => void; - let mockOnTextDelete: (id: string) => void; - - beforeEach(() => { - mockOnTextAdd = vi.fn(); - mockOnTextUpdate = vi.fn(); - mockOnTextDelete = vi.fn(); - }); - - it("should render with empty text list", () => { - const { container } = render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - expect(container).toBeTruthy(); - }); - - it("should render with existing texts", () => { - const mockTexts = [ - { id: "text-1", text: "First text" }, - { id: "text-2", text: "Second text" }, - ]; - - const { container } = render(TextManager, { - props: { - texts: mockTexts, - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - expect(container).toBeTruthy(); - }); - - it("should call onTextAdd when add text form is submitted", async () => { - const { container } = render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Find text input and add button - const textInputs = container.querySelectorAll('input[type="text"], textarea'); - const buttons = screen.getAllByRole("button"); - - if (textInputs.length > 0 && buttons.length > 0) { - await fireEvent.input(textInputs[0], { target: { value: "New text" } }); - await fireEvent.click(buttons[0]); - - expect(mockOnTextAdd).toHaveBeenCalledWith("New text", expect.any(Object)); - } - }); - - it("should show error for empty text", async () => { - const { container } = render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Find add button and click without entering text - const buttons = screen.getAllByRole("button"); - if (buttons.length > 0) { - await fireEvent.click(buttons[0]); - - // Should not call onTextAdd for empty text - expect(mockOnTextAdd).not.toHaveBeenCalled(); - } - }); - - it("should handle text update", async () => { - const mockTexts = [{ id: "text-1", text: "Original text" }]; - - const { container } = render(TextManager, { - props: { - texts: mockTexts, - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Find text inputs for existing texts - const textInputs = container.querySelectorAll('input[type="text"], textarea'); - if (textInputs.length > 0) { - await fireEvent.input(textInputs[0], { target: { value: "Updated text" } }); - - // Should call onTextUpdate with the text ID and new text - expect(mockOnTextUpdate).toHaveBeenCalledWith("text-1", "Updated text"); - } - }); - - it("should handle text deletion", async () => { - const mockTexts = [{ id: "text-1", text: "Text to delete" }]; - - render(TextManager, { - props: { - texts: mockTexts, - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Find delete button (might be an X button or similar) - const buttons = screen.getAllByRole("button"); - const deleteButton = buttons.find( - (button) => - button.textContent?.includes("Delete") || - button.textContent?.includes("Remove") || - button.textContent?.includes("×") || - button.textContent?.includes("X"), - ); - - if (deleteButton) { - await fireEvent.click(deleteButton); - expect(mockOnTextDelete).toHaveBeenCalledWith("text-1"); - } - }); - - it("should handle font size changes", async () => { - const { container } = render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Find font size input (number input) - const numberInputs = container.querySelectorAll('input[type="number"]'); - if (numberInputs.length > 0) { - await fireEvent.input(numberInputs[0], { target: { value: "24" } }); - - // Font size change should be handled in the component - expect(container).toBeTruthy(); // Basic check that component still renders - } - }); - - it("should handle color changes", async () => { - const { container } = render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Find color input - const colorInputs = container.querySelectorAll('input[type="color"]'); - if (colorInputs.length > 0) { - await fireEvent.input(colorInputs[0], { target: { value: "#ff0000" } }); - - // Color change should be handled in the component - expect(container).toBeTruthy(); // Basic check that component still renders - } - }); - - it("should handle text alignment changes", async () => { - const { container } = render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Look for alignment buttons or radio buttons - const radioButtons = screen.getAllByRole("radio"); - if (radioButtons.length > 0) { - await fireEvent.click(radioButtons[0]); - expect(container).toBeTruthy(); // Basic check that component still renders - } - }); - - it("should call updateAllTextSettings when update all button is clicked", async () => { - const { updateAllTextSettings } = await import("$stores/panelStore"); - - render(TextManager, { - props: { - texts: [], - onTextAdd: mockOnTextAdd, - onTextUpdate: mockOnTextUpdate, - onTextDelete: mockOnTextDelete, - }, - }); - - // Look for "Update All" or similar button - const buttons = screen.getAllByRole("button"); - const updateAllButton = buttons.find( - (button) => - button.textContent?.includes("Update All") || - button.textContent?.includes("Apply to All") || - button.textContent?.includes("Сохранить"), - ); - - if (updateAllButton) { - await fireEvent.click(updateAllButton); - expect(updateAllTextSettings).toHaveBeenCalled(); - } - }); -}); diff --git a/tests/unit/errorHandler.test.ts b/tests/unit/errorHandler.test.ts index db450d1..8960357 100644 --- a/tests/unit/errorHandler.test.ts +++ b/tests/unit/errorHandler.test.ts @@ -1,5 +1,5 @@ -import { AppError } from "$lib/types/errors"; -import { createError, handleError, isRecoverableError, logError, retryOperation } from "$lib/utils/errorHandler"; +import { AppError } from "$lib/error.types"; +import { createError, formatError, logError } from "$lib/utils/errorUtils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; describe("errorHandler", () => { @@ -14,70 +14,40 @@ describe("errorHandler", () => { vi.restoreAllMocks(); }); - describe("handleError", () => { - it("should handle AppError with recoverable flag", () => { + describe("formatError", () => { + it("should handle AppError", () => { const error = new AppError("Test error", "TEST_ERROR", true); - const result = handleError(error); + const result = formatError(error); - expect(result).toBe("Ошибка: Test error. Попробуйте снова."); - }); - - it("should handle AppError with non-recoverable flag", () => { - const error = new AppError("Critical error", "CRITICAL_ERROR", false); - const result = handleError(error); - - expect(result).toBe("Критическая ошибка: Critical error"); + expect(result).toBe("Произошла ошибка: Test error"); }); it("should handle standard Error objects", () => { const error = new Error("Standard error message"); - const result = handleError(error, "Custom prefix"); + const result = formatError(error, "Custom prefix"); expect(result).toBe("Custom prefix: Standard error message"); }); it("should handle string errors", () => { - const result = handleError("String error", "Custom prefix"); + const result = formatError("String error", "Custom prefix"); expect(result).toBe("Custom prefix: String error"); }); it("should handle unknown error types", () => { - const result = handleError({ some: "object" }, "Custom prefix"); + const result = formatError({ some: "object" }, "Custom prefix"); expect(result).toBe("Custom prefix: Произошла неизвестная ошибка"); }); it("should use default message when none provided", () => { - const result = handleError({ some: "object" }); + const result = formatError({ some: "object" }); expect(result).toBe("Произошла ошибка: Произошла неизвестная ошибка"); }); }); - describe("isRecoverableError", () => { - it("should return true for recoverable AppError", () => { - const error = new AppError("Test error", "TEST_ERROR", true); - const result = isRecoverableError(error); - - expect(result).toBe(true); - }); - - it("should return false for non-recoverable AppError", () => { - const error = new AppError("Test error", "TEST_ERROR", false); - const result = isRecoverableError(error); - - expect(result).toBe(false); - }); - - it("should return false for non-AppError objects", () => { - const error = new Error("Standard error"); - const result = isRecoverableError(error); - - expect(result).toBe(false); - }); - }); - describe("createError", () => { it("should create AppError with default recoverable flag", () => { const error = createError("Test message", "TEST_CODE"); @@ -85,19 +55,11 @@ describe("errorHandler", () => { expect(error).toBeInstanceOf(AppError); expect(error.message).toBe("Test message"); expect(error.code).toBe("TEST_CODE"); - expect(error.recoverable).toBe(true); - }); - - it("should create AppError with custom recoverable flag", () => { - const error = createError("Test message", "TEST_CODE", false); - - expect(error).toBeInstanceOf(AppError); - expect(error.recoverable).toBe(false); }); it("should create AppError with details", () => { const details = { some: "additional info" }; - const error = createError("Test message", "TEST_CODE", true, details); + const error = createError("Test message", "TEST_CODE", details); expect(error.details).toBe(details); }); @@ -144,41 +106,4 @@ describe("errorHandler", () => { }); }); }); - - describe("retryOperation", () => { - it("should succeed on first attempt", async () => { - const operation = vi.fn().mockResolvedValue("success"); - - const result = await retryOperation(operation, 3); - - expect(result).toBe("success"); - expect(operation).toHaveBeenCalledTimes(1); - }); - - it("should retry on failure and succeed", async () => { - const operation = vi.fn().mockRejectedValueOnce(new Error("First failure")).mockResolvedValueOnce("success"); - - const result = await retryOperation(operation, 3, 10); - - expect(result).toBe("success"); - expect(operation).toHaveBeenCalledTimes(2); - }); - - it("should throw after max retries", async () => { - const operation = vi.fn().mockRejectedValue(new Error("Persistent failure")); - - await expect(retryOperation(operation, 2, 10)).rejects.toThrow("Persistent failure"); - expect(operation).toHaveBeenCalledTimes(2); // Initial + 1 retry - }); - - it("should wait between retries", async () => { - const operation = vi.fn().mockRejectedValueOnce(new Error("First failure")).mockResolvedValueOnce("success"); - - const startTime = Date.now(); - await retryOperation(operation, 2, 50); - const endTime = Date.now(); - - expect(endTime - startTime).toBeGreaterThanOrEqual(50); - }); - }); }); diff --git a/tests/unit/panelStorage.test.ts b/tests/unit/panelStorage.test.ts deleted file mode 100644 index 8866e16..0000000 --- a/tests/unit/panelStorage.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -import type { Panel } from "$lib/types/panel"; -import { PanelStorage } from "$lib/utils/panelStorage"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock the error handler to avoid console noise during tests -vi.mock("$lib/utils/errorHandler", () => ({ - logError: vi.fn(), - handleError: vi.fn((error: any, message: string) => `${message}: ${error}`), -})); - -describe("PanelStorage", () => { - let storage: PanelStorage; - let mockPanel: Panel; - let mockLocalStorage: any; - - beforeEach(() => { - storage = new PanelStorage(); - mockPanel = { - id: "test-panel-1", - backgroundImage: "/backgrounds/b1.jpg", - text: { - id: "test-text-1", - text: "Test Panel", - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center", - paddingX: 10, - verticalOffset: 0, - }, - height: 100, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), - }; - - // Create a properly typed mock for localStorage - mockLocalStorage = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), - }; - - // Replace window.localStorage with our mock - Object.defineProperty(window, "localStorage", { - value: mockLocalStorage, - writable: true, - configurable: true, - }); - - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe("savePanel", () => { - it("should save a new panel successfully", () => { - const result = storage.savePanel(mockPanel); - - expect(result.success).toBe(true); - expect(mockLocalStorage.setItem).toHaveBeenCalledWith("twitch-panels", expect.stringContaining("test-panel-1")); - }); - - it("should update existing panel", () => { - // Save initial panel - storage.savePanel(mockPanel); - - // Update panel - const updatedPanel = { ...mockPanel, height: 150 }; - const result = storage.savePanel(updatedPanel); - - expect(result.success).toBe(true); - - // Get the last call to localStorage.setItem - const calls = mockLocalStorage.setItem.mock.calls; - const savedData = JSON.parse(calls[calls.length - 1][1]); - expect(savedData[0].height).toBe(150); - }); - - it("should limit panels to MAX_PANELS count", () => { - // Create and save 51 panels (exceeding MAX_PANELS = 50) - const panels = Array.from({ length: 51 }, (_, i) => ({ - ...mockPanel, - id: `panel-${i}`, - })); - - // Mock localStorage to return the saved data for getAllPanels - let savedData: any[] = []; - mockLocalStorage.setItem.mockImplementation((key: string, value: string) => { - if (key === "twitch-panels") { - savedData = JSON.parse(value); - } - }); - - mockLocalStorage.getItem.mockImplementation((key: string) => { - if (key === "twitch-panels") { - return JSON.stringify(savedData); - } - return null; - }); - - panels.forEach((panel) => storage.savePanel(panel)); - - // Check the final state by calling getAllPanels - const finalPanels = storage.getAllPanels(); - expect(finalPanels.length).toBe(50); - expect(finalPanels[0].id).toBe("panel-50"); // Most recent panel - }); - - it("should handle localStorage errors gracefully", () => { - // Mock localStorage to throw an error - mockLocalStorage.setItem.mockImplementation(() => { - throw new Error("Storage full"); - }); - - const result = storage.savePanel(mockPanel); - - expect(result.success).toBe(false); - expect(result.error).toContain("Ошибка сохранения панели"); - }); - }); - - describe("getAllPanels", () => { - it("should return empty array when no panels exist", () => { - mockLocalStorage.getItem.mockReturnValue(null); - const panels = storage.getAllPanels(); - - expect(panels).toEqual([]); - }); - - it("should return saved panels", () => { - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - - const panels = storage.getAllPanels(); - - expect(panels).toHaveLength(1); - expect(panels[0]).toEqual(mockPanel); - }); - - it("should filter out invalid panels", () => { - const validPanel = mockPanel; - const invalidPanel = { - id: "invalid-1", - backgroundImage: "/backgrounds/b2.jpg", - // Missing required text property - height: 100, - createdAt: new Date(), - updatedAt: new Date(), - }; - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([validPanel, invalidPanel])); - - const panels = storage.getAllPanels(); - - expect(panels).toHaveLength(1); - expect(panels[0]).toEqual(validPanel); - }); - - it("should handle corrupted localStorage data", () => { - mockLocalStorage.getItem.mockReturnValue("invalid json"); - - const panels = storage.getAllPanels(); - - expect(panels).toEqual([]); - }); - }); - - describe("getPanelById", () => { - it("should return panel by id", () => { - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - - const panel = storage.getPanelById("test-panel-1"); - - expect(panel).toEqual(mockPanel); - }); - - it("should return undefined for non-existent panel", () => { - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - - const panel = storage.getPanelById("non-existent"); - - expect(panel).toBeUndefined(); - }); - - it("should handle errors gracefully", () => { - // Mock localStorage to throw an error - mockLocalStorage.getItem.mockImplementation(() => { - throw new Error("Storage error"); - }); - - const panel = storage.getPanelById("test-panel-1"); - - expect(panel).toBeUndefined(); - }); - }); - - describe("deletePanel", () => { - it("should delete panel by id", () => { - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - - const result = storage.deletePanel("test-panel-1"); - - expect(result.success).toBe(true); - expect(mockLocalStorage.setItem).toHaveBeenCalledWith("twitch-panels", "[]"); - }); - - it("should succeed when panel does not exist", () => { - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - - const result = storage.deletePanel("non-existent"); - - expect(result.success).toBe(true); - // Should still save the unchanged array - expect(mockLocalStorage.setItem).toHaveBeenCalled(); - }); - - it("should handle localStorage errors", () => { - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - - // Mock localStorage to throw an error - mockLocalStorage.setItem.mockImplementation(() => { - throw new Error("Storage error"); - }); - - const result = storage.deletePanel("test-panel-1"); - - expect(result.success).toBe(false); - expect(result.error).toContain("Ошибка удаления панели"); - }); - }); - - describe("clearAll", () => { - it("should clear all panels", () => { - const result = storage.clearAll(); - - expect(result.success).toBe(true); - expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("twitch-panels"); - }); - - it("should handle localStorage errors", () => { - // Mock localStorage to throw an error - mockLocalStorage.removeItem.mockImplementation(() => { - throw new Error("Storage error"); - }); - - const result = storage.clearAll(); - - expect(result.success).toBe(false); - expect(result.error).toContain("Ошибка очистки хранилища"); - }); - }); - - describe("getPanelCount", () => { - it("should return correct panel count", () => { - expect(storage.getPanelCount()).toBe(0); - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - expect(storage.getPanelCount()).toBe(1); - - const secondPanel = { ...mockPanel, id: "test-panel-2" }; - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel, secondPanel])); - expect(storage.getPanelCount()).toBe(2); - }); - }); - - describe("hasSpaceForNewPanel", () => { - it("should return true when under limit", () => { - expect(storage.hasSpaceForNewPanel()).toBe(true); - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockPanel])); - expect(storage.hasSpaceForNewPanel()).toBe(true); - }); - - it("should return false when at limit", () => { - // Create and save 50 panels (MAX_PANELS limit) - const panels = Array.from({ length: 50 }, (_, i) => ({ - ...mockPanel, - id: `panel-${i}`, - })); - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify(panels)); - - expect(storage.hasSpaceForNewPanel()).toBe(false); - }); - }); - - describe("panel validation", () => { - it("should validate correct panel structure", () => { - const validPanel = mockPanel; - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([validPanel])); - const panels = storage.getAllPanels(); - - expect(panels).toHaveLength(1); - expect(panels[0]).toEqual(validPanel); - }); - - it("should reject panel with invalid height", () => { - const invalidPanel = { - ...mockPanel, - height: -100, // Negative height - }; - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([invalidPanel])); - const panels = storage.getAllPanels(); - - expect(panels).toHaveLength(0); - }); - - it("should reject panel with height exceeding maximum", () => { - const invalidPanel = { - ...mockPanel, - height: 2000, // Exceeds PANEL_HEIGHT_MAX (1000) - }; - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([invalidPanel])); - const panels = storage.getAllPanels(); - - expect(panels).toHaveLength(0); - }); - - it("should reject panel with missing required properties", () => { - const invalidPanel = { - id: "invalid-panel", - // Missing backgroundImage - text: mockPanel.text, - height: 100, - createdAt: new Date(), - updatedAt: new Date(), - } as any; - - mockLocalStorage.getItem.mockReturnValue(JSON.stringify([invalidPanel])); - const panels = storage.getAllPanels(); - - expect(panels).toHaveLength(0); - }); - }); -}); diff --git a/tests/unit/services/downloadService.test.ts b/tests/unit/services/downloadService.test.ts new file mode 100644 index 0000000..51d72be --- /dev/null +++ b/tests/unit/services/downloadService.test.ts @@ -0,0 +1,127 @@ +import { DownloadService, type DownloadItem } from "$services/downloadService"; +import { saveAs } from "file-saver"; +import JSZip from "jszip"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("file-saver", () => { + return { + saveAs: vi.fn(), + }; +}); + +vi.mock("jszip", () => { + // Создаем объект с методами заранее, чтобы иметь к ним доступ + const mockZipInstance = { + file: vi.fn().mockReturnThis(), + generateAsync: vi.fn().mockResolvedValue(new Blob([])), + }; + + // Возвращаем функцию-конструктор + return { + default: vi.fn(function () { + return mockZipInstance; + }), + }; +}); + +describe("DownloadService", () => { + let service: DownloadService; + let mockKonvaStage: any; + + beforeEach(() => { + service = new DownloadService(); + vi.clearAllMocks(); + + mockKonvaStage = { + toBlob: vi.fn(({ callback }) => { + callback(new Blob(["test image data"], { type: "image/png" })); + }), + }; + }); + + describe("downloadPanel", () => { + it("should export panel successfully", async () => { + const result = await service.downloadPanel(mockKonvaStage, "test-panel-1"); + + const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0]; + + expect(result.success).toBe(true); + expect(mockKonvaStage.toBlob).toHaveBeenCalled(); + expect(blobArg instanceof Blob).toBe(true); + expect(fileNameArg).toBe("test-panel-1.png"); + }); + + it("should handle Konva stage without toBlob method", async () => { + const invalidStage = { toBlob: undefined }; + + const result = await service.downloadPanel(invalidStage as any, "test-panel-1.png"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain("Konva Stage не найден или не поддерживает toBlob"); + } + }); + + it("should handle blob creation failure", async () => { + mockKonvaStage.toBlob = vi.fn(({ callback }) => { + callback(null); + }); + + const result = await service.downloadPanel(mockKonvaStage, "test-panel-1.png"); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain("Не удалось создать изображение"); + } + }); + }); + + describe("downloadAllPanels", () => { + it("should handle successful download of multiple panels", async () => { + const panels: Array = [{ filename: "test-panel-1", stage: mockKonvaStage }]; + const result = await service.downloadAll(panels); + const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0]; + const zipInstance = vi.mocked(JSZip).mock.instances[0]; + + expect(result.success).toBe(true); + expect(mockKonvaStage.toBlob).toHaveBeenCalled(); + expect(blobArg instanceof Blob).toBe(true); + expect(fileNameArg).toBe("panels.zip"); + expect(zipInstance.file).toHaveBeenCalledTimes(1); + expect(zipInstance.file).toHaveBeenCalledWith("test-panel-1.png", expect.anything()); + }); + + it("should add to zip all files", async () => { + const panels: Array = [ + { filename: "test-panel-1", stage: mockKonvaStage }, + { filename: "test-panel-2", stage: mockKonvaStage }, + { filename: "test-panel-3", stage: mockKonvaStage }, + { filename: "test-panel-4", stage: mockKonvaStage }, + { filename: "test-panel-5", stage: mockKonvaStage }, + ]; + const result = await service.downloadAll(panels); + const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0]; + const zipInstance = vi.mocked(JSZip).mock.instances[0]; + + expect(result.success).toBe(true); + expect(mockKonvaStage.toBlob).toHaveBeenCalledTimes(5); + expect(zipInstance.file).toHaveBeenCalledTimes(5); + expect(zipInstance.file).toHaveBeenCalledWith("test-panel-1.png", expect.anything()); + expect(zipInstance.file).toHaveBeenCalledWith("test-panel-2.png", expect.anything()); + expect(zipInstance.file).toHaveBeenCalledWith("test-panel-3.png", expect.anything()); + expect(zipInstance.file).toHaveBeenCalledWith("test-panel-4.png", expect.anything()); + expect(zipInstance.file).toHaveBeenCalledWith("test-panel-5.png", expect.anything()); + }); + + it("should handle failure", async () => { + const invalidStage = { toBlob: undefined }; + const panels: Array = [{ filename: "test-panel-1", stage: invalidStage as any }]; + const result = await service.downloadAll(panels); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain("Ошибка сохранения архива"); + } + }); + }); +}); diff --git a/tests/unit/services/exportService.test.ts b/tests/unit/services/exportService.test.ts deleted file mode 100644 index 0159ceb..0000000 --- a/tests/unit/services/exportService.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { Panel } from "$lib/types/panel"; -import { ExportService } from "$services/exportService"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock file-saver -vi.mock("file-saver", () => ({ - default: { - saveAs: vi.fn(), - }, -})); - -describe("ExportService", () => { - let service: ExportService; - let mockPanel: Panel; - let mockKonvaStage: any; - - beforeEach(() => { - service = new ExportService(); - - mockPanel = { - id: "test-panel-1", - backgroundImage: "/backgrounds/b1.jpg", - text: { - id: "test-text-1", - text: "Test Panel", - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center", - paddingX: 10, - verticalOffset: 0, - }, - height: 100, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), - }; - - mockKonvaStage = { - toBlob: vi.fn(({ callback }) => { - callback(new Blob(["test image data"], { type: "image/png" })); - }), - }; - }); - - describe("exportPanel", () => { - it("should export panel successfully with valid Konva stage", async () => { - const result = await service.exportPanel(mockPanel, mockKonvaStage); - - expect(result.success).toBe(true); - expect(result.error).toBeUndefined(); - expect(mockKonvaStage.toBlob).toHaveBeenCalled(); - }); - - it("should handle missing Konva stage gracefully", async () => { - const result = await service.exportPanel(mockPanel, null as any); - - expect(result.success).toBe(false); - expect(result.error).toContain("Konva Stage не передан для экспорта"); - }); - - it("should handle Konva stage without toBlob method", async () => { - const invalidStage = { toBlob: undefined }; - - const result = await service.exportPanel(mockPanel, invalidStage as any); - - expect(result.success).toBe(false); - expect(result.error).toContain("Konva Stage не найден или не поддерживает toBlob"); - }); - - it("should handle blob creation failure", async () => { - mockKonvaStage.toBlob = vi.fn(({ callback }) => { - callback(null); // Simulate blob creation failure - }); - - const result = await service.exportPanel(mockPanel, mockKonvaStage); - - expect(result.success).toBe(false); - expect(result.error).toContain("Не удалось создать изображение"); - }); - - it("should use custom filename when provided", async () => { - const customFilename = "custom-panel-name.png"; - - const result = await service.exportPanel(mockPanel, mockKonvaStage, customFilename); - - expect(result.success).toBe(true); - // The filename is passed to file-saver, but we mocked it - }); - - it("should use default filename when custom filename not provided", async () => { - const result = await service.exportPanel(mockPanel, mockKonvaStage); - - expect(result.success).toBe(true); - // Default filename should be "twitch-panel-test-panel-1.png" - }); - }); - - describe("handleDownload", () => { - it("should handle successful download", async () => { - // Mock successful export - mockKonvaStage.toBlob = vi.fn(({ callback }) => { - callback(new Blob(["test image data"], { type: "image/png" })); - }); - - await expect(service.handleDownload(mockPanel, mockKonvaStage)).resolves.not.toThrow(); - }); - - // it("should throw error when export fails", async () => { - // // Mock failed export - // mockKonvaStage.toBlob = vi.fn(({ callback }) => { - // callback(null); - // }); - - // await expect(service.handleDownload(mockPanel, mockKonvaStage)).rejects.toThrow("Ошибка экспорта панели"); - // }); - }); - - describe("exportPanels", () => { - it("should return not implemented error", async () => { - const panels = [mockPanel, { ...mockPanel, id: "test-panel-2" }]; - - const result = await service.exportPanels(panels); - - expect(result.success).toBe(false); - expect(result.error).toBe("Пакетный экспорт еще не реализован"); - }); - }); -}); diff --git a/tests/unit/stores/panelStore.test.ts b/tests/unit/stores/panelStore.test.ts deleted file mode 100644 index bde9ed3..0000000 --- a/tests/unit/stores/panelStore.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { Panel } from "$lib/types/panel"; -import { - createEmptyPanel, - panelStore, - textSettingsStore, - updateAllTextSettings, - updatePanel, -} from "$stores/panelStore"; -import { get } from "svelte/store"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock uuid -vi.mock("uuid", () => ({ - v4: vi.fn(() => "test-uuid-12345"), -})); - -describe("panelStore", () => { - beforeEach(() => { - // Reset stores to initial state - panelStore.set(undefined); - textSettingsStore.set({ - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "left", - paddingX: 10, - verticalOffset: 0, - }); - }); - - describe("panelStore", () => { - it("should initialize with undefined value", () => { - const store = get(panelStore); - expect(store).toBeUndefined(); - }); - - it("should update panel value", () => { - const mockPanel: Panel = { - id: "test-panel-1", - backgroundImage: "/backgrounds/b1.jpg", - text: { - id: "test-text-1", - text: "Test Panel", - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center", - paddingX: 10, - verticalOffset: 0, - }, - height: 100, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), - }; - - panelStore.set(mockPanel); - - const store = get(panelStore); - expect(store).toEqual(mockPanel); - }); - }); - - describe("textSettingsStore", () => { - it("should initialize with default text settings", () => { - const settings = get(textSettingsStore); - expect(settings).toEqual({ - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "left", - paddingX: 10, - verticalOffset: 0, - }); - }); - - it("should update text settings", () => { - const newSettings = { - fontSize: 24, - fontFamily: "Helvetica", - color: "#000000", - }; - - updateAllTextSettings(newSettings); - - const settings = get(textSettingsStore); - expect(settings.fontSize).toBe(24); - expect(settings.fontFamily).toBe("Helvetica"); - expect(settings.color).toBe("#000000"); - // Other properties should remain unchanged - expect(settings.textAlign).toBe("left"); - expect(settings.paddingX).toBe(10); - expect(settings.verticalOffset).toBe(0); - }); - }); - - describe("createEmptyPanel", () => { - // it("should create panel with default settings", () => { - // const panel = createEmptyPanel(); - - // expect(panel.id).toBe("test-uuid-12345"); - // expect(panel.backgroundImage).toBe(""); - // expect(panel.height).toBe(320); // PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT - // expect(panel.text.text).toBe(""); - // expect(panel.text.fontSize).toBe(18); - // expect(panel.text.fontFamily).toBe("Arial"); - // expect(panel.text.color).toBe("#ffffff"); - // expect(panel.text.textAlign).toBe("center"); - // expect(panel.text.paddingX).toBe(10); - // expect(panel.text.verticalOffset).toBe(0); - // expect(panel.createdAt).toBeInstanceOf(Date); - // expect(panel.updatedAt).toBeInstanceOf(Date); - // }); - - it("should create panel with custom height", () => { - const customHeight = 500; - const panel = createEmptyPanel(customHeight); - - expect(panel.height).toBe(customHeight); - }); - }); - - describe("updatePanel", () => { - it("should update panel properties and timestamp", () => { - const originalPanel: Panel = { - id: "test-panel-1", - backgroundImage: "/backgrounds/b1.jpg", - text: { - id: "test-text-1", - text: "Original Text", - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center", - paddingX: 10, - verticalOffset: 0, - }, - height: 100, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), - }; - - const updates = { - backgroundImage: "/backgrounds/b2.jpg", - height: 150, - text: { - ...originalPanel.text, - text: "Updated Text", - fontSize: 24, - }, - }; - - const updatedPanel = updatePanel(originalPanel, updates); - - expect(updatedPanel.backgroundImage).toBe("/backgrounds/b2.jpg"); - expect(updatedPanel.height).toBe(150); - expect(updatedPanel.text.text).toBe("Updated Text"); - expect(updatedPanel.text.fontSize).toBe(24); - // Unchanged properties should remain the same - expect(updatedPanel.id).toBe("test-panel-1"); - expect(updatedPanel.text.fontFamily).toBe("Arial"); - expect(updatedPanel.text.color).toBe("#ffffff"); - // updatedAt should be changed to current time - expect(updatedPanel.updatedAt.getTime()).toBeGreaterThan(originalPanel.updatedAt.getTime()); - // createdAt should remain unchanged - expect(updatedPanel.createdAt).toEqual(originalPanel.createdAt); - }); - - it("should handle empty updates", () => { - const originalPanel: Panel = { - id: "test-panel-1", - backgroundImage: "/backgrounds/b1.jpg", - text: { - id: "test-text-1", - text: "Original Text", - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center", - paddingX: 10, - verticalOffset: 0, - }, - height: 100, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), - }; - - const updatedPanel = updatePanel(originalPanel, {}); - - // Should only update the updatedAt timestamp - expect(updatedPanel).toEqual({ - ...originalPanel, - updatedAt: expect.any(Date), - }); - expect(updatedPanel.updatedAt.getTime()).toBeGreaterThan(originalPanel.updatedAt.getTime()); - }); - }); -});