test: update coverage, add some tests
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Extend global type
|
||||
@@ -31,36 +30,6 @@ Object.defineProperty(window, "crypto", {
|
||||
},
|
||||
});
|
||||
|
||||
// Mock HTMLCanvasElement for Konva testing
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, "getContext", {
|
||||
value: vi.fn(() => ({
|
||||
fillRect: vi.fn(),
|
||||
clearRect: vi.fn(),
|
||||
getImageData: vi.fn(() => ({ data: new Array(4) })),
|
||||
putImageData: vi.fn(),
|
||||
createImageData: vi.fn(() => ({ data: new Array(4) })),
|
||||
setTransform: vi.fn(),
|
||||
drawImage: vi.fn(),
|
||||
save: vi.fn(),
|
||||
fillText: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
closePath: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
translate: vi.fn(),
|
||||
scale: vi.fn(),
|
||||
rotate: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
measureText: vi.fn(() => ({ width: 0 })),
|
||||
transform: vi.fn(),
|
||||
rect: vi.fn(),
|
||||
clip: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock FileReader for image upload testing
|
||||
Object.defineProperty(window, "FileReader", {
|
||||
value: vi.fn(() => ({
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import PanelPreview from "$components/panel/PanelPreview.svelte";
|
||||
import type { Panel } from "$lib/types/panel";
|
||||
import { fireEvent, render, screen } from "@testing-library/svelte";
|
||||
import type { Stage as KonvaStage } from "konva/lib/Stage";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock the UI store
|
||||
vi.mock("$stores/uiStore", () => ({
|
||||
setCurrentStep: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("PanelPreview", () => {
|
||||
let mockPanel: Panel;
|
||||
let mockOnDownload: (panel: Panel, konvaStage: KonvaStage) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
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"),
|
||||
};
|
||||
|
||||
mockOnDownload = vi.fn();
|
||||
});
|
||||
|
||||
it("should render panel with text content", () => {
|
||||
const { container } = render(PanelPreview, {
|
||||
props: {
|
||||
panel: mockPanel,
|
||||
onDownload: mockOnDownload,
|
||||
},
|
||||
});
|
||||
|
||||
// Should render without errors
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should call onDownload when download button is clicked", async () => {
|
||||
render(PanelPreview, {
|
||||
props: {
|
||||
panel: mockPanel,
|
||||
onDownload: mockOnDownload,
|
||||
},
|
||||
});
|
||||
|
||||
// Find and click download button - button text might vary, so we'll look for a button
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const downloadButton = buttons.find(
|
||||
(button) => button.textContent?.includes("Скачать") || button.textContent?.includes("Download"),
|
||||
);
|
||||
|
||||
if (downloadButton) {
|
||||
await fireEvent.click(downloadButton);
|
||||
// Should call onDownload with panel and konva stage
|
||||
expect(mockOnDownload).toHaveBeenCalledWith(mockPanel, expect.any(Object));
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle panel without onDownload callback", () => {
|
||||
const { container } = render(PanelPreview, {
|
||||
props: {
|
||||
panel: mockPanel,
|
||||
},
|
||||
});
|
||||
|
||||
// Should render without errors
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should display panel text content", () => {
|
||||
render(PanelPreview, {
|
||||
props: {
|
||||
panel: mockPanel,
|
||||
onDownload: mockOnDownload,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if panel text is rendered (this might be in a canvas, so we check container)
|
||||
const { container } = render(PanelPreview, {
|
||||
props: {
|
||||
panel: mockPanel,
|
||||
onDownload: mockOnDownload,
|
||||
},
|
||||
});
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
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,18 +0,0 @@
|
||||
import { PanelStorage } from "$lib/utils/panelStorage";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let storage: any;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = new PanelStorage();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("PanelStorage", () => {
|
||||
it("should create storage instance", () => {
|
||||
expect(storage).toBeInstanceOf(PanelStorage);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
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("Пакетный экспорт еще не реализован");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,22 @@ export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./tests/setup.ts"],
|
||||
include: ["tests/**/*.{test,spec}.{js,ts}"],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
enabled: true,
|
||||
include: ["src/**/*.{js,ts,svelte}"],
|
||||
exclude: [
|
||||
"src/**/*.d.ts",
|
||||
"src/**/types.ts",
|
||||
"src/**/index.ts",
|
||||
"**/*.config.*",
|
||||
"**/build/**",
|
||||
"**/node_modules/**",
|
||||
"**/coverage/**",
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: process.env.VITEST
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user