style: add eslint and prettier, fix style errors
This commit is contained in:
@@ -1,192 +1,216 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
import { PANEL_SETTINGS } from "$lib/constants";
|
||||
import { imageConfigState, ImageConfigState } from "$states/imageConfig.svelte";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type ImageEventHandler = ((this: HTMLImageElement, ev?: Event) => void) | null;
|
||||
type ImageErrorEventHandler =
|
||||
| ((
|
||||
this: HTMLImageElement,
|
||||
ev?: string | Event,
|
||||
source?: string,
|
||||
lineno?: number,
|
||||
colno?: number,
|
||||
error?: Error,
|
||||
) => void)
|
||||
| null;
|
||||
|
||||
let lastOnload: ImageEventHandler = null;
|
||||
let lastOnerror: ImageErrorEventHandler = null;
|
||||
|
||||
vi.stubGlobal(
|
||||
"Image",
|
||||
class {
|
||||
_onload: ImageEventHandler = null;
|
||||
_onerror: ImageErrorEventHandler = null;
|
||||
_src: string = "";
|
||||
crossOrigin: string = "";
|
||||
|
||||
set src(val: string) {
|
||||
this._src = val;
|
||||
if (val.includes("error")) {
|
||||
setTimeout(
|
||||
() => this._onerror?.call(this as unknown as HTMLImageElement, new Event("error")),
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
setTimeout(
|
||||
() => this._onload?.call(this as unknown as HTMLImageElement, new Event("load")),
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
get src() {
|
||||
return this._src;
|
||||
}
|
||||
|
||||
set onload(val: ImageEventHandler) {
|
||||
this._onload = val;
|
||||
if (val) lastOnload = val;
|
||||
}
|
||||
get onload() {
|
||||
return this._onload;
|
||||
}
|
||||
|
||||
set onerror(val: ImageErrorEventHandler) {
|
||||
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.call(new Image() as HTMLImageElement, new Event("load"));
|
||||
}
|
||||
|
||||
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.call(new Image() as HTMLImageElement, new Event("load"));
|
||||
}
|
||||
|
||||
await expect(promise).rejects.toThrow();
|
||||
expect(imageConfigState.imageReady).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("konvaAllStages.svelte", () => {
|
||||
it("should import successfully", () => {
|
||||
expect(konvaAllStagesState).toBeDefined();
|
||||
});
|
||||
});
|
||||
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("konvaAllStages.svelte", () => {
|
||||
it("should import successfully", () => {
|
||||
expect(konvaAllStagesState).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,54 +1,14 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { konvaStageState } from "$states/konvaStage.svelte";
|
||||
import type { Stage } from "svelte-konva";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("konvaStageState integration", () => {
|
||||
it("should work", () => {
|
||||
expect(konvaStageState.stage).toBeUndefined();
|
||||
|
||||
const mock = { name: "stage" } as unknown as Stage;
|
||||
konvaStageState.stage = mock;
|
||||
|
||||
expect(konvaStageState.stage).toStrictEqual(mock);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,158 +1,167 @@
|
||||
import { STATE_DATA, withPersistence } from "$states/persisted.svelte";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store[key] = value;
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete store[key];
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store = {};
|
||||
}),
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
describe("persisted.svelte", () => {
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("withPersistence", () => {
|
||||
it("should return state unchanged when not in browser", () => {
|
||||
const mockState = {
|
||||
[STATE_DATA]: { test: "value" },
|
||||
};
|
||||
const result = withPersistence("test-key", mockState);
|
||||
expect(result).toBe(mockState);
|
||||
});
|
||||
|
||||
it("should restore state from localStorage when valid JSON exists", () => {
|
||||
const savedData = { value: 42, name: "test" };
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(savedData));
|
||||
|
||||
const state = {
|
||||
[STATE_DATA]: { value: 0, name: "" },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state);
|
||||
|
||||
expect(state[STATE_DATA]).toEqual(savedData);
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith("test-key");
|
||||
});
|
||||
|
||||
it("should handle corrupted JSON in localStorage gracefully", () => {
|
||||
localStorageMock.getItem.mockReturnValue("{ invalid json");
|
||||
|
||||
const state = {
|
||||
[STATE_DATA]: { value: 0 },
|
||||
};
|
||||
|
||||
expect(() => withPersistence("test-key", state)).not.toThrow();
|
||||
expect(state[STATE_DATA]).toEqual({ value: 0 });
|
||||
});
|
||||
|
||||
it("should handle empty localStorage (no saved data)", () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
const state = {
|
||||
[STATE_DATA]: { value: 0 },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state);
|
||||
|
||||
expect(state[STATE_DATA]).toEqual({ value: 0 });
|
||||
});
|
||||
|
||||
it("should save state to localStorage with debounce", async () => {
|
||||
vi.useFakeTimers();
|
||||
const state = {
|
||||
[STATE_DATA]: { count: 1 },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state, 500);
|
||||
|
||||
state[STATE_DATA] = { count: 2 };
|
||||
await Promise.resolve();
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 2 }));
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should save state immediately when debounce is 0", async () => {
|
||||
const state = {
|
||||
[STATE_DATA]: { count: 1 },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state, 0);
|
||||
|
||||
state[STATE_DATA] = { count: 2 };
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 2 }));
|
||||
});
|
||||
|
||||
it("should cleanup timeout on state change during debounce", async () => {
|
||||
vi.useFakeTimers();
|
||||
const data = $state({ count: 1 });
|
||||
|
||||
const state = {
|
||||
get [STATE_DATA]() {
|
||||
return data;
|
||||
},
|
||||
set [STATE_DATA](v) {
|
||||
data.count = v.count;
|
||||
},
|
||||
};
|
||||
|
||||
withPersistence("test-key", state, 500);
|
||||
|
||||
state[STATE_DATA] = { count: 2 };
|
||||
await Promise.resolve();
|
||||
|
||||
state[STATE_DATA] = { count: 3 };
|
||||
await Promise.resolve();
|
||||
|
||||
vi.runOnlyPendingTimers();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 3 }));
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should use DEBOUNCE_DURATION constant as default", async () => {
|
||||
const state = {
|
||||
[STATE_DATA]: { test: true },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state);
|
||||
|
||||
state[STATE_DATA] = { test: false };
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
import { STATE_DATA, withPersistence } from "$states/persisted.svelte";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] || null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store[key] = value;
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete store[key];
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store = {};
|
||||
}),
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
describe("persisted.svelte", () => {
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("withPersistence", () => {
|
||||
it("should return state unchanged when not in browser", () => {
|
||||
const mockState = {
|
||||
[STATE_DATA]: { test: "value" },
|
||||
};
|
||||
const result = withPersistence("test-key", mockState);
|
||||
expect(result).toBe(mockState);
|
||||
});
|
||||
|
||||
it("should restore state from localStorage when valid JSON exists", () => {
|
||||
const savedData = { value: 42, name: "test" };
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(savedData));
|
||||
|
||||
const state = {
|
||||
[STATE_DATA]: { value: 0, name: "" },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state);
|
||||
|
||||
expect(state[STATE_DATA]).toEqual(savedData);
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith("test-key");
|
||||
});
|
||||
|
||||
it("should handle corrupted JSON in localStorage gracefully", () => {
|
||||
localStorageMock.getItem.mockReturnValue("{ invalid json");
|
||||
|
||||
const state = {
|
||||
[STATE_DATA]: { value: 0 },
|
||||
};
|
||||
|
||||
expect(() => withPersistence("test-key", state)).not.toThrow();
|
||||
expect(state[STATE_DATA]).toEqual({ value: 0 });
|
||||
});
|
||||
|
||||
it("should handle empty localStorage (no saved data)", () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
const state = {
|
||||
[STATE_DATA]: { value: 0 },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state);
|
||||
|
||||
expect(state[STATE_DATA]).toEqual({ value: 0 });
|
||||
});
|
||||
|
||||
it("should save state to localStorage with debounce", async () => {
|
||||
vi.useFakeTimers();
|
||||
const state = {
|
||||
[STATE_DATA]: { count: 1 },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state, 500);
|
||||
|
||||
state[STATE_DATA] = { count: 2 };
|
||||
await Promise.resolve();
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
"test-key",
|
||||
JSON.stringify({ count: 2 }),
|
||||
);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should save state immediately when debounce is 0", async () => {
|
||||
const state = {
|
||||
[STATE_DATA]: { count: 1 },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state, 0);
|
||||
|
||||
state[STATE_DATA] = { count: 2 };
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
"test-key",
|
||||
JSON.stringify({ count: 2 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should cleanup timeout on state change during debounce", async () => {
|
||||
vi.useFakeTimers();
|
||||
const data = $state({ count: 1 });
|
||||
|
||||
const state = {
|
||||
get [STATE_DATA]() {
|
||||
return data;
|
||||
},
|
||||
set [STATE_DATA](v) {
|
||||
data.count = v.count;
|
||||
},
|
||||
};
|
||||
|
||||
withPersistence("test-key", state, 500);
|
||||
|
||||
state[STATE_DATA] = { count: 2 };
|
||||
await Promise.resolve();
|
||||
|
||||
state[STATE_DATA] = { count: 3 };
|
||||
await Promise.resolve();
|
||||
|
||||
vi.runOnlyPendingTimers();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
"test-key",
|
||||
JSON.stringify({ count: 3 }),
|
||||
);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should use DEBOUNCE_DURATION constant as default", async () => {
|
||||
const state = {
|
||||
[STATE_DATA]: { test: true },
|
||||
};
|
||||
|
||||
withPersistence("test-key", state);
|
||||
|
||||
state[STATE_DATA] = { test: false };
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
import { TextAlign } from "$lib/constants";
|
||||
import type { HexColor } from "$lib/types";
|
||||
import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { textConfigState } from "$states/textConfig.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const TEXT_ALIGN_VALUES = Object.values(TextAlign);
|
||||
|
||||
describe("textConfig.svelte", () => {
|
||||
describe("initial state", () => {
|
||||
it("should have valid initial state structure", () => {
|
||||
expect(typeof textConfigState.fontSize).toBe("number");
|
||||
expect(typeof textConfigState.fontFamily).toBe("string");
|
||||
expect(typeof textConfigState.color).toBe("string");
|
||||
expect(textConfigState.color).toMatch(/^#[0-9a-fA-F]{6}$/);
|
||||
expect(TEXT_ALIGN_VALUES).toContain(textConfigState.align);
|
||||
expect(typeof textConfigState.paddingX).toBe("number");
|
||||
expect(typeof textConfigState.offsetY).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("property setters", () => {
|
||||
it("should update all properties correctly", () => {
|
||||
textConfigState.fontSize = 32;
|
||||
textConfigState.fontFamily = "Roboto";
|
||||
textConfigState.color = "#ff0000" as HexColor;
|
||||
textConfigState.align = TextAlign.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(TextAlign.LEFT);
|
||||
expect(textConfigState.paddingX).toBe(20);
|
||||
expect(textConfigState.offsetY).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("STATE_DATA", () => {
|
||||
it("should serialize and deserialize state", () => {
|
||||
// Set custom values
|
||||
textConfigState.fontSize = 18;
|
||||
textConfigState.fontFamily = "Georgia";
|
||||
textConfigState.color = "#00ff00" as HexColor;
|
||||
textConfigState.align = TextAlign.RIGHT;
|
||||
textConfigState.paddingX = 5;
|
||||
textConfigState.offsetY = 15;
|
||||
|
||||
// Get serialized data
|
||||
const data = textConfigState[STATE_DATA];
|
||||
expect(data).toEqual({
|
||||
fontSize: 18,
|
||||
fontFamily: "Georgia",
|
||||
color: "#00ff00",
|
||||
align: TextAlign.RIGHT,
|
||||
paddingX: 5,
|
||||
offsetY: 15,
|
||||
});
|
||||
|
||||
// Restore from serialized data
|
||||
textConfigState[STATE_DATA] = {
|
||||
fontSize: 24,
|
||||
fontFamily: "Arial",
|
||||
color: "#ffffff" as HexColor,
|
||||
align: TextAlign.CENTER,
|
||||
paddingX: 10,
|
||||
offsetY: 0,
|
||||
};
|
||||
|
||||
expect(textConfigState.fontSize).toBe(24);
|
||||
expect(textConfigState.fontFamily).toBe("Arial");
|
||||
expect(textConfigState.color).toBe("#ffffff");
|
||||
expect(textConfigState.align).toBe(TextAlign.CENTER);
|
||||
expect(textConfigState.paddingX).toBe(10);
|
||||
expect(textConfigState.offsetY).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { TextAlign } from "$lib/constants";
|
||||
import type { HexColor } from "$lib/types";
|
||||
import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { textConfigState } from "$states/textConfig.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const TEXT_ALIGN_VALUES = Object.values(TextAlign);
|
||||
|
||||
describe("textConfig.svelte", () => {
|
||||
describe("initial state", () => {
|
||||
it("should have valid initial state structure", () => {
|
||||
expect(typeof textConfigState.fontSize).toBe("number");
|
||||
expect(typeof textConfigState.fontFamily).toBe("string");
|
||||
expect(typeof textConfigState.color).toBe("string");
|
||||
expect(textConfigState.color).toMatch(/^#[0-9a-fA-F]{6}$/);
|
||||
expect(TEXT_ALIGN_VALUES).toContain(textConfigState.align);
|
||||
expect(typeof textConfigState.paddingX).toBe("number");
|
||||
expect(typeof textConfigState.offsetY).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("property setters", () => {
|
||||
it("should update all properties correctly", () => {
|
||||
textConfigState.fontSize = 32;
|
||||
textConfigState.fontFamily = "Roboto";
|
||||
textConfigState.color = "#ff0000" as HexColor;
|
||||
textConfigState.align = TextAlign.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(TextAlign.LEFT);
|
||||
expect(textConfigState.paddingX).toBe(20);
|
||||
expect(textConfigState.offsetY).toBe(-10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("STATE_DATA", () => {
|
||||
it("should serialize and deserialize state", () => {
|
||||
// Set custom values
|
||||
textConfigState.fontSize = 18;
|
||||
textConfigState.fontFamily = "Georgia";
|
||||
textConfigState.color = "#00ff00" as HexColor;
|
||||
textConfigState.align = TextAlign.RIGHT;
|
||||
textConfigState.paddingX = 5;
|
||||
textConfigState.offsetY = 15;
|
||||
|
||||
// Get serialized data
|
||||
const data = textConfigState[STATE_DATA];
|
||||
expect(data).toEqual({
|
||||
fontSize: 18,
|
||||
fontFamily: "Georgia",
|
||||
color: "#00ff00",
|
||||
align: TextAlign.RIGHT,
|
||||
paddingX: 5,
|
||||
offsetY: 15,
|
||||
});
|
||||
|
||||
// Restore from serialized data
|
||||
textConfigState[STATE_DATA] = {
|
||||
fontSize: 24,
|
||||
fontFamily: "Arial",
|
||||
color: "#ffffff" as HexColor,
|
||||
align: TextAlign.CENTER,
|
||||
paddingX: 10,
|
||||
offsetY: 0,
|
||||
};
|
||||
|
||||
expect(textConfigState.fontSize).toBe(24);
|
||||
expect(textConfigState.fontFamily).toBe("Arial");
|
||||
expect(textConfigState.color).toBe("#ffffff");
|
||||
expect(textConfigState.align).toBe(TextAlign.CENTER);
|
||||
expect(textConfigState.paddingX).toBe(10);
|
||||
expect(textConfigState.offsetY).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { createState } from "$states/texts.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("texts.svelte", () => {
|
||||
describe("addText", () => {
|
||||
it("should add new text with unique ID", () => {
|
||||
const state = createState();
|
||||
const initialLength = state.texts.length;
|
||||
|
||||
state.addText("Test text");
|
||||
|
||||
expect(state.texts).toHaveLength(initialLength + 1);
|
||||
expect(state.texts[initialLength].text).toBe("Test text");
|
||||
expect(state.texts[initialLength].id).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not add empty text", () => {
|
||||
const state = createState();
|
||||
const initialLength = state.texts.length;
|
||||
|
||||
state.addText("");
|
||||
state.addText(" ");
|
||||
|
||||
expect(state.texts).toHaveLength(initialLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeText", () => {
|
||||
it("should remove text by ID", () => {
|
||||
const state = createState();
|
||||
const idToRemove = state.texts[0].id;
|
||||
const initialLength = state.texts.length;
|
||||
|
||||
state.removeText(idToRemove);
|
||||
|
||||
expect(state.texts).toHaveLength(initialLength - 1);
|
||||
expect(state.texts.find((item) => item.id === idToRemove)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not affect other texts when removing", () => {
|
||||
const state = createState();
|
||||
const remainingTexts = state.texts.filter((item) => item.id !== state.texts[0].id);
|
||||
const idToRemove = state.texts[0].id;
|
||||
|
||||
state.removeText(idToRemove);
|
||||
|
||||
expect(state.texts).toStrictEqual(remainingTexts);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("should remove all texts", () => {
|
||||
const state = createState();
|
||||
state.addText("Test 1");
|
||||
state.addText("Test 2");
|
||||
state.addText("Test 3");
|
||||
|
||||
state.clear();
|
||||
|
||||
expect(state.texts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("STATE_DATA", () => {
|
||||
it("should serialize and deserialize texts", () => {
|
||||
const state = createState();
|
||||
state.clear();
|
||||
state.addText("First");
|
||||
state.addText("Second");
|
||||
state.addText("Third");
|
||||
|
||||
const data = state[STATE_DATA];
|
||||
expect(data).toEqual(["First", "Second", "Third"]);
|
||||
|
||||
// Restore from serialized data
|
||||
state[STATE_DATA] = ["New 1", "New 2"];
|
||||
|
||||
expect(state.texts).toHaveLength(2);
|
||||
expect(state.texts[0].text).toBe("New 1");
|
||||
expect(state.texts[1].text).toBe("New 2");
|
||||
expect(state.texts[0].id).toBe(0);
|
||||
expect(state.texts[1].id).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const state = createState();
|
||||
state.addText("Test");
|
||||
|
||||
state[STATE_DATA] = [];
|
||||
|
||||
expect(state.texts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { createState } from "$states/texts.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("texts.svelte", () => {
|
||||
describe("addText", () => {
|
||||
it("should add new text with unique ID", () => {
|
||||
const state = createState();
|
||||
const initialLength = state.texts.length;
|
||||
|
||||
state.addText("Test text");
|
||||
|
||||
expect(state.texts).toHaveLength(initialLength + 1);
|
||||
expect(state.texts[initialLength].text).toBe("Test text");
|
||||
expect(state.texts[initialLength].id).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not add empty text", () => {
|
||||
const state = createState();
|
||||
const initialLength = state.texts.length;
|
||||
|
||||
state.addText("");
|
||||
state.addText(" ");
|
||||
|
||||
expect(state.texts).toHaveLength(initialLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeText", () => {
|
||||
it("should remove text by ID", () => {
|
||||
const state = createState();
|
||||
const idToRemove = state.texts[0].id;
|
||||
const initialLength = state.texts.length;
|
||||
|
||||
state.removeText(idToRemove);
|
||||
|
||||
expect(state.texts).toHaveLength(initialLength - 1);
|
||||
expect(state.texts.find((item) => item.id === idToRemove)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not affect other texts when removing", () => {
|
||||
const state = createState();
|
||||
const remainingTexts = state.texts.filter((item) => item.id !== state.texts[0].id);
|
||||
const idToRemove = state.texts[0].id;
|
||||
|
||||
state.removeText(idToRemove);
|
||||
|
||||
expect(state.texts).toStrictEqual(remainingTexts);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("should remove all texts", () => {
|
||||
const state = createState();
|
||||
state.addText("Test 1");
|
||||
state.addText("Test 2");
|
||||
state.addText("Test 3");
|
||||
|
||||
state.clear();
|
||||
|
||||
expect(state.texts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("STATE_DATA", () => {
|
||||
it("should serialize and deserialize texts", () => {
|
||||
const state = createState();
|
||||
state.clear();
|
||||
state.addText("First");
|
||||
state.addText("Second");
|
||||
state.addText("Third");
|
||||
|
||||
const data = state[STATE_DATA];
|
||||
expect(data).toEqual(["First", "Second", "Third"]);
|
||||
|
||||
// Restore from serialized data
|
||||
state[STATE_DATA] = ["New 1", "New 2"];
|
||||
|
||||
expect(state.texts).toHaveLength(2);
|
||||
expect(state.texts[0].text).toBe("New 1");
|
||||
expect(state.texts[1].text).toBe("New 2");
|
||||
expect(state.texts[0].id).toBe(0);
|
||||
expect(state.texts[1].id).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const state = createState();
|
||||
state.addText("Test");
|
||||
|
||||
state[STATE_DATA] = [];
|
||||
|
||||
expect(state.texts).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import { Theme } from "$lib/constants";
|
||||
import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { themeState } from "$states/theme.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const THEME_VALUES = Object.values(Theme);
|
||||
|
||||
describe("theme.svelte", () => {
|
||||
describe("initial state", () => {
|
||||
it("should have valid initial theme value", () => {
|
||||
expect(THEME_VALUES).toContain(themeState.current);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggle", () => {
|
||||
it("should toggle between themes", () => {
|
||||
themeState.current = Theme.LIGHT;
|
||||
themeState.toggle();
|
||||
expect(themeState.current).toBe(Theme.DARK);
|
||||
|
||||
themeState.toggle();
|
||||
expect(themeState.current).toBe(Theme.LIGHT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("current setter", () => {
|
||||
it("should set theme correctly", () => {
|
||||
themeState.current = Theme.DARK;
|
||||
expect(themeState.current).toBe(Theme.DARK);
|
||||
|
||||
themeState.current = Theme.LIGHT;
|
||||
expect(themeState.current).toBe(Theme.LIGHT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("STATE_DATA", () => {
|
||||
it("should serialize and deserialize theme", () => {
|
||||
themeState.current = Theme.DARK;
|
||||
expect(themeState[STATE_DATA]).toEqual({ current: Theme.DARK });
|
||||
|
||||
themeState[STATE_DATA] = { current: Theme.LIGHT };
|
||||
expect(themeState.current).toBe(Theme.LIGHT);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { Theme } from "$lib/constants";
|
||||
import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { themeState } from "$states/theme.svelte";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const THEME_VALUES = Object.values(Theme);
|
||||
|
||||
describe("theme.svelte", () => {
|
||||
describe("initial state", () => {
|
||||
it("should have valid initial theme value", () => {
|
||||
expect(THEME_VALUES).toContain(themeState.current);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggle", () => {
|
||||
it("should toggle between themes", () => {
|
||||
themeState.current = Theme.LIGHT;
|
||||
themeState.toggle();
|
||||
expect(themeState.current).toBe(Theme.DARK);
|
||||
|
||||
themeState.toggle();
|
||||
expect(themeState.current).toBe(Theme.LIGHT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("current setter", () => {
|
||||
it("should set theme correctly", () => {
|
||||
themeState.current = Theme.DARK;
|
||||
expect(themeState.current).toBe(Theme.DARK);
|
||||
|
||||
themeState.current = Theme.LIGHT;
|
||||
expect(themeState.current).toBe(Theme.LIGHT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("STATE_DATA", () => {
|
||||
it("should serialize and deserialize theme", () => {
|
||||
themeState.current = Theme.DARK;
|
||||
expect(themeState[STATE_DATA]).toEqual({ current: Theme.DARK });
|
||||
|
||||
themeState[STATE_DATA] = { current: Theme.LIGHT };
|
||||
expect(themeState.current).toBe(Theme.LIGHT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user