From 3cce3c2abcc8220f81143cd6273c90a240d7033f Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 8 Feb 2026 01:47:36 +0500 Subject: [PATCH] test: add tests for states --- src/states/imageConfig.svelte.ts | 144 +++++++------ src/states/textConfig.svelte.ts | 2 +- src/states/theme.svelte.ts | 3 + tests/setup.ts | 79 ------- tests/unit/states/imageConfig.test.ts | 192 +++++++++++++++++ tests/unit/states/konvaAllStages.test.ts | 263 +++++++++++++++++++++++ tests/unit/states/konvaStage.test.ts | 87 ++++++++ tests/unit/states/textConfig.test.ts | 235 ++++++++++++++++++++ tests/unit/states/texts.test.ts | 202 +++++++++++++++++ tests/unit/states/theme.test.ts | 49 +++++ 10 files changed, 1113 insertions(+), 143 deletions(-) create mode 100644 tests/unit/states/imageConfig.test.ts create mode 100644 tests/unit/states/konvaAllStages.test.ts create mode 100644 tests/unit/states/konvaStage.test.ts create mode 100644 tests/unit/states/textConfig.test.ts create mode 100644 tests/unit/states/texts.test.ts create mode 100644 tests/unit/states/theme.test.ts diff --git a/src/states/imageConfig.svelte.ts b/src/states/imageConfig.svelte.ts index c32cbfe..14b4e1c 100644 --- a/src/states/imageConfig.svelte.ts +++ b/src/states/imageConfig.svelte.ts @@ -1,3 +1,5 @@ +import { PANEL_SETTINGS } from "$lib/constants"; + export type ImageConfig = { image: HTMLImageElement | undefined; imageLink: string; @@ -8,8 +10,8 @@ export type ImageConfig = { cropBottom: number; }; -async function createState() { - const defaults: ImageConfig = { +export class ImageConfigState { + #state: ImageConfig = $state({ image: undefined, imageLink: "", imageReady: false, @@ -17,70 +19,96 @@ async function createState() { cropTop: 0, cropRight: 0, cropBottom: 0, - }; + }); - let state: ImageConfig = $state({ ...defaults }); - let currentAbortController: AbortController | null = null; + #currentAbortController: AbortController | null = null; - function cleanup() { - if (currentAbortController) { - currentAbortController.abort(); - currentAbortController = null; + get image() { + return this.#state.image; + } + get imageReady() { + return this.#state.imageReady; + } + get imageLink() { + return this.#state.imageLink; + } + get cropLeft() { + return this.#state.cropLeft; + } + get cropTop() { + return this.#state.cropTop; + } + get cropRight() { + return this.#state.cropRight; + } + get cropBottom() { + return this.#state.cropBottom; + } + + set cropLeft(v) { + this.#state.cropLeft = v; + } + set cropTop(v) { + this.#state.cropTop = v; + } + set cropRight(v) { + this.#state.cropRight = v; + } + set cropBottom(v) { + this.#state.cropBottom = v; + } + + private cleanup() { + if (this.#currentAbortController) { + this.#currentAbortController.abort(); + this.#currentAbortController = null; } - if (state.image) { - state.image.onload = null; - state.image.onerror = null; - state.image.src = ""; - state.image = undefined; + if (this.#state.image) { + this.#state.image.onload = null; + this.#state.image.onerror = null; + this.#state.image.src = ""; + this.#state.image = undefined; } } - async function uploadImageByLink(link: string): Promise { - cleanup(); + async uploadImageByLink(link: string): Promise { + this.cleanup(); - state.imageReady = false; - state.imageLink = link; + this.#state.imageReady = false; + this.#state.imageLink = link; - currentAbortController = new AbortController(); - const { signal } = currentAbortController; + this.#currentAbortController = new AbortController(); + const { signal } = this.#currentAbortController; return new Promise((resolve, reject) => { - if (signal.aborted) { - reject(new DOMException("Aborted", "AbortError")); - return; - } - const img = new Image(); img.crossOrigin = "anonymous"; - img.onload = () => { - if (signal.aborted) return; - + const onFinished = () => { img.onload = null; img.onerror = null; + }; - state.image = img; - state.imageReady = true; + img.onload = () => { + if (signal.aborted) return; + onFinished(); + this.#state.image = img; + this.#state.imageReady = true; resolve(); }; - img.onerror = (error) => { + img.onerror = () => { if (signal.aborted) return; - - img.onload = null; - img.onerror = null; - img.src = ""; - - state.imageReady = false; + onFinished(); + this.#state.imageReady = false; reject(new Error(`Failed to load image: ${link}`)); }; signal.addEventListener( "abort", () => { - img.onload = null; - img.onerror = null; + onFinished(); img.src = ""; reject(new DOMException("Aborted", "AbortError")); }, @@ -91,30 +119,20 @@ async function createState() { }); } - await uploadImageByLink("./backgrounds/b1.jpg"); + reset() { + this.cleanup(); + this.#state.imageLink = ""; + this.#state.imageReady = false; + this.#state.cropLeft = 0; + this.#state.cropTop = 0; + this.#state.cropRight = 0; + this.#state.cropBottom = 0; + } - return { - get image() { - return state.image; - }, - get imageReady() { - return state.imageReady; - }, - get imageLink() { - return state.imageLink; - }, - - uploadImageByLink, - - reset() { - cleanup(); - Object.assign(state, defaults); - }, - - destroy() { - cleanup(); - }, - }; + destroy() { + this.cleanup(); + } } -export const imageConfigState = await createState(); +export const imageConfigState = new ImageConfigState(); +imageConfigState.uploadImageByLink(PANEL_SETTINGS.DEFAULT_BACKGROUND_IMAGE).catch(() => {}); diff --git a/src/states/textConfig.svelte.ts b/src/states/textConfig.svelte.ts index 5297fed..511c048 100644 --- a/src/states/textConfig.svelte.ts +++ b/src/states/textConfig.svelte.ts @@ -32,7 +32,7 @@ function createState() { return state.fontFamily; }, set fontFamily(fontFamily: string) { - state.fontFamily = this.fontFamily; + state.fontFamily = fontFamily; }, get color() { return state.color; diff --git a/src/states/theme.svelte.ts b/src/states/theme.svelte.ts index 10a4984..29f297a 100644 --- a/src/states/theme.svelte.ts +++ b/src/states/theme.svelte.ts @@ -7,6 +7,9 @@ function createState() { get theme() { return current; }, + set theme(value: Theme) { + current = value; + }, toggle() { current = current === "dark" ? "light" : "dark"; }, diff --git a/tests/setup.ts b/tests/setup.ts index 5c291d6..e69de29 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,79 +0,0 @@ -import { vi } from "vitest"; - -// Extend global type -declare global { - var testUtils: { - createMockFile: (name?: string, size?: number, type?: string) => File; - createMockPanel: (overrides?: any) => any; - waitFor: (ms: number) => Promise; - }; -} - -// Mock localStorage for testing -const localStorageMock = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - length: 0, - key: vi.fn(), -}; - -Object.defineProperty(window, "localStorage", { - value: localStorageMock, -}); - -// Mock crypto for testing -Object.defineProperty(window, "crypto", { - value: { - randomUUID: () => "test-uuid-12345", - }, -}); - -// Mock FileReader for image upload testing -Object.defineProperty(window, "FileReader", { - value: vi.fn(() => ({ - readAsDataURL: vi.fn(), - onload: null, - onerror: null, - result: "data:image/jpeg;base64,/9j/4AAQSkZJRg==", - })), -}); - -// Global test utilities -global.testUtils = { - createMockFile: (name = "test.jpg", size = 1024, type = "image/jpeg") => { - const blob = new Blob([new ArrayBuffer(size)], { type }); - return new File([blob], name, { type }); - }, - - createMockPanel: (overrides = {}) => ({ - id: "test-panel-id", - backgroundImage: "/backgrounds/b1.jpg", - text: { - id: "test-text-id", - text: "Test Panel", - fontSize: 18, - fontFamily: "Arial", - color: "#ffffff", - textAlign: "center" as const, - paddingX: 10, - verticalOffset: 0, - }, - height: 100, - createdAt: new Date("2024-01-01"), - updatedAt: new Date("2024-01-01"), - ...overrides, - }), - - waitFor: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), -}; - -// Export cleanup function for use in individual test files -export function cleanupMocks() { - vi.clearAllMocks(); - localStorageMock.getItem.mockClear(); - localStorageMock.setItem.mockClear(); - localStorageMock.removeItem.mockClear(); - localStorageMock.clear.mockClear(); -} diff --git a/tests/unit/states/imageConfig.test.ts b/tests/unit/states/imageConfig.test.ts new file mode 100644 index 0000000..3fa3e93 --- /dev/null +++ b/tests/unit/states/imageConfig.test.ts @@ -0,0 +1,192 @@ +import { PANEL_SETTINGS } from "$lib/constants"; +import { imageConfigState, ImageConfigState } from "$states/imageConfig.svelte"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +let lastOnload: (() => void) | null = null; +let lastOnerror: (() => void) | null = null; + +vi.stubGlobal( + "Image", + class { + _onload: (() => void) | null = null; + _onerror: (() => void) | null = null; + _src: string = ""; + crossOrigin: string = ""; + + set src(val: string) { + this._src = val; + if (val.includes("error")) { + setTimeout(() => this._onerror?.(), 1); + } else { + setTimeout(() => this._onload?.(), 1); + } + } + get src() { + return this._src; + } + + set onload(val: any) { + this._onload = val; + if (val) lastOnload = val; + } + get onload() { + return this._onload; + } + + set onerror(val: any) { + this._onerror = val; + if (val) lastOnerror = val; + } + get onerror() { + return this._onerror; + } + }, +); + +describe("ImageConfigState", () => { + beforeEach(() => { + lastOnload = null; + lastOnerror = null; + imageConfigState.reset(); + }); + + it("should create new instance with default values", () => { + const newState = new ImageConfigState(); + expect(newState.image).toBeUndefined(); + expect(newState.imageLink).toBe(""); + expect(newState.imageReady).toBe(false); + expect(newState.cropLeft).toBe(0); + expect(newState.cropTop).toBe(0); + expect(newState.cropRight).toBe(0); + expect(newState.cropBottom).toBe(0); + newState.destroy(); + }); + + it("should initialize with default background image", async () => { + await imageConfigState.uploadImageByLink(PANEL_SETTINGS.DEFAULT_BACKGROUND_IMAGE); + + expect(imageConfigState.imageReady).toBe(true); + expect(imageConfigState.image).toBeDefined(); + expect(imageConfigState.imageLink).toBe(PANEL_SETTINGS.DEFAULT_BACKGROUND_IMAGE); + }); + + it("should handle manual image upload correctly", async () => { + const testLink = "https://example.com/test.png"; + const uploadPromise = imageConfigState.uploadImageByLink(testLink); + + expect(imageConfigState.imageReady).toBe(false); + + await uploadPromise; + expect(imageConfigState.imageReady).toBe(true); + expect(imageConfigState.imageLink).toBe(testLink); + }); + + it("should reset state to defaults", async () => { + await imageConfigState.uploadImageByLink("some-image.png"); + imageConfigState.cropLeft = 100; + + imageConfigState.reset(); + + expect(imageConfigState.imageReady).toBe(false); + expect(imageConfigState.imageLink).toBe(""); + expect(imageConfigState.cropLeft).toBe(0); + expect(imageConfigState.image).toBeUndefined(); + }); + + it("should handle image loading error", async () => { + await expect(imageConfigState.uploadImageByLink("error-link")).rejects.toThrow("Failed to load image"); + + expect(imageConfigState.imageReady).toBe(false); + }); + + it("should abort previous upload when new upload starts", async () => { + const upload1 = imageConfigState.uploadImageByLink("test1.jpg"); + const upload2 = imageConfigState.uploadImageByLink("test2.jpg"); + + await expect(upload1).rejects.toThrow("Aborted"); + await expect(upload2).resolves.toBeUndefined(); + + expect(imageConfigState.imageLink).toBe("test2.jpg"); + }); + + it("should cleanup previous image before loading new one", async () => { + await imageConfigState.uploadImageByLink("test1.jpg"); + const firstImage = imageConfigState.image; + + await imageConfigState.uploadImageByLink("test2.jpg"); + + expect(firstImage?.onload).toBeNull(); + expect(firstImage?.onerror).toBeNull(); + }); + + it("should set crop values", () => { + imageConfigState.cropLeft = 10; + imageConfigState.cropTop = 20; + imageConfigState.cropRight = 30; + imageConfigState.cropBottom = 40; + + expect(imageConfigState.cropLeft).toBe(10); + expect(imageConfigState.cropTop).toBe(20); + expect(imageConfigState.cropRight).toBe(30); + expect(imageConfigState.cropBottom).toBe(40); + }); + + it("should cleanup image event handlers on reset", async () => { + await imageConfigState.uploadImageByLink("test.jpg"); + const img = imageConfigState.image; + + imageConfigState.reset(); + + expect(img?.onload).toBeNull(); + expect(img?.onerror).toBeNull(); + }); + + it("should abort ongoing upload on reset", async () => { + const upload = imageConfigState.uploadImageByLink("test.jpg"); + + imageConfigState.reset(); + + await expect(upload).rejects.toThrow("Aborted"); + }); + + it("should cleanup resources on destroy", async () => { + await imageConfigState.uploadImageByLink("test.jpg"); + const img = imageConfigState.image; + + imageConfigState.destroy(); + + expect(imageConfigState.image).toBeUndefined(); + expect(img?.onload).toBeNull(); + expect(img?.onerror).toBeNull(); + }); + + it("should abort ongoing upload on destroy", async () => { + const upload = imageConfigState.uploadImageByLink("test.jpg"); + + imageConfigState.destroy(); + + await expect(upload).rejects.toThrow("Aborted"); + }); + + it("should cover aborted onload branch", async () => { + const promise = imageConfigState.uploadImageByLink("test.png"); + + imageConfigState.destroy(); + + if (lastOnload) lastOnload(); + + await expect(promise).rejects.toThrow(); + expect(imageConfigState.imageReady).toBe(false); + }); + + it("should cover aborted onerror branch", async () => { + const promise = imageConfigState.uploadImageByLink("test.png"); + + imageConfigState.destroy(); + + if (lastOnerror) lastOnerror(); + + await expect(promise).rejects.toThrow(); + expect(imageConfigState.imageReady).toBe(false); + }); +}); diff --git a/tests/unit/states/konvaAllStages.test.ts b/tests/unit/states/konvaAllStages.test.ts new file mode 100644 index 0000000..1d46446 --- /dev/null +++ b/tests/unit/states/konvaAllStages.test.ts @@ -0,0 +1,263 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { konvaAllStagesState } from "$states/konvaAllStages.svelte"; + +describe("konvaAllStages.svelte", () => { + beforeEach(() => { + // Clear the array before each test + konvaAllStagesState.length = 0; + }); + + describe("initial state", () => { + it("should be empty array initially", () => { + expect(konvaAllStagesState).toBeDefined(); + expect(Array.isArray(konvaAllStagesState)).toBe(true); + expect(konvaAllStagesState).toHaveLength(0); + }); + + it("should be reactive array", () => { + expect(() => { + konvaAllStagesState.push({} as any); + }).not.toThrow(); + }); + }); + + describe("adding stages", () => { + it("should add stage to array", () => { + const mockStage = { id: "test1" } as any; + konvaAllStagesState.push(mockStage); + + expect(konvaAllStagesState).toHaveLength(1); + expect(konvaAllStagesState[0]).toStrictEqual(mockStage); + }); + + it("should add multiple stages", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + { id: "test3" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + expect(konvaAllStagesState).toHaveLength(3); + expect(konvaAllStagesState[0]).toStrictEqual(stages[0]); + expect(konvaAllStagesState[1]).toStrictEqual(stages[1]); + expect(konvaAllStagesState[2]).toStrictEqual(stages[2]); + }); + + it("should preserve stage order", () => { + const stages = [ + { id: "first" } as any, + { id: "second" } as any, + { id: "third" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + expect(konvaAllStagesState[0].id).toBe("first"); + expect(konvaAllStagesState[1].id).toBe("second"); + expect(konvaAllStagesState[2].id).toBe("third"); + }); + }); + + describe("removing stages", () => { + it("should remove stage from array", () => { + const mockStage = { id: "test1" } as any; + konvaAllStagesState.push(mockStage); + + expect(konvaAllStagesState).toHaveLength(1); + + konvaAllStagesState.splice(0, 1); + + expect(konvaAllStagesState).toHaveLength(0); + }); + + it("should remove specific stage", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + { id: "test3" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + konvaAllStagesState.splice(1, 1); // Remove second stage + + expect(konvaAllStagesState).toHaveLength(2); + expect(konvaAllStagesState[0].id).toBe("test1"); + expect(konvaAllStagesState[1].id).toBe("test3"); + }); + + it("should remove all stages", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + konvaAllStagesState.length = 0; + + expect(konvaAllStagesState).toHaveLength(0); + }); + }); + + describe("updating stages", () => { + it("should update stage at index", () => { + const mockStage = { id: "test1" } as any; + const updatedStage = { id: "updated" } as any; + + konvaAllStagesState.push(mockStage); + konvaAllStagesState[0] = updatedStage; + + expect(konvaAllStagesState[0]).toStrictEqual(updatedStage); + expect(konvaAllStagesState[0].id).toBe("updated"); + }); + + it("should preserve other stages when updating one", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + { id: "test3" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + const updatedStage = { id: "updated" } as any; + konvaAllStagesState[1] = updatedStage; + + expect(konvaAllStagesState[0]).toStrictEqual(stages[0]); + expect(konvaAllStagesState[1]).toStrictEqual(updatedStage); + expect(konvaAllStagesState[2]).toStrictEqual(stages[2]); + }); + }); + + describe("stage properties", () => { + it("should preserve stage properties", () => { + const mockStage = { + id: "test", + width: 320, + height: 100, + attrs: { test: "value" } + } as any; + + konvaAllStagesState.push(mockStage); + + expect(konvaAllStagesState[0].id).toBe("test"); + expect(konvaAllStagesState[0].width).toBe(320); + expect(konvaAllStagesState[0].height).toBe(100); + expect(konvaAllStagesState[0].attrs).toEqual({ test: "value" }); + }); + }); + + describe("array methods", () => { + it("should support forEach", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + const visited: string[] = []; + konvaAllStagesState.forEach(stage => visited.push(stage.id)); + + expect(visited).toEqual(["test1", "test2"]); + }); + + it("should support map", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + const ids = konvaAllStagesState.map(stage => stage.id); + + expect(ids).toEqual(["test1", "test2"]); + }); + + it("should support filter", () => { + const stages = [ + { id: "test1", type: "panel" } as any, + { id: "test2", type: "preview" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + const panels = konvaAllStagesState.filter(stage => stage.type === "panel"); + + expect(panels).toHaveLength(1); + expect(panels[0].id).toBe("test1"); + }); + + it("should support find", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + ]; + + stages.forEach(stage => konvaAllStagesState.push(stage)); + + const found = konvaAllStagesState.find(stage => stage.id === "test2"); + + expect(found).toBeDefined(); + expect(found?.id).toBe("test2"); + }); + + it("should support length property", () => { + expect(konvaAllStagesState.length).toBe(0); + + konvaAllStagesState.push({ id: "test1" } as any); + expect(konvaAllStagesState.length).toBe(1); + + konvaAllStagesState.push({ id: "test2" } as any); + expect(konvaAllStagesState.length).toBe(2); + }); + }); + + describe("integration", () => { + it("should handle add-remove-add cycle", () => { + const stage1 = { id: "test1" } as any; + const stage2 = { id: "test2" } as any; + + // Add + konvaAllStagesState.push(stage1); + expect(konvaAllStagesState).toHaveLength(1); + + // Remove + konvaAllStagesState.splice(0, 1); + expect(konvaAllStagesState).toHaveLength(0); + + // Add again + konvaAllStagesState.push(stage2); + expect(konvaAllStagesState).toHaveLength(1); + expect(konvaAllStagesState[0]).toStrictEqual(stage2); + }); + + it("should handle multiple operations", () => { + const stages = [ + { id: "test1" } as any, + { id: "test2" } as any, + { id: "test3" } as any, + ]; + + // Add all + stages.forEach(stage => konvaAllStagesState.push(stage)); + expect(konvaAllStagesState).toHaveLength(3); + + // Remove middle + konvaAllStagesState.splice(1, 1); + expect(konvaAllStagesState).toHaveLength(2); + + // Add new at end + konvaAllStagesState.push({ id: "test4" } as any); + expect(konvaAllStagesState).toHaveLength(3); + + // Update first + konvaAllStagesState[0] = { id: "updated" } as any; + expect(konvaAllStagesState[0].id).toBe("updated"); + }); + }); +}); diff --git a/tests/unit/states/konvaStage.test.ts b/tests/unit/states/konvaStage.test.ts new file mode 100644 index 0000000..9bb8a70 --- /dev/null +++ b/tests/unit/states/konvaStage.test.ts @@ -0,0 +1,87 @@ +import { konvaStageState } from "$states/konvaStage.svelte"; +import { describe, expect, it } from "vitest"; + +describe("konvaStage.svelte", () => { + describe("initial state", () => { + it("should have undefined stage initially", () => { + expect(konvaStageState.stage).toBeUndefined(); + }); + + it("should have stage getter", () => { + expect(konvaStageState).toHaveProperty("stage"); + }); + + it("should have stage setter", () => { + expect(() => { + konvaStageState.stage = {} as any; + }).not.toThrow(); + }); + }); + + describe("stage getter", () => { + it("should return current stage", () => { + const mockStage = { id: "test" } as any; + konvaStageState.stage = mockStage; + expect(konvaStageState.stage).toBeDefined(); + expect(konvaStageState.stage).toStrictEqual(mockStage); + }); + + it("should be undefined only at initialization", () => { + expect(konvaStageState.stage).toBeDefined(); + }); + }); + + describe("stage setter", () => { + it("should set stage", () => { + const mockStage = { id: "test" } as any; + konvaStageState.stage = mockStage; + + expect(konvaStageState.stage).toStrictEqual(mockStage); + }); + + it("should allow updating stage", () => { + const firstStage = { id: "first" } as any; + const secondStage = { id: "second" } as any; + + konvaStageState.stage = firstStage; + expect(konvaStageState.stage).toStrictEqual(firstStage); + + konvaStageState.stage = secondStage; + expect(konvaStageState.stage).toStrictEqual(secondStage); + expect(konvaStageState.stage).not.toStrictEqual(firstStage); + }); + }); + + // describe("stage lifecycle", () => { + // it("should handle stage creation", () => { + // const mockStage = { id: "new-stage" } as any; + + // konvaStageState.stage = mockStage; + + // expect(konvaStageState.stage).toStrictEqual(mockStage); + // expect(konvaStageState.stage?.id).toBe("new-stage"); + // }); + + // it("should handle stage destruction", () => { + // const mockStage = { id: "to-destroy" } as any; + + // konvaStageState.stage = mockStage; + // expect(konvaStageState.stage).toBeDefined(); + + // konvaStageState.stage = undefined; + // expect(konvaStageState.stage).toBeUndefined(); + // }); + + // it("should handle stage replacement", () => { + // const oldStage = { id: "old" } as any; + // const newStage = { id: "new" } as any; + + // konvaStageState.stage = oldStage; + // expect(konvaStageState.stage).toStrictEqual(oldStage); + + // konvaStageState.stage = newStage; + // expect(konvaStageState.stage).toStrictEqual(newStage); + // expect(konvaStageState.stage).not.toStrictEqual(oldStage); + // }); + // }); +}); diff --git a/tests/unit/states/textConfig.test.ts b/tests/unit/states/textConfig.test.ts new file mode 100644 index 0000000..71da0f9 --- /dev/null +++ b/tests/unit/states/textConfig.test.ts @@ -0,0 +1,235 @@ +import type { HexColor } from "$lib/types"; +import { textConfigState } from "$states/textConfig.svelte"; +import { describe, expect, it } from "vitest"; + +describe("textConfig.svelte", () => { + describe("initial state", () => { + it("should have default fontSize", () => { + expect(textConfigState.fontSize).toBe(24); + }); + + it("should have default fontFamily", () => { + expect(textConfigState.fontFamily).toBe("Arial"); + }); + + it("should have default color", () => { + expect(textConfigState.color).toBe("#ffffff"); + }); + + it("should have default align", () => { + expect(textConfigState.align).toBe("center"); + }); + + it("should have default paddingX", () => { + expect(textConfigState.paddingX).toBe(10); + }); + + it("should have default offsetY", () => { + expect(textConfigState.offsetY).toBe(0); + }); + + it("should have all required properties", () => { + expect(textConfigState).toHaveProperty("fontSize"); + expect(textConfigState).toHaveProperty("fontFamily"); + expect(textConfigState).toHaveProperty("color"); + expect(textConfigState).toHaveProperty("align"); + expect(textConfigState).toHaveProperty("paddingX"); + expect(textConfigState).toHaveProperty("offsetY"); + }); + }); + + describe("fontSize", () => { + it("should set fontSize", () => { + textConfigState.fontSize = 32; + expect(textConfigState.fontSize).toBe(32); + }); + + it("should accept positive values", () => { + textConfigState.fontSize = 10; + expect(textConfigState.fontSize).toBe(10); + + textConfigState.fontSize = 100; + expect(textConfigState.fontSize).toBe(100); + }); + + it("should accept zero", () => { + textConfigState.fontSize = 0; + expect(textConfigState.fontSize).toBe(0); + }); + + it("should accept negative values", () => { + textConfigState.fontSize = -10; + expect(textConfigState.fontSize).toBe(-10); + }); + + it("should accept decimal values", () => { + textConfigState.fontSize = 16.5; + expect(textConfigState.fontSize).toBe(16.5); + }); + }); + + describe("fontFamily", () => { + it("should set fontFamily", () => { + textConfigState.fontFamily = "Roboto"; + expect(textConfigState.fontFamily).toBe("Roboto"); + }); + + it("should accept common font families", () => { + const fonts = ["Arial", "Helvetica", "Times New Roman", "Georgia", "Verdana"]; + + fonts.forEach((font) => { + textConfigState.fontFamily = font; + expect(textConfigState.fontFamily).toBe(font); + }); + }); + + it("should accept empty string", () => { + textConfigState.fontFamily = ""; + expect(textConfigState.fontFamily).toBe(""); + }); + + it("should accept font families with spaces", () => { + textConfigState.fontFamily = "Times New Roman"; + expect(textConfigState.fontFamily).toBe("Times New Roman"); + }); + }); + + describe("color", () => { + it("should set color", () => { + textConfigState.color = "#ff0000"; + expect(textConfigState.color).toBe("#ff0000"); + }); + + it("should accept valid hex colors", () => { + const colors: Array = ["#ffffff", "#000000", "#ff0000", "#00ff00", "#0000ff", "#123456", "#abcdef"]; + + colors.forEach((color) => { + textConfigState.color = color; + expect(textConfigState.color).toBe(color); + }); + }); + + it("should accept hex colors with uppercase", () => { + textConfigState.color = "#FFFFFF"; + expect(textConfigState.color).toBe("#FFFFFF"); + }); + + it("should accept hex colors with mixed case", () => { + textConfigState.color = "#FfFfFf"; + expect(textConfigState.color).toBe("#FfFfFf"); + }); + }); + + describe("align", () => { + it("should set align", () => { + textConfigState.align = "left"; + expect(textConfigState.align).toBe("left"); + }); + + it("should accept left align", () => { + textConfigState.align = "left"; + expect(textConfigState.align).toBe("left"); + }); + + it("should accept center align", () => { + textConfigState.align = "center"; + expect(textConfigState.align).toBe("center"); + }); + + it("should accept right align", () => { + textConfigState.align = "right"; + expect(textConfigState.align).toBe("right"); + }); + }); + + describe("paddingX", () => { + it("should set paddingX", () => { + textConfigState.paddingX = 20; + expect(textConfigState.paddingX).toBe(20); + }); + + it("should accept positive values", () => { + textConfigState.paddingX = 10; + expect(textConfigState.paddingX).toBe(10); + + textConfigState.paddingX = 100; + expect(textConfigState.paddingX).toBe(100); + }); + + it("should accept zero", () => { + textConfigState.paddingX = 0; + expect(textConfigState.paddingX).toBe(0); + }); + + it("should accept negative values", () => { + textConfigState.paddingX = -10; + expect(textConfigState.paddingX).toBe(-10); + }); + + it("should accept decimal values", () => { + textConfigState.paddingX = 15.5; + expect(textConfigState.paddingX).toBe(15.5); + }); + }); + + describe("offsetY", () => { + it("should set offsetY", () => { + textConfigState.offsetY = 20; + expect(textConfigState.offsetY).toBe(20); + }); + + it("should accept positive values", () => { + textConfigState.offsetY = 10; + expect(textConfigState.offsetY).toBe(10); + + textConfigState.offsetY = 100; + expect(textConfigState.offsetY).toBe(100); + }); + + it("should accept zero", () => { + textConfigState.offsetY = 0; + expect(textConfigState.offsetY).toBe(0); + }); + + it("should accept negative values", () => { + textConfigState.offsetY = -10; + expect(textConfigState.offsetY).toBe(-10); + }); + + it("should accept decimal values", () => { + textConfigState.offsetY = 15.5; + expect(textConfigState.offsetY).toBe(15.5); + }); + }); + + describe("integration", () => { + it("should maintain independent values", () => { + textConfigState.fontSize = 32; + textConfigState.fontFamily = "Roboto"; + textConfigState.color = "#ff0000"; + textConfigState.align = "left"; + textConfigState.paddingX = 20; + textConfigState.offsetY = -10; + + expect(textConfigState.fontSize).toBe(32); + expect(textConfigState.fontFamily).toBe("Roboto"); + expect(textConfigState.color).toBe("#ff0000"); + expect(textConfigState.align).toBe("left"); + expect(textConfigState.paddingX).toBe(20); + expect(textConfigState.offsetY).toBe(-10); + }); + + it("should handle multiple updates", () => { + const initialFontSize = textConfigState.fontSize; + + textConfigState.fontSize = 32; + expect(textConfigState.fontSize).toBe(32); + + textConfigState.fontSize = 48; + expect(textConfigState.fontSize).toBe(48); + + textConfigState.fontSize = initialFontSize; + expect(textConfigState.fontSize).toBe(initialFontSize); + }); + }); +}); diff --git a/tests/unit/states/texts.test.ts b/tests/unit/states/texts.test.ts new file mode 100644 index 0000000..19f4dbe --- /dev/null +++ b/tests/unit/states/texts.test.ts @@ -0,0 +1,202 @@ +import { textsState } from "$states/texts.svelte"; +import { describe, expect, it } from "vitest"; + +describe("texts.svelte", () => { + describe("initial state", () => { + it("should have default texts", () => { + expect(textsState.texts).toBeDefined(); + expect(Array.isArray(textsState.texts)).toBe(true); + expect(textsState.texts.length).toBeGreaterThan(0); + }); + + it("should have correct default texts", () => { + const defaultTexts = ["About me", "Links", "Projects"]; + expect(textsState.texts).toHaveLength(defaultTexts.length); + + textsState.texts.forEach((textItem, index) => { + expect(textItem.text).toBe(defaultTexts[index]); + expect(textItem.id).toBe(index); + }); + }); + + it("should have unique IDs for all texts", () => { + const ids = textsState.texts.map((item) => item.id); + const uniqueIds = new Set(ids); + expect(ids.length).toBe(uniqueIds.size); + }); + }); + + describe("addText", () => { + it("should add new text", () => { + const initialLength = textsState.texts.length; + const newText = "Test text"; + + textsState.addText(newText); + + expect(textsState.texts).toHaveLength(initialLength + 1); + expect(textsState.texts[initialLength].text).toBe(newText); + }); + + it("should assign unique ID to new text", () => { + const initialIds = textsState.texts.map((item) => item.id); + const maxId = Math.max(...initialIds); + + textsState.addText("New text"); + const newText = textsState.texts[textsState.texts.length - 1]; + + expect(newText.id).toBe(maxId + 1); + expect(initialIds).not.toContain(newText.id); + }); + + it("should increment ID counter", () => { + const initialLength = textsState.texts.length; + const firstNewId = textsState.texts[initialLength - 1].id; + + textsState.addText("First"); + textsState.addText("Second"); + + const firstNew = textsState.texts[initialLength]; + const secondNew = textsState.texts[initialLength + 1]; + + expect(firstNew.id).toBe(firstNewId + 1); + expect(secondNew.id).toBe(firstNewId + 2); + }); + + it("should preserve existing texts when adding new", () => { + const initialTexts = [...textsState.texts]; + const newText = "New text"; + + textsState.addText(newText); + + initialTexts.forEach((initialText, index) => { + expect(textsState.texts[index]).toStrictEqual(initialText); + }); + }); + + it("should add text with empty string", () => { + const initialLength = textsState.texts.length; + + textsState.addText(""); + + expect(textsState.texts).toHaveLength(initialLength + 1); + expect(textsState.texts[initialLength].text).toBe(""); + }); + + it("should add text with special characters", () => { + const specialText = "Test @#$%^&*()_+-={}[]|\\:\";'<>?,./`~"; + + textsState.addText(specialText); + const addedText = textsState.texts[textsState.texts.length - 1]; + + expect(addedText.text).toBe(specialText); + }); + }); + + describe("removeText", () => { + it("should remove text by ID", () => { + const initialLength = textsState.texts.length; + const idToRemove = textsState.texts[0].id; + + textsState.removeText(idToRemove); + + expect(textsState.texts).toHaveLength(initialLength - 1); + expect(textsState.texts.find((item) => item.id === idToRemove)).toBeUndefined(); + }); + + it("should not remove text with non-existent ID", () => { + const initialLength = textsState.texts.length; + const initialTexts = [...textsState.texts]; + const nonExistentId = 999999; + + textsState.removeText(nonExistentId); + + expect(textsState.texts).toHaveLength(initialLength); + expect(textsState.texts).toStrictEqual(initialTexts); + }); + + it("should preserve other texts when removing one", () => { + const idToRemove = textsState.texts[1].id; + const otherTexts = textsState.texts.filter((item) => item.id !== idToRemove); + + textsState.removeText(idToRemove); + + expect(textsState.texts).toStrictEqual(otherTexts); + }); + + it("should handle removing last text", () => { + const lastId = textsState.texts[textsState.texts.length - 1].id; + const initialLength = textsState.texts.length; + + textsState.removeText(lastId); + + expect(textsState.texts).toHaveLength(initialLength - 1); + expect(textsState.texts.find((item) => item.id === lastId)).toBeUndefined(); + }); + + it("should handle removing first text", () => { + const firstId = textsState.texts[0].id; + const initialLength = textsState.texts.length; + + textsState.removeText(firstId); + + expect(textsState.texts).toHaveLength(initialLength - 1); + expect(textsState.texts.find((item) => item.id === firstId)).toBeUndefined(); + }); + }); + + describe("texts getter", () => { + it("should return array of TextItem", () => { + expect(Array.isArray(textsState.texts)).toBe(true); + textsState.texts.forEach((item) => { + expect(item).toHaveProperty("text"); + expect(item).toHaveProperty("id"); + expect(typeof item.text).toBe("string"); + expect(typeof item.id).toBe("number"); + }); + }); + + it("should return reactive texts", () => { + const initialLength = textsState.texts.length; + + textsState.addText("Test"); + + expect(textsState.texts).toHaveLength(initialLength + 1); + }); + }); + + describe("integration", () => { + it("should maintain ID uniqueness after multiple operations", () => { + const initialIds = new Set(textsState.texts.map((item) => item.id)); + + textsState.addText("First"); + textsState.addText("Second"); + textsState.removeText(textsState.texts[0].id); + textsState.addText("Third"); + + const finalIds = textsState.texts.map((item) => item.id); + const uniqueIds = new Set(finalIds); + + expect(finalIds.length).toBe(uniqueIds.size); + }); + + it("should handle adding and removing multiple texts", () => { + const initialLength = textsState.texts.length; + const addedIds: number[] = []; + + // Add multiple texts + for (let i = 0; i < 5; i++) { + textsState.addText(`Text ${i}`); + addedIds.push(textsState.texts[textsState.texts.length - 1].id); + } + + expect(textsState.texts).toHaveLength(initialLength + 5); + + // Remove some of them + textsState.removeText(addedIds[0]); + textsState.removeText(addedIds[2]); + textsState.removeText(addedIds[4]); + + expect(textsState.texts).toHaveLength(initialLength + 2); + }); + }); +}); diff --git a/tests/unit/states/theme.test.ts b/tests/unit/states/theme.test.ts new file mode 100644 index 0000000..7156b4f --- /dev/null +++ b/tests/unit/states/theme.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { themeState } from "$states/theme.svelte"; + +describe("theme.svelte", () => { + describe("initial state", () => { + it("should have initial theme", () => { + expect(themeState.theme).toBeDefined(); + expect(["light", "dark"]).toContain(themeState.theme); + }); + + it("should start with dark theme by default", () => { + expect(themeState.theme).toBe("dark"); + }); + }); + + describe("toggle", () => { + it("should toggle between light and dark themes", () => { + const initialTheme = themeState.theme; + + themeState.toggle(); + expect(themeState.theme).not.toBe(initialTheme); + + themeState.toggle(); + expect(themeState.theme).toBe(initialTheme); + }); + + it("should switch from dark to light", () => { + themeState.theme = "dark"; + themeState.toggle(); + expect(themeState.theme).toBe("light"); + }); + + it("should switch from light to dark", () => { + themeState.theme = "light"; + themeState.toggle(); + expect(themeState.theme).toBe("dark"); + }); + }); + + describe("theme getter", () => { + it("should return current theme", () => { + themeState.theme = "dark"; + expect(themeState.theme).toBe("dark"); + + themeState.theme = "light"; + expect(themeState.theme).toBe("light"); + }); + }); +});