test: update tests
This commit is contained in:
Generated
+15
@@ -25,6 +25,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
"@vitest/coverage-istanbul": "^4.0.18",
|
"@vitest/coverage-istanbul": "^4.0.18",
|
||||||
@@ -1768,6 +1769,20 @@
|
|||||||
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
|
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/user-event": {
|
||||||
|
"version": "14.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
||||||
|
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": ">=7.21.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/aria-query": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/svelte": "^5.3.1",
|
"@testing-library/svelte": "^5.3.1",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
"@vitest/coverage-istanbul": "^4.0.18",
|
"@vitest/coverage-istanbul": "^4.0.18",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "$components/ui/Button.svelte";
|
import Button from "$components/ui/Button.svelte";
|
||||||
import type { SlideDirectionType } from "$lib/constants";
|
import { SlideDirection, type SlideDirectionType } from "$lib/constants";
|
||||||
import ChevronLeft from "~icons/lucide/chevron-left";
|
import ChevronLeft from "~icons/lucide/chevron-left";
|
||||||
import ChevronRight from "~icons/lucide/chevron-right";
|
import ChevronRight from "~icons/lucide/chevron-right";
|
||||||
|
|
||||||
@@ -10,19 +10,19 @@
|
|||||||
max: number;
|
max: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { current = $bindable(), direction = $bindable("next"), max }: Props = $props();
|
let { current = $bindable(), direction = $bindable(SlideDirection.NEXT), max }: Props = $props();
|
||||||
|
|
||||||
let isFirst = $derived(current === 0);
|
let isFirst = $derived(current === 0);
|
||||||
let isLast = $derived(current === max - 1);
|
let isLast = $derived(current === max - 1);
|
||||||
|
|
||||||
function prev() {
|
function prev() {
|
||||||
if (current > 0) current--;
|
if (current > 0) current--;
|
||||||
direction = "prev";
|
direction = SlideDirection.PREV;
|
||||||
}
|
}
|
||||||
|
|
||||||
function next() {
|
async function next() {
|
||||||
if (current < max - 1) current++;
|
if (current < max - 1) current++;
|
||||||
direction = "next";
|
direction = SlideDirection.NEXT;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<div class="empty-state" aria-label="Empty texts info">
|
||||||
<StickyNote />
|
<StickyNote />
|
||||||
<p>Добавьте тексты для создания панелей</p>
|
<p>Добавьте тексты для создания панелей</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TextAlignType } from "$lib/constants";
|
import { TextAlign, type TextAlignType } from "$lib/constants";
|
||||||
import TextAlignCenter from "~icons/lucide/text-align-center";
|
import TextAlignCenter from "~icons/lucide/text-align-center";
|
||||||
import TextAlignEnd from "~icons/lucide/text-align-end";
|
import TextAlignEnd from "~icons/lucide/text-align-end";
|
||||||
import TextAlignStart from "~icons/lucide/text-align-start";
|
import TextAlignStart from "~icons/lucide/text-align-start";
|
||||||
@@ -9,16 +9,26 @@
|
|||||||
align: TextAlignType;
|
align: TextAlignType;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { align = $bindable("left") }: Props = $props();
|
let { align = $bindable(TextAlign.LEFT) }: Props = $props();
|
||||||
|
|
||||||
|
function onAlignLeft() {
|
||||||
|
align = TextAlign.LEFT;
|
||||||
|
}
|
||||||
|
function onAlignCenter() {
|
||||||
|
align = TextAlign.CENTER;
|
||||||
|
}
|
||||||
|
function onAlignRight() {
|
||||||
|
align = TextAlign.RIGHT;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="align-btn" class:active={align === "left"} onclick={() => (align = "left")}>
|
<button aria-label="Set left" class="align-btn" class:active={align === TextAlign.LEFT} onclick={onAlignLeft}>
|
||||||
<TextAlignStart />
|
<TextAlignStart />
|
||||||
</button>
|
</button>
|
||||||
<button class="align-btn" class:active={align === "center"} onclick={() => (align = "center")}>
|
<button aria-label="Set center" class="align-btn" class:active={align === TextAlign.CENTER} onclick={onAlignCenter}>
|
||||||
<TextAlignCenter />
|
<TextAlignCenter />
|
||||||
</button>
|
</button>
|
||||||
<button class="align-btn" class:active={align === "right"} onclick={() => (align = "right")}>
|
<button aria-label="Set right" class="align-btn" class:active={align === TextAlign.RIGHT} onclick={onAlignRight}>
|
||||||
<TextAlignEnd />
|
<TextAlignEnd />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export const TextAlign = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type TextAlignType = (typeof TextAlign)[keyof typeof TextAlign];
|
export type TextAlignType = (typeof TextAlign)[keyof typeof TextAlign];
|
||||||
|
|
||||||
export const DEFAULT_TEXT_ALIGN: TextAlignType = TextAlign.CENTER;
|
export const DEFAULT_TEXT_ALIGN: TextAlignType = TextAlign.CENTER;
|
||||||
|
|
||||||
export const Theme = {
|
export const Theme = {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Application Constants Logic > Global Contract (Snapshot) > should match the previous configuration snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"DEFAULT_TEXT_ALIGN": "center",
|
||||||
|
"IMAGE_SETTINGS": {
|
||||||
|
"MAX_FILE_SIZE": 10485760,
|
||||||
|
"SUPPORTED_FORMATS": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/gif",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"PANEL_SETTINGS": {
|
||||||
|
"DEFAULT_BACKGROUND_IMAGE": "./backgrounds/b1.jpg",
|
||||||
|
"PANEL_HEIGHT_DEFAULT": 100,
|
||||||
|
"PANEL_HEIGHT_MAX": 200,
|
||||||
|
"PANEL_WIDTH": 320,
|
||||||
|
},
|
||||||
|
"SlideDirection": {
|
||||||
|
"NEXT": "next",
|
||||||
|
"PREV": "prev",
|
||||||
|
},
|
||||||
|
"TRANSITION_DURATION": 0,
|
||||||
|
"TYPOGRAPHY": {
|
||||||
|
"FONT_FAMILIES": [
|
||||||
|
"Arial",
|
||||||
|
"Verdana",
|
||||||
|
"Georgia",
|
||||||
|
"Times New Roman",
|
||||||
|
"Courier New",
|
||||||
|
"Impact",
|
||||||
|
"Comic Sans MS",
|
||||||
|
"Trebuchet MS",
|
||||||
|
],
|
||||||
|
"FONT_FAMILY_DEFAULT": "Arial",
|
||||||
|
"FONT_SIZE_DEFAULT": 32,
|
||||||
|
"FONT_SIZE_MAX": 72,
|
||||||
|
"FONT_SIZE_MIN": 10,
|
||||||
|
"MAX_TEXT_LENGTH": 100,
|
||||||
|
"PADDING_X_DEFAULT": 10,
|
||||||
|
"PADDING_X_MAX": 100,
|
||||||
|
"TEXT_COLOR_DEFAULT": "#ffffff",
|
||||||
|
"VERTICAL_OFFSET_MAX": 100,
|
||||||
|
"VERTICAL_OFFSET_MIN": -100,
|
||||||
|
},
|
||||||
|
"TextAlign": {
|
||||||
|
"CENTER": "center",
|
||||||
|
"LEFT": "left",
|
||||||
|
"RIGHT": "right",
|
||||||
|
},
|
||||||
|
"Theme": {
|
||||||
|
"DARK": "dark",
|
||||||
|
"LIGHT": "light",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import AppHeader from "$components/layout/AppHeader.svelte";
|
import AppHeader from "$components/layout/AppHeader.svelte";
|
||||||
import { themeState } from "$states/theme.svelte";
|
import { themeState } from "$states/theme.svelte";
|
||||||
import { fireEvent, render, screen } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("AppHeader.svelte", () => {
|
describe("AppHeader.svelte", () => {
|
||||||
@@ -9,15 +10,16 @@ describe("AppHeader.svelte", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should toggle theme on button click", async () => {
|
it("should toggle theme on button click", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(AppHeader);
|
render(AppHeader);
|
||||||
|
|
||||||
const toggleButton = screen.getByRole("button", { name: /toggle theme/i });
|
const toggleButton = screen.getByRole("button", { name: /toggle theme/i });
|
||||||
|
|
||||||
themeState.current = "dark";
|
themeState.current = "dark";
|
||||||
await fireEvent.click(toggleButton);
|
await user.click(toggleButton);
|
||||||
expect(themeState.current).toBe("light");
|
expect(themeState.current).toBe("light");
|
||||||
|
|
||||||
await fireEvent.click(toggleButton);
|
await user.click(toggleButton);
|
||||||
expect(themeState.current).toBe("dark");
|
expect(themeState.current).toBe("dark");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
import PreviewControls from "$components/panel/PreviewControls.svelte";
|
import { SlideDirection } from "$lib/constants";
|
||||||
import { cleanup, render, screen } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import PreviewControlsTest from "./PreviewControlsTest.svelte";
|
||||||
|
|
||||||
afterEach(() => {
|
describe("PreviewControls logic", () => {
|
||||||
cleanup();
|
it("should increment current and update direction on next click", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(PreviewControlsTest, { props: { current: 0, max: 3 } });
|
||||||
|
|
||||||
|
const nextBtn = screen.getByRole("button", { name: /next slide/i });
|
||||||
|
|
||||||
|
await user.click(nextBtn);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("current").textContent).toBe("1");
|
||||||
|
expect(screen.getByTestId("direction").textContent).toBe(SlideDirection.NEXT);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("PreviewControls.svelte", () => {
|
it("should decrement current on prev click", async () => {
|
||||||
it("should render navigation buttons", () => {
|
const user = userEvent.setup();
|
||||||
render(PreviewControls, {
|
render(PreviewControlsTest, { props: { current: 2, max: 3 } });
|
||||||
props: {
|
|
||||||
current: 1,
|
const prevBtn = screen.getByRole("button", { name: /previous slide/i });
|
||||||
direction: "next",
|
|
||||||
max: 3,
|
await user.click(prevBtn);
|
||||||
},
|
|
||||||
|
expect(screen.getByTestId("current").textContent).toBe("1");
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = screen.getAllByRole("button");
|
it("should handle boundaries and disable buttons", async () => {
|
||||||
expect(buttons.length).toBeGreaterThan(0);
|
const user = userEvent.setup();
|
||||||
|
render(PreviewControlsTest, { props: { current: 0, max: 2 } });
|
||||||
|
|
||||||
|
const prevBtn = screen.getByRole("button", { name: /previous slide/i });
|
||||||
|
const nextBtn = screen.getByRole("button", { name: /next slide/i });
|
||||||
|
|
||||||
|
expect(prevBtn).toBeDisabled();
|
||||||
|
|
||||||
|
await user.click(nextBtn);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("current").textContent).toBe("1");
|
||||||
|
expect(nextBtn).toBeDisabled();
|
||||||
|
expect(prevBtn).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import PreviewControls from "$components/panel/PreviewControls.svelte";
|
||||||
|
|
||||||
|
import { SlideDirection } from "$lib/constants";
|
||||||
|
|
||||||
|
let { current = 0, max = 5 } = $props();
|
||||||
|
let direction = $state(SlideDirection.NEXT);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PreviewControls bind:current bind:direction {max} />
|
||||||
|
|
||||||
|
<div data-testid="current">{current}</div>
|
||||||
|
<div data-testid="direction">{direction}</div>
|
||||||
@@ -1,14 +1,102 @@
|
|||||||
import PreviewManager from "$components/panel/PreviewManager.svelte";
|
import PreviewManager from "$components/panel/PreviewManager.svelte";
|
||||||
|
import { downloadService } from "$services/downloadService";
|
||||||
|
import { konvaStageState } from "$states/konvaStage.svelte";
|
||||||
|
import { STATE_DATA } from "$states/persisted.svelte";
|
||||||
import { textsState } from "$states/texts.svelte";
|
import { textsState } from "$states/texts.svelte";
|
||||||
import { render } from "@testing-library/svelte";
|
import { render, screen, waitFor } from "@testing-library/svelte";
|
||||||
import { beforeEach, describe, it } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("PreviewManager.svelte", () => {
|
vi.mock("$services/downloadService", () => ({
|
||||||
|
downloadService: {
|
||||||
|
downloadAll: vi.fn(),
|
||||||
|
downloadPanel: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./Preview.svelte", () => ({
|
||||||
|
default: { render: () => ({}) },
|
||||||
|
}));
|
||||||
|
describe("PreviewManager Integration", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
textsState.texts.length = 0;
|
vi.clearAllMocks();
|
||||||
|
textsState[STATE_DATA] = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render without crashing", () => {
|
it("should show empty state by aria-label when no texts", () => {
|
||||||
render(PreviewManager);
|
render(PreviewManager);
|
||||||
|
expect(screen.getByLabelText(/Empty texts info/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle navigation buttons availability based on texts length", async () => {
|
||||||
|
textsState[STATE_DATA] = ["1", "2"];
|
||||||
|
render(PreviewManager);
|
||||||
|
|
||||||
|
const nextBtn = screen.getByRole("button", { name: /next slide/i });
|
||||||
|
const prevBtn = screen.getByRole("button", { name: /previous slide/i });
|
||||||
|
|
||||||
|
expect(nextBtn).not.toBeDisabled();
|
||||||
|
expect(prevBtn).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send correct data to downloadAll service", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
textsState[STATE_DATA] = ["Apple", "Banana"];
|
||||||
|
render(PreviewManager);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /download all/i }));
|
||||||
|
|
||||||
|
expect(downloadService.downloadAll).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ filename: "Apple" }),
|
||||||
|
expect.objectContaining({ filename: "Banana" }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call downloadPanel with current active text", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
textsState[STATE_DATA] = ["First", "Second"];
|
||||||
|
konvaStageState.stage = { node: { id: "stage-ref" } } as any;
|
||||||
|
|
||||||
|
render(PreviewManager);
|
||||||
|
|
||||||
|
// Переходим на второй слайд
|
||||||
|
await user.click(screen.getByRole("button", { name: /next slide/i }));
|
||||||
|
await user.click(screen.getByRole("button", { name: /download current/i }));
|
||||||
|
|
||||||
|
expect(downloadService.downloadPanel).toHaveBeenCalledWith(expect.anything(), "Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return to empty state when texts are removed", async () => {
|
||||||
|
textsState[STATE_DATA] = ["Temp"];
|
||||||
|
render(PreviewManager);
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(/Empty texts info/i)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
textsState[STATE_DATA] = [];
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByLabelText(/Empty texts info/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should automatically correct current index on items deletion", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
textsState[STATE_DATA] = ["1", "2", "3"];
|
||||||
|
render(PreviewManager);
|
||||||
|
|
||||||
|
// Уходим на последний слайд
|
||||||
|
await user.click(screen.getByRole("button", { name: /next slide/i }));
|
||||||
|
await user.click(screen.getByRole("button", { name: /next slide/i }));
|
||||||
|
|
||||||
|
// Удаляем элементы. Индекс должен упасть с 2 до 0
|
||||||
|
textsState[STATE_DATA] = ["Only one left"];
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Проверяем, что кнопка "Next" заблокировалась (значит мы на последнем/единственном)
|
||||||
|
expect(screen.getByRole("button", { name: /next slide/i })).toBeDisabled();
|
||||||
|
expect(screen.getByRole("button", { name: /previous slide/i })).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import TextInput from "$components/text/TextInput.svelte";
|
import TextInput from "$components/text/TextInput.svelte";
|
||||||
import { fireEvent, render, screen } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("TextInput.svelte", () => {
|
describe("TextInput.svelte", () => {
|
||||||
@@ -18,6 +19,7 @@ describe("TextInput.svelte", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update value on input", async () => {
|
it("should update value on input", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(TextInput, {
|
render(TextInput, {
|
||||||
props: {
|
props: {
|
||||||
text: "Initial",
|
text: "Initial",
|
||||||
@@ -27,41 +29,33 @@ describe("TextInput.svelte", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const input = screen.getByRole("textbox", { name: /test input/i });
|
const input = screen.getByRole("textbox", { name: /test input/i });
|
||||||
await fireEvent.input(input, { target: { value: "Updated text" } });
|
await user.clear(input);
|
||||||
|
await user.type(input, "Updated text");
|
||||||
|
|
||||||
expect(input).toHaveValue("Updated text");
|
expect(input).toHaveValue("Updated text");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call onenter when Enter key is pressed", async () => {
|
it("should call onenter only on Enter key and not on others", async () => {
|
||||||
const onenter = vi.fn();
|
const user = userEvent.setup();
|
||||||
|
const onenterSpy = vi.fn();
|
||||||
|
|
||||||
render(TextInput, {
|
render(TextInput, {
|
||||||
props: {
|
props: {
|
||||||
text: "Test",
|
text: "test",
|
||||||
onenter,
|
onenter: onenterSpy,
|
||||||
ariaLabel: "Test input",
|
ariaLabel: "Test input",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = screen.getByRole("textbox", { name: /test input/i });
|
const input = screen.getByRole("textbox", { name: /test input/i });
|
||||||
await fireEvent.keyDown(input, { key: "Enter" });
|
|
||||||
|
|
||||||
expect(onenter).toHaveBeenCalledTimes(1);
|
await user.type(input, "abc");
|
||||||
});
|
expect(onenterSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
it("should not call onenter when other keys are pressed", async () => {
|
await user.keyboard("{Escape}");
|
||||||
const onenter = vi.fn();
|
expect(onenterSpy).not.toHaveBeenCalled();
|
||||||
render(TextInput, {
|
|
||||||
props: {
|
|
||||||
text: "Test",
|
|
||||||
onenter,
|
|
||||||
ariaLabel: "Test input",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = screen.getByRole("textbox", { name: /test input/i });
|
await user.type(input, "{Enter}");
|
||||||
await fireEvent.keyDown(input, { key: "Escape" });
|
expect(onenterSpy).toHaveBeenCalledTimes(1);
|
||||||
await fireEvent.keyDown(input, { key: "Tab" });
|
|
||||||
|
|
||||||
expect(onenter).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,193 +1,60 @@
|
|||||||
import TextManager from "$components/text/TextManager.svelte";
|
import TextManager from "$components/text/TextManager.svelte";
|
||||||
import { textsState } from "$states/texts.svelte";
|
import { textsState } from "$states/texts.svelte";
|
||||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("TextManager.svelte", () => {
|
describe("TextManager", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
textsState.clear();
|
textsState.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Rendering", () => {
|
it("should add text to state and clear input on button click", async () => {
|
||||||
it("should render card with title, input and add button", () => {
|
const user = userEvent.setup();
|
||||||
render(TextManager);
|
render(TextManager);
|
||||||
|
|
||||||
const cardTitle = screen.getByText("Тексты панелей");
|
const input = screen.getByRole("textbox", { name: /input new text/i });
|
||||||
expect(cardTitle).toBeInTheDocument();
|
const addButton = screen.getByRole("button", { name: /add text/i });
|
||||||
|
|
||||||
const input = screen.getByRole("textbox");
|
await user.type(input, "New Note");
|
||||||
expect(input).toBeInTheDocument();
|
await user.click(addButton);
|
||||||
|
|
||||||
const addButton = screen.getByRole("button", { name: /add/i });
|
expect(textsState.texts).toHaveLength(1);
|
||||||
expect(addButton).toBeInTheDocument();
|
expect(textsState.texts[0].text).toBe("New Note");
|
||||||
});
|
|
||||||
|
|
||||||
it("should show empty list when textsState.texts is empty", () => {
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
const textItems = screen.queryAllByRole("listitem");
|
|
||||||
expect(textItems).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Adding text", () => {
|
|
||||||
it("should call textsState.addText when add button is clicked", async () => {
|
|
||||||
const addTextSpy = vi.spyOn(textsState, "addText");
|
|
||||||
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
const input = screen.getByRole("textbox");
|
|
||||||
const addButton = screen.getByRole("button", { name: /add/i });
|
|
||||||
|
|
||||||
fireEvent.input(input, { target: { value: "New text" } });
|
|
||||||
fireEvent.click(addButton);
|
|
||||||
|
|
||||||
expect(addTextSpy).toHaveBeenCalledWith("New text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should clear input after adding text", async () => {
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
const input = screen.getByRole("textbox");
|
|
||||||
const addButton = screen.getByRole("button", { name: /add/i });
|
|
||||||
|
|
||||||
fireEvent.input(input, { target: { value: "New text" } });
|
|
||||||
fireEvent.click(addButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input).toHaveValue("");
|
expect(input).toHaveValue("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add text on enter key", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(TextManager);
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: /input new text/i });
|
||||||
|
|
||||||
|
await user.type(input, "Enter Note{Enter}");
|
||||||
|
|
||||||
|
expect(textsState.texts).toHaveLength(1);
|
||||||
|
expect(textsState.texts[0].text).toBe("Enter Note");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add text when Enter key is pressed", () => {
|
it("should display all items from state", async () => {
|
||||||
const addTextSpy = vi.spyOn(textsState, "addText");
|
textsState.addText("First");
|
||||||
|
textsState.addText("Second");
|
||||||
|
|
||||||
render(TextManager);
|
render(TextManager);
|
||||||
|
|
||||||
const input = screen.getByRole("textbox");
|
const items = screen.getAllByRole("listitem");
|
||||||
|
expect(items).toHaveLength(2);
|
||||||
fireEvent.input(input, { target: { value: "New text" } });
|
|
||||||
fireEvent.keyDown(input, { key: "Enter" });
|
|
||||||
|
|
||||||
expect(addTextSpy).toHaveBeenCalledWith("New text");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove text from state when delete button is clicked", async () => {
|
||||||
});
|
const user = userEvent.setup();
|
||||||
|
textsState.addText("To be deleted");
|
||||||
describe("List and deletion", () => {
|
|
||||||
it("should render TextInlineEdit for each text", () => {
|
|
||||||
textsState.addText("Text 1");
|
|
||||||
textsState.addText("Text 2");
|
|
||||||
textsState.addText("Text 3");
|
|
||||||
|
|
||||||
render(TextManager);
|
render(TextManager);
|
||||||
|
|
||||||
const textItems = screen.queryAllByRole("listitem");
|
const deleteBtn = screen.getByRole("button", { name: /delete/i });
|
||||||
expect(textItems).toHaveLength(3);
|
await user.click(deleteBtn);
|
||||||
});
|
|
||||||
|
|
||||||
it("should pass correct props to TextInlineEdit", () => {
|
expect(textsState.texts).toHaveLength(0);
|
||||||
textsState.addText("Test text");
|
|
||||||
const textId = textsState.texts[0].id;
|
|
||||||
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
const textItems = screen.queryAllByRole("listitem");
|
|
||||||
expect(textItems).toHaveLength(1);
|
|
||||||
|
|
||||||
const input = within(textItems[0]).getByRole("textbox");
|
|
||||||
expect(input).toHaveValue("Test text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call textsState.removeText when delete button is clicked", () => {
|
|
||||||
textsState.addText("Text to delete");
|
|
||||||
const textId = textsState.texts[0].id;
|
|
||||||
const removeTextSpy = vi.spyOn(textsState, "removeText");
|
|
||||||
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
const deleteButton = screen.getByRole("button", { name: /delete/i });
|
|
||||||
fireEvent.click(deleteButton);
|
|
||||||
|
|
||||||
expect(removeTextSpy).toHaveBeenCalledWith(textId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove element from DOM after deletion", async () => {
|
|
||||||
textsState.addText("Text to delete");
|
|
||||||
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
let textItems = screen.queryAllByRole("listitem");
|
|
||||||
expect(textItems).toHaveLength(1);
|
|
||||||
|
|
||||||
const deleteButton = screen.getByRole("button", { name: /delete/i });
|
|
||||||
fireEvent.click(deleteButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
textItems = screen.queryAllByRole("listitem");
|
|
||||||
expect(textItems).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Reactivity", () => {
|
|
||||||
it("should update list when textsState.texts changes externally", async () => {
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
let textItems = screen.queryAllByRole("listitem");
|
|
||||||
expect(textItems).toHaveLength(0);
|
|
||||||
|
|
||||||
textsState.addText("New text");
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
textItems = screen.queryAllByRole("listitem");
|
|
||||||
expect(textItems).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update when text is removed externally", async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
textsState.addText("Text to remove");
|
|
||||||
const textId = textsState.texts[0].id;
|
|
||||||
|
|
||||||
render(TextManager);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryAllByRole("listitem")).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
textsState.removeText(textId);
|
|
||||||
await vi.advanceTimersByTimeAsync(400);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
|
|
||||||
});
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Accessibility", () => {
|
|
||||||
// it("should keep focus in input after adding text", async () => {
|
|
||||||
// render(TextManager);
|
|
||||||
// const input = screen.getByRole("textbox");
|
|
||||||
// const addButton = screen.getByRole("button", { name: /add/i });
|
|
||||||
// fireEvent.input(input, { target: { value: "New text" } });
|
|
||||||
// fireEvent.click(addButton);
|
|
||||||
// await waitFor(() => {
|
|
||||||
// expect(input).toHaveFocus();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
it("should have proper aria-labels for input and button", () => {
|
|
||||||
render(TextManager);
|
|
||||||
const input = screen.getByRole("textbox");
|
|
||||||
expect(input).toHaveAttribute("aria-label", "Input new text");
|
|
||||||
const addButton = screen.getByRole("button", { name: /add/i });
|
|
||||||
expect(addButton).toHaveAttribute("aria-label", "Add text");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,44 @@
|
|||||||
import Alignment from "$components/ui/Alignment.svelte";
|
import { TextAlign } from "$lib/constants";
|
||||||
import { render } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
import { describe, it } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import AlignmentTest from "./AlignmentTest.svelte";
|
||||||
|
|
||||||
describe("Alignment.svelte", () => {
|
describe("Alignment.svelte", () => {
|
||||||
it("should render without crashing", () => {
|
it("should update state for every button by clicking in sequence", async () => {
|
||||||
render(Alignment, {
|
const user = userEvent.setup();
|
||||||
props: {
|
render(AlignmentTest);
|
||||||
align: "left",
|
|
||||||
},
|
const buttons = screen.getAllByRole("button");
|
||||||
});
|
const stateDisplay = screen.getByTestId("align-value");
|
||||||
|
|
||||||
|
const allButtonsToClick = [...buttons, buttons[0]];
|
||||||
|
|
||||||
|
for (const button of allButtonsToClick) {
|
||||||
|
const label = button.getAttribute("aria-label")?.toLowerCase() || "";
|
||||||
|
|
||||||
|
await user.click(button);
|
||||||
|
|
||||||
|
const finalValue = stateDisplay.textContent?.toLowerCase() || "";
|
||||||
|
expect(label).toContain(finalValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have initial state from props", () => {
|
||||||
|
render(AlignmentTest, { props: { align: TextAlign.RIGHT } });
|
||||||
|
expect(screen.getByTestId("align-value").textContent).toBe(TextAlign.RIGHT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sync with initial state and change state", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { rerender } = render(AlignmentTest, { props: { align: TextAlign.RIGHT } });
|
||||||
|
const stateDisplay = screen.getByTestId("align-value");
|
||||||
|
|
||||||
|
expect(stateDisplay.textContent).toBe(TextAlign.RIGHT);
|
||||||
|
|
||||||
|
await rerender({ align: TextAlign.CENTER });
|
||||||
|
|
||||||
|
const centerBtn = screen.getByRole("button", { name: new RegExp(TextAlign.CENTER, "i") });
|
||||||
|
expect(centerBtn).toHaveClass("active");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Alignment from "$components/ui/Alignment.svelte";
|
||||||
|
|
||||||
|
import { TextAlign } from "$lib/constants";
|
||||||
|
|
||||||
|
let { align = TextAlign.LEFT } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Alignment bind:align />
|
||||||
|
|
||||||
|
<div data-testid="align-value">{align}</div>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import Button from "$components/ui/Button.svelte";
|
import Button from "$components/ui/Button.svelte";
|
||||||
import { fireEvent, render, screen } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import MockIcon from "./MockIcon.svelte";
|
import MockIcon from "./MockIcon.svelte";
|
||||||
|
|
||||||
@@ -25,11 +26,15 @@ describe("Button.svelte", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: /test button/i })).toBeInTheDocument();
|
const button = screen.getByRole("button", { name: /test button/i });
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
expect(button.textContent.trim()).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call onclick handler", async () => {
|
it("should call onclick handler", async () => {
|
||||||
const onclick = vi.fn();
|
const onclick = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
render(Button, {
|
render(Button, {
|
||||||
props: {
|
props: {
|
||||||
icon: MockIcon,
|
icon: MockIcon,
|
||||||
@@ -40,11 +45,27 @@ describe("Button.svelte", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const button = screen.getByRole("button", { name: /test button/i });
|
const button = screen.getByRole("button", { name: /test button/i });
|
||||||
await fireEvent.click(button);
|
await user.click(button);
|
||||||
|
|
||||||
expect(onclick).toHaveBeenCalledTimes(1);
|
expect(onclick).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not throw error when clicked without onclick prop", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(Button, {
|
||||||
|
props: {
|
||||||
|
icon: MockIcon,
|
||||||
|
label: "Click me",
|
||||||
|
ariaLabel: "Test button",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const button = screen.getByRole("button", { name: /test button/i });
|
||||||
|
|
||||||
|
expect(() => user.click(button)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("should be disabled when disabled prop is true", () => {
|
it("should be disabled when disabled prop is true", () => {
|
||||||
render(Button, {
|
render(Button, {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
describe("RangeSlider.svelte", () => {
|
describe("RangeSlider.svelte", () => {
|
||||||
it("should call onchange handler", async () => {
|
it("should call onchange handler", async () => {
|
||||||
const onchange = vi.fn();
|
const onchange = vi.fn();
|
||||||
|
|
||||||
render(RangeSlider, {
|
render(RangeSlider, {
|
||||||
props: {
|
props: {
|
||||||
value: 50,
|
value: 50,
|
||||||
|
|||||||
+37
-228
@@ -1,259 +1,68 @@
|
|||||||
import {
|
import * as Constants from "$lib/constants";
|
||||||
DEFAULT_TEXT_ALIGN,
|
import fs from "fs";
|
||||||
IMAGE_SETTINGS,
|
import path from "path";
|
||||||
PANEL_SETTINGS,
|
|
||||||
SlideDirection,
|
|
||||||
type SlideDirectionType,
|
|
||||||
TextAlign,
|
|
||||||
type TextAlignType,
|
|
||||||
TYPOGRAPHY,
|
|
||||||
} from "$lib/constants";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("constants", () => {
|
describe("Application Constants Logic", () => {
|
||||||
describe("PANEL_SETTINGS", () => {
|
describe("Typography & Panel Constraints", () => {
|
||||||
it("should have correct panel width", () => {
|
it("should have default values within allowed boundaries", () => {
|
||||||
expect(PANEL_SETTINGS.PANEL_WIDTH).toBe(320);
|
const { TYPOGRAPHY, PANEL_SETTINGS } = Constants;
|
||||||
});
|
|
||||||
|
|
||||||
it("should have correct default panel height", () => {
|
|
||||||
expect(PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have correct max panel height", () => {
|
|
||||||
expect(PANEL_SETTINGS.PANEL_HEIGHT_MAX).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have default background image path", () => {
|
|
||||||
expect(PANEL_SETTINGS.DEFAULT_BACKGROUND_IMAGE).toBe("./backgrounds/b1.jpg");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have all required properties", () => {
|
|
||||||
expect(PANEL_SETTINGS).toHaveProperty("PANEL_WIDTH");
|
|
||||||
expect(PANEL_SETTINGS).toHaveProperty("PANEL_HEIGHT_DEFAULT");
|
|
||||||
expect(PANEL_SETTINGS).toHaveProperty("PANEL_HEIGHT_MAX");
|
|
||||||
expect(PANEL_SETTINGS).toHaveProperty("DEFAULT_BACKGROUND_IMAGE");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have numeric dimensions", () => {
|
|
||||||
expect(typeof PANEL_SETTINGS.PANEL_WIDTH).toBe("number");
|
|
||||||
expect(typeof PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT).toBe("number");
|
|
||||||
expect(typeof PANEL_SETTINGS.PANEL_HEIGHT_MAX).toBe("number");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have string background image path", () => {
|
|
||||||
expect(typeof PANEL_SETTINGS.DEFAULT_BACKGROUND_IMAGE).toBe("string");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid panel dimensions", () => {
|
|
||||||
expect(PANEL_SETTINGS.PANEL_WIDTH).toBeGreaterThan(0);
|
|
||||||
expect(PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT).toBeGreaterThan(0);
|
|
||||||
expect(PANEL_SETTINGS.PANEL_HEIGHT_MAX).toBeGreaterThan(PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("TYPOGRAPHY", () => {
|
|
||||||
it("should have default font family", () => {
|
|
||||||
expect(TYPOGRAPHY.FONT_FAMILY_DEFAULT).toBe("Arial");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have array of font families", () => {
|
|
||||||
expect(Array.isArray(TYPOGRAPHY.FONT_FAMILIES)).toBe(true);
|
|
||||||
expect(TYPOGRAPHY.FONT_FAMILIES.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should include default font in font families", () => {
|
|
||||||
expect(TYPOGRAPHY.FONT_FAMILIES).toContain(TYPOGRAPHY.FONT_FAMILY_DEFAULT);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have correct font size range", () => {
|
|
||||||
expect(TYPOGRAPHY.FONT_SIZE_MIN).toBe(10);
|
|
||||||
expect(TYPOGRAPHY.FONT_SIZE_MAX).toBe(72);
|
|
||||||
expect(TYPOGRAPHY.FONT_SIZE_DEFAULT).toBe(32);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid font size range", () => {
|
|
||||||
expect(TYPOGRAPHY.FONT_SIZE_MIN).toBeLessThan(TYPOGRAPHY.FONT_SIZE_MAX);
|
|
||||||
expect(TYPOGRAPHY.FONT_SIZE_DEFAULT).toBeGreaterThanOrEqual(TYPOGRAPHY.FONT_SIZE_MIN);
|
expect(TYPOGRAPHY.FONT_SIZE_DEFAULT).toBeGreaterThanOrEqual(TYPOGRAPHY.FONT_SIZE_MIN);
|
||||||
expect(TYPOGRAPHY.FONT_SIZE_DEFAULT).toBeLessThanOrEqual(TYPOGRAPHY.FONT_SIZE_MAX);
|
expect(TYPOGRAPHY.FONT_SIZE_DEFAULT).toBeLessThanOrEqual(TYPOGRAPHY.FONT_SIZE_MAX);
|
||||||
|
|
||||||
|
expect(TYPOGRAPHY.FONT_FAMILIES).toContain(TYPOGRAPHY.FONT_FAMILY_DEFAULT);
|
||||||
|
|
||||||
|
expect(PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT).toBeLessThanOrEqual(PANEL_SETTINGS.PANEL_HEIGHT_MAX);
|
||||||
|
|
||||||
|
expect(TYPOGRAPHY.VERTICAL_OFFSET_MAX).toBeGreaterThan(TYPOGRAPHY.VERTICAL_OFFSET_MIN);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have max text length", () => {
|
it("should not have duplicate values in lists", () => {
|
||||||
expect(TYPOGRAPHY.MAX_TEXT_LENGTH).toBe(100);
|
const { TYPOGRAPHY, IMAGE_SETTINGS } = Constants;
|
||||||
expect(typeof TYPOGRAPHY.MAX_TEXT_LENGTH).toBe("number");
|
|
||||||
expect(TYPOGRAPHY.MAX_TEXT_LENGTH).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have padding settings", () => {
|
const uniqueFonts = new Set(TYPOGRAPHY.FONT_FAMILIES);
|
||||||
expect(TYPOGRAPHY.PADDING_X_DEFAULT).toBe(10);
|
expect(uniqueFonts.size).toBe(TYPOGRAPHY.FONT_FAMILIES.length);
|
||||||
expect(TYPOGRAPHY.PADDING_X_MAX).toBe(100);
|
|
||||||
expect(typeof TYPOGRAPHY.PADDING_X_DEFAULT).toBe("number");
|
|
||||||
expect(typeof TYPOGRAPHY.PADDING_X_MAX).toBe("number");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid padding range", () => {
|
const uniqueFormats = new Set(IMAGE_SETTINGS.SUPPORTED_FORMATS);
|
||||||
expect(TYPOGRAPHY.PADDING_X_DEFAULT).toBeLessThanOrEqual(TYPOGRAPHY.PADDING_X_MAX);
|
expect(uniqueFormats.size).toBe(IMAGE_SETTINGS.SUPPORTED_FORMATS.length);
|
||||||
expect(TYPOGRAPHY.PADDING_X_DEFAULT).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have vertical offset settings", () => {
|
|
||||||
expect(TYPOGRAPHY.VERTICAL_OFFSET_MAX).toBe(100);
|
|
||||||
expect(TYPOGRAPHY.VERTICAL_OFFSET_MIN).toBe(-100);
|
|
||||||
expect(typeof TYPOGRAPHY.VERTICAL_OFFSET_MAX).toBe("number");
|
|
||||||
expect(typeof TYPOGRAPHY.VERTICAL_OFFSET_MIN).toBe("number");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid vertical offset range", () => {
|
|
||||||
expect(TYPOGRAPHY.VERTICAL_OFFSET_MIN).toBeLessThan(TYPOGRAPHY.VERTICAL_OFFSET_MAX);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have default text color", () => {
|
|
||||||
expect(TYPOGRAPHY.TEXT_COLOR_DEFAULT).toBe("#ffffff");
|
|
||||||
expect(typeof TYPOGRAPHY.TEXT_COLOR_DEFAULT).toBe("string");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid hex color format for default text color", () => {
|
|
||||||
const hexColorRegex = /^#[0-9A-Fa-f]{6}$/;
|
|
||||||
expect(TYPOGRAPHY.TEXT_COLOR_DEFAULT).toMatch(hexColorRegex);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have all required typography properties", () => {
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("FONT_FAMILY_DEFAULT");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("FONT_FAMILIES");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("FONT_SIZE_MIN");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("FONT_SIZE_MAX");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("FONT_SIZE_DEFAULT");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("MAX_TEXT_LENGTH");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("PADDING_X_DEFAULT");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("PADDING_X_MAX");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("VERTICAL_OFFSET_MAX");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("VERTICAL_OFFSET_MIN");
|
|
||||||
expect(TYPOGRAPHY).toHaveProperty("TEXT_COLOR_DEFAULT");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("IMAGE_SETTINGS", () => {
|
describe("Integrity & Formats", () => {
|
||||||
it("should have correct max file size", () => {
|
it("should have valid format patterns for colors and mime-types", () => {
|
||||||
expect(IMAGE_SETTINGS.MAX_FILE_SIZE).toBe(10 * 1024 * 1024); // 10MB
|
const { TYPOGRAPHY, IMAGE_SETTINGS } = Constants;
|
||||||
});
|
|
||||||
|
|
||||||
it("should have array of supported formats", () => {
|
expect(TYPOGRAPHY.TEXT_COLOR_DEFAULT).toMatch(/^#[0-9a-f]{6}$/i);
|
||||||
expect(Array.isArray(IMAGE_SETTINGS.SUPPORTED_FORMATS)).toBe(true);
|
|
||||||
expect(IMAGE_SETTINGS.SUPPORTED_FORMATS.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should include common image formats", () => {
|
|
||||||
expect(IMAGE_SETTINGS.SUPPORTED_FORMATS).toContain("image/jpeg");
|
|
||||||
expect(IMAGE_SETTINGS.SUPPORTED_FORMATS).toContain("image/png");
|
|
||||||
expect(IMAGE_SETTINGS.SUPPORTED_FORMATS).toContain("image/webp");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have all required image settings properties", () => {
|
|
||||||
expect(IMAGE_SETTINGS).toHaveProperty("MAX_FILE_SIZE");
|
|
||||||
expect(IMAGE_SETTINGS).toHaveProperty("SUPPORTED_FORMATS");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid file size", () => {
|
|
||||||
expect(typeof IMAGE_SETTINGS.MAX_FILE_SIZE).toBe("number");
|
|
||||||
expect(IMAGE_SETTINGS.MAX_FILE_SIZE).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have string format types", () => {
|
|
||||||
IMAGE_SETTINGS.SUPPORTED_FORMATS.forEach((format) => {
|
IMAGE_SETTINGS.SUPPORTED_FORMATS.forEach((format) => {
|
||||||
expect(typeof format).toBe("string");
|
expect(format).toMatch(/^image\/(jpeg|jpg|png|webp|gif)$/);
|
||||||
expect(format).toMatch(/^image\//);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("SlideDirection", () => {
|
describe("Environment Specifics", () => {
|
||||||
it("should have NEXT and PREV directions", () => {
|
it("should set transition duration to 0 in test mode", () => {
|
||||||
expect(SlideDirection.NEXT).toBe("next");
|
expect(Constants.TRANSITION_DURATION).toBe(0);
|
||||||
expect(SlideDirection.PREV).toBe("prev");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have correct type for slide directions", () => {
|
|
||||||
const next: SlideDirectionType = SlideDirection.NEXT;
|
|
||||||
const prev: SlideDirectionType = SlideDirection.PREV;
|
|
||||||
|
|
||||||
expect(next).toBe("next");
|
|
||||||
expect(prev).toBe("prev");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have all required slide direction properties", () => {
|
|
||||||
expect(SlideDirection).toHaveProperty("NEXT");
|
|
||||||
expect(SlideDirection).toHaveProperty("PREV");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have string values for directions", () => {
|
|
||||||
expect(typeof SlideDirection.NEXT).toBe("string");
|
|
||||||
expect(typeof SlideDirection.PREV).toBe("string");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("TextAlign", () => {
|
describe("Global Contract (Snapshot)", () => {
|
||||||
it("should have LEFT, CENTER and RIGHT alignments", () => {
|
it("should match the previous configuration snapshot", () => {
|
||||||
expect(TextAlign.LEFT).toBe("left");
|
expect(Constants).toMatchSnapshot();
|
||||||
expect(TextAlign.CENTER).toBe("center");
|
|
||||||
expect(TextAlign.RIGHT).toBe("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have correct type for text alignments", () => {
|
|
||||||
const left: TextAlignType = TextAlign.LEFT;
|
|
||||||
const center: TextAlignType = TextAlign.CENTER;
|
|
||||||
const right: TextAlignType = TextAlign.RIGHT;
|
|
||||||
|
|
||||||
expect(left).toBe("left");
|
|
||||||
expect(center).toBe("center");
|
|
||||||
expect(right).toBe("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have all required text alignment properties", () => {
|
|
||||||
expect(TextAlign).toHaveProperty("LEFT");
|
|
||||||
expect(TextAlign).toHaveProperty("CENTER");
|
|
||||||
expect(TextAlign).toHaveProperty("RIGHT");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have string values for alignments", () => {
|
|
||||||
expect(typeof TextAlign.LEFT).toBe("string");
|
|
||||||
expect(typeof TextAlign.CENTER).toBe("string");
|
|
||||||
expect(typeof TextAlign.RIGHT).toBe("string");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DEFAULT_TEXT_ALIGN", () => {
|
describe("Assets Existence", () => {
|
||||||
it("should be CENTER by default", () => {
|
it("should verify that the default background image exists", () => {
|
||||||
expect(DEFAULT_TEXT_ALIGN).toBe("center");
|
const { DEFAULT_BACKGROUND_IMAGE } = Constants.PANEL_SETTINGS;
|
||||||
});
|
|
||||||
|
|
||||||
it("should be of type TextAlignType", () => {
|
const relativePath = DEFAULT_BACKGROUND_IMAGE.replace(/^\.\//, "");
|
||||||
const align: TextAlignType = DEFAULT_TEXT_ALIGN;
|
const fullPath = path.resolve(process.cwd(), "static", relativePath);
|
||||||
expect(align).toBe("center");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should match one of the TextAlign values", () => {
|
const exists = fs.existsSync(fullPath);
|
||||||
expect([TextAlign.LEFT, TextAlign.CENTER, TextAlign.RIGHT]).toContain(DEFAULT_TEXT_ALIGN);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Constants integration", () => {
|
expect(exists, `Image not found at: ${fullPath}`).toBe(true);
|
||||||
it("should have consistent panel dimensions", () => {
|
|
||||||
expect(PANEL_SETTINGS.PANEL_WIDTH).toBe(320);
|
|
||||||
expect(TYPOGRAPHY.PADDING_X_MAX).toBeLessThan(PANEL_SETTINGS.PANEL_WIDTH / 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have valid text constraints for panel width", () => {
|
|
||||||
const maxTextWidth = PANEL_SETTINGS.PANEL_WIDTH - 2 * TYPOGRAPHY.PADDING_X_MAX;
|
|
||||||
expect(maxTextWidth).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have reasonable font size range for panel height", () => {
|
|
||||||
const minFontSize = TYPOGRAPHY.FONT_SIZE_MIN;
|
|
||||||
const maxFontSize = TYPOGRAPHY.FONT_SIZE_MAX;
|
|
||||||
const panelHeight = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT;
|
|
||||||
|
|
||||||
expect(minFontSize).toBeLessThan(panelHeight);
|
|
||||||
expect(maxFontSize).toBeLessThan(panelHeight * 2);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { STATE_DATA, withPersistence } from "$states/persisted.svelte";
|
import { STATE_DATA, withPersistence } from "$states/persisted.svelte";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
// Mock localStorage
|
|
||||||
const localStorageMock = (() => {
|
const localStorageMock = (() => {
|
||||||
let store: Record<string, string> = {};
|
let store: Record<string, string> = {};
|
||||||
return {
|
return {
|
||||||
@@ -62,7 +61,6 @@ describe("persisted.svelte", () => {
|
|||||||
[STATE_DATA]: { value: 0 },
|
[STATE_DATA]: { value: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Should not throw and state should remain unchanged
|
|
||||||
expect(() => withPersistence("test-key", state)).not.toThrow();
|
expect(() => withPersistence("test-key", state)).not.toThrow();
|
||||||
expect(state[STATE_DATA]).toEqual({ value: 0 });
|
expect(state[STATE_DATA]).toEqual({ value: 0 });
|
||||||
});
|
});
|
||||||
@@ -88,10 +86,8 @@ describe("persisted.svelte", () => {
|
|||||||
withPersistence("test-key", state, 500);
|
withPersistence("test-key", state, 500);
|
||||||
|
|
||||||
state[STATE_DATA] = { count: 2 };
|
state[STATE_DATA] = { count: 2 };
|
||||||
// Wait for effect to set the debounced timeout
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
// Fast-forward time to trigger debounced save
|
|
||||||
vi.advanceTimersByTime(500);
|
vi.advanceTimersByTime(500);
|
||||||
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
@@ -110,7 +106,6 @@ describe("persisted.svelte", () => {
|
|||||||
|
|
||||||
state[STATE_DATA] = { count: 2 };
|
state[STATE_DATA] = { count: 2 };
|
||||||
|
|
||||||
// Wait for effect to run
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 2 }));
|
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 2 }));
|
||||||
@@ -118,10 +113,6 @@ describe("persisted.svelte", () => {
|
|||||||
|
|
||||||
it("should cleanup timeout on state change during debounce", async () => {
|
it("should cleanup timeout on state change during debounce", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
// const state = {
|
|
||||||
// [STATE_DATA]: { count: 1 },
|
|
||||||
// };
|
|
||||||
|
|
||||||
const data = $state({ count: 1 });
|
const data = $state({ count: 1 });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -135,21 +126,15 @@ describe("persisted.svelte", () => {
|
|||||||
|
|
||||||
withPersistence("test-key", state, 500);
|
withPersistence("test-key", state, 500);
|
||||||
|
|
||||||
// First change - sets timeout for count:2
|
|
||||||
state[STATE_DATA] = { count: 2 };
|
state[STATE_DATA] = { count: 2 };
|
||||||
// Let effect run and set the timeout
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
// Second change before first timeout fires - should clear previous and set new timeout for count:3
|
|
||||||
state[STATE_DATA] = { count: 3 };
|
state[STATE_DATA] = { count: 3 };
|
||||||
// Let effect run and clear old timeout, set new one
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
// Run only the currently pending timer (the one for count:3)
|
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
// Should only have saved the latest value (count:3)
|
|
||||||
expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
|
expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
|
||||||
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 3 }));
|
expect(localStorageMock.setItem).toHaveBeenCalledWith("test-key", JSON.stringify({ count: 3 }));
|
||||||
|
|
||||||
@@ -163,13 +148,10 @@ describe("persisted.svelte", () => {
|
|||||||
|
|
||||||
withPersistence("test-key", state);
|
withPersistence("test-key", state);
|
||||||
|
|
||||||
// The default debounce should be used (0 in test mode)
|
|
||||||
state[STATE_DATA] = { test: false };
|
state[STATE_DATA] = { test: false };
|
||||||
|
|
||||||
// Wait for effect to run
|
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
// In test mode, DEBOUNCE_DURATION is 0, so immediate save
|
|
||||||
expect(localStorageMock.setItem).toHaveBeenCalled();
|
expect(localStorageMock.setItem).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user