test: update tests
This commit is contained in:
@@ -6,6 +6,7 @@ export class AppError extends Error {
|
|||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "AppError";
|
this.name = "AppError";
|
||||||
|
if (details) this.details = details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export function formatError(error: unknown, defaultMessage: string = "Произ
|
|||||||
return `${defaultMessage}: Произошла неизвестная ошибка`;
|
return `${defaultMessage}: Произошла неизвестная ошибка`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createError(message: string, code: string, recoverable: boolean = true, details?: unknown): AppError {
|
export function createError(message: string, code: string, details?: unknown): AppError {
|
||||||
return new AppError(message, code, recoverable);
|
return new AppError(message, code, details);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logError(error: unknown, context?: string): void {
|
export function logError(error: unknown, context?: string): void {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AppError } from "$lib/types/errors";
|
import { AppError } from "$lib/error.types";
|
||||||
import { createError, handleError, isRecoverableError, logError, retryOperation } from "$lib/utils/errorHandler";
|
import { createError, formatError, logError } from "$lib/utils/errorUtils";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("errorHandler", () => {
|
describe("errorHandler", () => {
|
||||||
@@ -14,70 +14,40 @@ describe("errorHandler", () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("handleError", () => {
|
describe("formatError", () => {
|
||||||
it("should handle AppError with recoverable flag", () => {
|
it("should handle AppError", () => {
|
||||||
const error = new AppError("Test error", "TEST_ERROR", true);
|
const error = new AppError("Test error", "TEST_ERROR", true);
|
||||||
const result = handleError(error);
|
const result = formatError(error);
|
||||||
|
|
||||||
expect(result).toBe("Ошибка: Test 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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle standard Error objects", () => {
|
it("should handle standard Error objects", () => {
|
||||||
const error = new Error("Standard error message");
|
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");
|
expect(result).toBe("Custom prefix: Standard error message");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle string errors", () => {
|
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");
|
expect(result).toBe("Custom prefix: String error");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle unknown error types", () => {
|
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: Произошла неизвестная ошибка");
|
expect(result).toBe("Custom prefix: Произошла неизвестная ошибка");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use default message when none provided", () => {
|
it("should use default message when none provided", () => {
|
||||||
const result = handleError({ some: "object" });
|
const result = formatError({ some: "object" });
|
||||||
|
|
||||||
expect(result).toBe("Произошла ошибка: Произошла неизвестная ошибка");
|
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", () => {
|
describe("createError", () => {
|
||||||
it("should create AppError with default recoverable flag", () => {
|
it("should create AppError with default recoverable flag", () => {
|
||||||
const error = createError("Test message", "TEST_CODE");
|
const error = createError("Test message", "TEST_CODE");
|
||||||
@@ -85,19 +55,11 @@ describe("errorHandler", () => {
|
|||||||
expect(error).toBeInstanceOf(AppError);
|
expect(error).toBeInstanceOf(AppError);
|
||||||
expect(error.message).toBe("Test message");
|
expect(error.message).toBe("Test message");
|
||||||
expect(error.code).toBe("TEST_CODE");
|
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", () => {
|
it("should create AppError with details", () => {
|
||||||
const details = { some: "additional info" };
|
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);
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<DownloadItem> = [{ 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<DownloadItem> = [
|
||||||
|
{ 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<DownloadItem> = [{ 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("Ошибка сохранения архива");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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("Пакетный экспорт еще не реализован");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user