diff --git a/src/app.html b/src/app.html
index f273cc5..258219d 100644
--- a/src/app.html
+++ b/src/app.html
@@ -1,11 +1,24 @@
-
-
-
- %sveltekit.head%
-
-
- %sveltekit.body%
-
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
diff --git a/src/components/layout/AppHeader.svelte b/src/components/layout/AppHeader.svelte
index 12f97a9..f21de50 100644
--- a/src/components/layout/AppHeader.svelte
+++ b/src/components/layout/AppHeader.svelte
@@ -12,8 +12,8 @@
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 018d72f..68ea78f 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -66,4 +66,11 @@ export const TextAlign = {
export type TextAlignType = (typeof TextAlign)[keyof typeof TextAlign];
export const DEFAULT_TEXT_ALIGN: TextAlignType = TextAlign.CENTER;
+export const Theme = {
+ DARK: "dark",
+ LIGHT: "light",
+} as const;
+
+export type ThemeType = (typeof Theme)[keyof typeof Theme];
+
export const TRANSITION_DURATION = import.meta.env.MODE === "test" ? 0 : 300;
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 98e58dc..8326c5e 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -10,9 +10,8 @@
}
$effect(() => {
- const newTheme = themeState.theme;
+ const newTheme = themeState.current;
document.documentElement.setAttribute("data-theme", newTheme);
- localStorage.setItem("theme", newTheme);
});
let { children }: Props = $props();
diff --git a/src/states/persisted.svelte.ts b/src/states/persisted.svelte.ts
new file mode 100644
index 0000000..5d590c8
--- /dev/null
+++ b/src/states/persisted.svelte.ts
@@ -0,0 +1,48 @@
+import { browser } from "$app/environment";
+
+export const STATE_DATA = Symbol("state-data");
+export const DEBOUNCE_DURATION = import.meta.env.MODE === "test" ? 0 : 500;
+
+type OnlyData = {
+ [K in keyof T as T[K] extends Function ? never : K]: T[K];
+};
+
+export interface Persistable {
+ [STATE_DATA]: D;
+}
+
+export function withPersistence>(
+ key: string,
+ state: T,
+ debounceMs = DEBOUNCE_DURATION,
+): T {
+ if (!browser) return state;
+
+ const saved = localStorage.getItem(key);
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+
+ if (parsed) {
+ state[STATE_DATA] = parsed;
+ }
+ } catch (e) {
+ console.error(`Error repairing state for ${key}`, e);
+ }
+ }
+
+ $effect.root(() => {
+ $effect(() => {
+ const data = JSON.stringify($state.snapshot(state[STATE_DATA]));
+
+ if (debounceMs > 0) {
+ const timeout = setTimeout(() => localStorage.setItem(key, data), debounceMs);
+ return () => clearTimeout(timeout);
+ } else {
+ localStorage.setItem(key, data);
+ }
+ });
+ });
+
+ return state;
+}
diff --git a/src/states/textConfig.svelte.ts b/src/states/textConfig.svelte.ts
index 511c048..8002220 100644
--- a/src/states/textConfig.svelte.ts
+++ b/src/states/textConfig.svelte.ts
@@ -1,5 +1,6 @@
-import type { TextAlignType } from "$lib/constants";
+import { type TextAlignType } from "$lib/constants";
import type { HexColor } from "$lib/types";
+import { STATE_DATA, withPersistence } from "./persisted.svelte";
export type TextConfig = {
fontSize: number;
@@ -58,7 +59,25 @@ function createState() {
set offsetY(offsetY: number) {
state.offsetY = offsetY;
},
+ get [STATE_DATA]() {
+ return {
+ fontSize: state.fontSize,
+ fontFamily: state.fontFamily,
+ color: state.color,
+ align: state.align,
+ paddingX: state.paddingX,
+ offsetY: state.offsetY,
+ };
+ },
+ set [STATE_DATA](newConfig: TextConfig) {
+ state.fontSize = newConfig.fontSize;
+ state.fontFamily = newConfig.fontFamily;
+ state.color = newConfig.color;
+ state.align = newConfig.align;
+ state.paddingX = newConfig.paddingX;
+ state.offsetY = newConfig.offsetY;
+ },
};
}
-export const textConfigState = createState();
+export const textConfigState = withPersistence("text-config", createState());
diff --git a/src/states/texts.svelte.ts b/src/states/texts.svelte.ts
index 04fbf68..8978d6a 100644
--- a/src/states/texts.svelte.ts
+++ b/src/states/texts.svelte.ts
@@ -1,3 +1,5 @@
+import { STATE_DATA, withPersistence } from "./persisted.svelte";
+
export interface TextItem {
text: string;
id: number;
@@ -25,7 +27,14 @@ export function createState() {
texts = [];
nextId = 0;
},
+ get [STATE_DATA]() {
+ return texts.map(({ text }) => text);
+ },
+ set [STATE_DATA](newTexts: Array) {
+ texts = newTexts.map((text, idx) => ({ text, id: idx }));
+ nextId = newTexts.length;
+ },
};
}
-export const textsState = createState();
+export const textsState = withPersistence("texts", createState());
diff --git a/src/states/theme.svelte.ts b/src/states/theme.svelte.ts
index 29f297a..6e9440c 100644
--- a/src/states/theme.svelte.ts
+++ b/src/states/theme.svelte.ts
@@ -1,19 +1,28 @@
-export type Theme = "dark" | "light";
+import { Theme, type ThemeType } from "$lib/constants";
+import { STATE_DATA, withPersistence } from "./persisted.svelte";
function createState() {
- let current: Theme = $state("dark");
+ let current: ThemeType = $state(Theme.LIGHT);
return {
- get theme() {
+ get current() {
return current;
},
- set theme(value: Theme) {
+ set current(value: ThemeType) {
current = value;
},
toggle() {
- current = current === "dark" ? "light" : "dark";
+ current = current === Theme.DARK ? Theme.LIGHT : Theme.DARK;
+ },
+ get [STATE_DATA]() {
+ return {
+ current,
+ };
+ },
+ set [STATE_DATA](data: { current: ThemeType }) {
+ current = data.current;
},
};
}
-export const themeState = createState();
+export const themeState = withPersistence("theme", createState());
diff --git a/tests/unit/components/layout/AppHeader.test.ts b/tests/unit/components/layout/AppHeader.test.ts
index 5c15c72..5efd1a0 100644
--- a/tests/unit/components/layout/AppHeader.test.ts
+++ b/tests/unit/components/layout/AppHeader.test.ts
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it } from "vitest";
describe("AppHeader.svelte", () => {
beforeEach(() => {
- themeState.theme = "dark";
+ themeState.current = "dark";
});
it("should toggle theme on button click", async () => {
@@ -13,11 +13,11 @@ describe("AppHeader.svelte", () => {
const toggleButton = screen.getByRole("button", { name: /toggle theme/i });
- themeState.theme = "dark";
+ themeState.current = "dark";
await fireEvent.click(toggleButton);
- expect(themeState.theme).toBe("light");
+ expect(themeState.current).toBe("light");
await fireEvent.click(toggleButton);
- expect(themeState.theme).toBe("dark");
+ expect(themeState.current).toBe("dark");
});
});
diff --git a/tests/unit/routes/layout.test.ts b/tests/unit/routes/layout.test.ts
index 0a43660..1afff3a 100644
--- a/tests/unit/routes/layout.test.ts
+++ b/tests/unit/routes/layout.test.ts
@@ -5,27 +5,27 @@ import LayoutTest from "./LayoutTest.svelte";
describe("+layout.svelte", () => {
beforeEach(() => {
- themeState.theme = "dark";
+ themeState.current = "dark";
vi.clearAllMocks();
});
it("should apply dark theme", () => {
- themeState.theme = "dark";
+ themeState.current = "dark";
- expect(themeState.theme).toBe("dark");
+ expect(themeState.current).toBe("dark");
});
it("should apply light theme", () => {
- themeState.theme = "light";
+ themeState.current = "light";
- expect(themeState.theme).toBe("light");
+ expect(themeState.current).toBe("light");
});
it("should toggle theme", () => {
- themeState.theme = "dark";
+ themeState.current = "dark";
themeState.toggle();
- expect(themeState.theme).toBe("light");
+ expect(themeState.current).toBe("light");
});
});
diff --git a/tests/unit/states/persisted.svelte.test.ts b/tests/unit/states/persisted.svelte.test.ts
new file mode 100644
index 0000000..e0c9ef6
--- /dev/null
+++ b/tests/unit/states/persisted.svelte.test.ts
@@ -0,0 +1,176 @@
+import { STATE_DATA, withPersistence } from "$states/persisted.svelte";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// Mock localStorage
+const localStorageMock = (() => {
+ let store: Record = {};
+ 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 },
+ };
+
+ // Should not throw and state should remain unchanged
+ 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 };
+ // Wait for effect to set the debounced timeout
+ await Promise.resolve();
+
+ // Fast-forward time to trigger debounced save
+ 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 };
+
+ // Wait for effect to run
+ 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 state = {
+ // [STATE_DATA]: { count: 1 },
+ // };
+
+ 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);
+
+ // First change - sets timeout for count:2
+ state[STATE_DATA] = { count: 2 };
+ // Let effect run and set the timeout
+ await Promise.resolve();
+
+ // Second change before first timeout fires - should clear previous and set new timeout for count:3
+ state[STATE_DATA] = { count: 3 };
+ // Let effect run and clear old timeout, set new one
+ await Promise.resolve();
+
+ // Run only the currently pending timer (the one for count:3)
+ vi.runOnlyPendingTimers();
+ await Promise.resolve();
+
+ // Should only have saved the latest value (count:3)
+ 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);
+
+ // The default debounce should be used (0 in test mode)
+ state[STATE_DATA] = { test: false };
+
+ // Wait for effect to run
+ await Promise.resolve();
+
+ // In test mode, DEBOUNCE_DURATION is 0, so immediate save
+ expect(localStorageMock.setItem).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/tests/unit/states/textConfig.test.ts b/tests/unit/states/textConfig.test.ts
index 71da0f9..0f7f3a4 100644
--- a/tests/unit/states/textConfig.test.ts
+++ b/tests/unit/states/textConfig.test.ts
@@ -1,235 +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 default fontSize", () => {
+ 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);
- });
-
- 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.align).toBe(TextAlign.CENTER);
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
index 2fe7d05..50f2990 100644
--- a/tests/unit/states/texts.test.ts
+++ b/tests/unit/states/texts.test.ts
@@ -1,105 +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 and increment counter", () => {
+ it("should add new text with unique ID", () => {
const state = createState();
const initialLength = state.texts.length;
- const initialIds = state.texts.map((item) => item.id);
- const maxId = Math.max(...initialIds);
- const newText = "Test text";
- state.addText(newText);
+ state.addText("Test text");
expect(state.texts).toHaveLength(initialLength + 1);
- expect(state.texts[initialLength].text).toBe(newText);
- expect(state.texts[initialLength].id).toBe(maxId + 1);
- expect(initialIds).not.toContain(state.texts[initialLength].id);
+ expect(state.texts[initialLength].text).toBe("Test text");
+ expect(state.texts[initialLength].id).toBeDefined();
});
- it("should not add text with empty string", () => {
+ 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 and preserve other texts", () => {
+ it("should remove text by ID", () => {
const state = createState();
+ const idToRemove = state.texts[0].id;
const initialLength = state.texts.length;
- const idToRemove = state.texts[1].id;
- const otherTexts = state.texts.filter((item) => item.id !== idToRemove);
state.removeText(idToRemove);
expect(state.texts).toHaveLength(initialLength - 1);
expect(state.texts.find((item) => item.id === idToRemove)).toBeUndefined();
- expect(state.texts).toStrictEqual(otherTexts);
});
- it("should not remove text with non-existent ID", () => {
+ it("should not affect other texts when removing", () => {
const state = createState();
- const initialLength = state.texts.length;
- const initialTexts = [...state.texts];
- const nonExistentId = 999999;
+ const remainingTexts = state.texts.filter((item) => item.id !== state.texts[0].id);
+ const idToRemove = state.texts[0].id;
- state.removeText(nonExistentId);
+ state.removeText(idToRemove);
- expect(state.texts).toHaveLength(initialLength);
- expect(state.texts).toStrictEqual(initialTexts);
+ expect(state.texts).toStrictEqual(remainingTexts);
});
});
describe("clear", () => {
- it("should clear all texts", () => {
+ it("should remove all texts", () => {
const state = createState();
state.addText("Test 1");
state.addText("Test 2");
- expect(state.texts.length).toBeGreaterThanOrEqual(2);
+ state.addText("Test 3");
state.clear();
+
expect(state.texts).toHaveLength(0);
});
});
- describe("integration", () => {
- it("should maintain ID uniqueness after multiple operations", () => {
+ describe("STATE_DATA", () => {
+ it("should serialize and deserialize texts", () => {
const state = createState();
- const initialIds = new Set(state.texts.map((item) => item.id));
-
+ state.clear();
state.addText("First");
state.addText("Second");
- state.removeText(state.texts[0].id);
state.addText("Third");
- const finalIds = state.texts.map((item) => item.id);
- const uniqueIds = new Set(finalIds);
+ const data = state[STATE_DATA];
+ expect(data).toEqual(["First", "Second", "Third"]);
- expect(finalIds.length).toBe(uniqueIds.size);
+ // 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 adding and removing multiple texts", () => {
+ it("should handle empty array", () => {
const state = createState();
- const initialLength = state.texts.length;
- const addedIds: number[] = [];
+ state.addText("Test");
- for (let i = 0; i < 5; i++) {
- state.addText(`Text ${i}`);
- addedIds.push(state.texts[state.texts.length - 1].id);
- }
+ state[STATE_DATA] = [];
- expect(state.texts).toHaveLength(initialLength + 5);
-
- state.removeText(addedIds[0]);
- state.removeText(addedIds[2]);
- state.removeText(addedIds[4]);
-
- expect(state.texts).toHaveLength(initialLength + 2);
+ expect(state.texts).toHaveLength(0);
});
});
});
diff --git a/tests/unit/states/theme.test.ts b/tests/unit/states/theme.test.ts
index 7f7e2d6..44445c0 100644
--- a/tests/unit/states/theme.test.ts
+++ b/tests/unit/states/theme.test.ts
@@ -1,49 +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 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");
+ it("should have valid initial theme value", () => {
+ expect(THEME_VALUES).toContain(themeState.current);
});
});
describe("toggle", () => {
- it("should toggle between light and dark themes", () => {
- const initialTheme = themeState.theme;
+ it("should toggle between themes", () => {
+ themeState.current = Theme.LIGHT;
+ themeState.toggle();
+ expect(themeState.current).toBe(Theme.DARK);
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");
+ expect(themeState.current).toBe(Theme.LIGHT);
});
});
- describe("theme getter", () => {
- it("should return current theme", () => {
- themeState.theme = "dark";
- expect(themeState.theme).toBe("dark");
+ describe("current setter", () => {
+ it("should set theme correctly", () => {
+ themeState.current = Theme.DARK;
+ expect(themeState.current).toBe(Theme.DARK);
- themeState.theme = "light";
- expect(themeState.theme).toBe("light");
+ 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);
});
});
});