style: add eslint and prettier, fix style errors

This commit is contained in:
2026-02-13 09:59:19 +05:00
parent 5d6159fe29
commit 387c1751c6
101 changed files with 14596 additions and 12946 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+9
View File
@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": false,
"singleQuote": false,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+40
View File
@@ -0,0 +1,40 @@
import { includeIgnoreFile } from "@eslint/compat";
import js from "@eslint/js";
import prettier from "eslint-config-prettier";
import svelte from "eslint-plugin-svelte";
import { defineConfig } from "eslint/config";
import globals from "globals";
import path from "node:path";
import ts from "typescript-eslint";
import svelteConfig from "./svelte.config.js";
const gitignorePath = path.resolve(import.meta.dirname, ".gitignore");
export default defineConfig(
{
ignores: ["reference/"],
},
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
"no-undef": "off",
},
},
{
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: [".svelte"],
parser: ts.parser,
svelteConfig,
},
},
},
);
+1474 -8
View File
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -11,6 +11,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"lint:css": "stylelint '**/*.{css,svelte}", "lint:css": "stylelint '**/*.{css,svelte}",
"check:css": "node scripts/css-vars.js", "check:css": "node scripts/css-vars.js",
"check:all": "npm run check && npm run check:css && npm run lint:css && npm run lint && npm run test:run",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
@@ -18,9 +19,13 @@
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"test:unit": "vitest run tests/unit", "test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration", "test:integration": "vitest run tests/integration",
"test:e2e": "playwright test" "test:e2e": "playwright test",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@iconify-json/lucide": "^1.2.89", "@iconify-json/lucide": "^1.2.89",
"@playwright/test": "^1.58.1", "@playwright/test": "^1.58.1",
"@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/adapter-auto": "^7.0.0",
@@ -31,18 +36,25 @@
"@testing-library/svelte": "^5.3.1", "@testing-library/svelte": "^5.3.1",
"@testing-library/user-event": "^14.6.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": "^20",
"@vitest/coverage-istanbul": "^4.0.18", "@vitest/coverage-istanbul": "^4.0.18",
"@vitest/ui": "^4.0.18", "@vitest/ui": "^4.0.18",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0",
"glob": "^13.0.2", "glob": "^13.0.2",
"globals": "^17.3.0",
"jsdom": "^28.0.0", "jsdom": "^28.0.0",
"konva": "^10.2.0", "konva": "^10.2.0",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"stylelint": "^17.2.0", "stylelint": "^17.2.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^18.0.0", "stylelint-config-recommended": "^18.0.0",
"svelte": "^5.48.2", "svelte": "^5.48.2",
"svelte-check": "^4.3.5", "svelte-check": "^4.3.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"unplugin-icons": "^23.0.1", "unplugin-icons": "^23.0.1",
"vite": "^7.3.1", "vite": "^7.3.1",
"vitest": "^4.0.18", "vitest": "^4.0.18",
+44 -6
View File
@@ -12,7 +12,13 @@
<div class="header-content"> <div class="header-content">
<h1>Twitch Panels</h1> <h1>Twitch Panels</h1>
<button id="themeToggle" class="theme-toggle" aria-label="Toggle theme"> <button id="themeToggle" class="theme-toggle" aria-label="Toggle theme">
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="sun-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5"></circle> <circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line> <line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line> <line x1="12" y1="21" x2="12" y2="23"></line>
@@ -23,7 +29,13 @@
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg> </svg>
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg
class="moon-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg> </svg>
</button> </button>
@@ -37,7 +49,12 @@
<h2 class="card-title">Тексты панелей</h2> <h2 class="card-title">Тексты панелей</h2>
<div class="input-group"> <div class="input-group">
<input type="text" id="panelTextInput" placeholder="Введите текст..." class="text-input" /> <input
type="text"
id="panelTextInput"
placeholder="Введите текст..."
class="text-input"
/>
<button id="addTextBtn" class="btn btn-primary"> <button id="addTextBtn" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line> <line x1="12" y1="5" x2="12" y2="19"></line>
@@ -124,7 +141,14 @@
<div class="setting-row"> <div class="setting-row">
<label class="setting-label">Смещение</label> <label class="setting-label">Смещение</label>
<div class="setting-control"> <div class="setting-control">
<input type="range" id="centerOffset" min="-50" max="50" value="0" class="slider" /> <input
type="range"
id="centerOffset"
min="-50"
max="50"
value="0"
class="slider"
/>
<span class="value-display" id="centerOffsetValue">0</span> <span class="value-display" id="centerOffsetValue">0</span>
</div> </div>
</div> </div>
@@ -176,7 +200,14 @@
<div class="setting-row"> <div class="setting-row">
<label class="setting-label">Яркость</label> <label class="setting-label">Яркость</label>
<div class="setting-control"> <div class="setting-control">
<input type="range" id="bgBrightness" min="50" max="150" value="100" class="slider" /> <input
type="range"
id="bgBrightness"
min="50"
max="150"
value="100"
class="slider"
/>
<span class="value-display" id="bgBrightnessValue">100</span> <span class="value-display" id="bgBrightnessValue">100</span>
</div> </div>
</div> </div>
@@ -184,7 +215,14 @@
<div class="setting-row"> <div class="setting-row">
<label class="setting-label">Контраст</label> <label class="setting-label">Контраст</label>
<div class="setting-control"> <div class="setting-control">
<input type="range" id="bgContrast" min="50" max="150" value="100" class="slider" /> <input
type="range"
id="bgContrast"
min="50"
max="150"
value="100"
class="slider"
/>
<span class="value-display" id="bgContrastValue">100</span> <span class="value-display" id="bgContrastValue">100</span>
</div> </div>
</div> </div>
+71 -16
View File
@@ -325,7 +325,12 @@ function handleCropMouseDown(e) {
if (clickedHandle) { if (clickedHandle) {
isResizing = true; isResizing = true;
resizeHandle = clickedHandle; resizeHandle = clickedHandle;
} else if (x >= cropBox.x && x <= cropBox.x + cropBox.width && y >= cropBox.y && y <= cropBox.y + cropBox.height) { } else if (
x >= cropBox.x &&
x <= cropBox.x + cropBox.width &&
y >= cropBox.y &&
y <= cropBox.y + cropBox.height
) {
isDragging = true; isDragging = true;
} }
@@ -350,38 +355,74 @@ function handleCropMouseMove(e) {
switch (resizeHandle) { switch (resizeHandle) {
case "nw": case "nw":
cropBox.x = Math.max(0, Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize)); cropBox.x = Math.max(
cropBox.y = Math.max(0, Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize)); 0,
Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize),
);
cropBox.y = Math.max(
0,
Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize),
);
cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x); cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x);
cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y); cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
break; break;
case "ne": case "ne":
cropBox.y = Math.max(0, Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize)); cropBox.y = Math.max(
cropBox.width = Math.max(minSize, Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x)); 0,
Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize),
);
cropBox.width = Math.max(
minSize,
Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x),
);
cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y); cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
break; break;
case "sw": case "sw":
cropBox.x = Math.max(0, Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize)); cropBox.x = Math.max(
0,
Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize),
);
cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x); cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x);
cropBox.height = Math.max(minSize, Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y)); cropBox.height = Math.max(
minSize,
Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y),
);
break; break;
case "se": case "se":
cropBox.width = Math.max(minSize, Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x)); cropBox.width = Math.max(
cropBox.height = Math.max(minSize, Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y)); minSize,
Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x),
);
cropBox.height = Math.max(
minSize,
Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y),
);
break; break;
case "n": case "n":
cropBox.y = Math.max(0, Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize)); cropBox.y = Math.max(
0,
Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize),
);
cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y); cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
break; break;
case "s": case "s":
cropBox.height = Math.max(minSize, Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y)); cropBox.height = Math.max(
minSize,
Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y),
);
break; break;
case "w": case "w":
cropBox.x = Math.max(0, Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize)); cropBox.x = Math.max(
0,
Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize),
);
cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x); cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x);
break; break;
case "e": case "e":
cropBox.width = Math.max(minSize, Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x)); cropBox.width = Math.max(
minSize,
Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x),
);
break; break;
} }
} }
@@ -422,7 +463,17 @@ function applyCrop() {
tempCanvas.width = cropWidth; tempCanvas.width = cropWidth;
tempCanvas.height = cropHeight; tempCanvas.height = cropHeight;
tempCtx.drawImage(originalImage, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); tempCtx.drawImage(
originalImage,
cropX,
cropY,
cropWidth,
cropHeight,
0,
0,
cropWidth,
cropHeight,
);
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
@@ -480,7 +531,8 @@ function updatePanelNavigation() {
panels.length > 0 ? `${currentPanelIndex + 1} / ${panels.length}` : "0 / 0"; panels.length > 0 ? `${currentPanelIndex + 1} / ${panels.length}` : "0 / 0";
document.getElementById("prevPanel").disabled = currentPanelIndex === 0 || panels.length === 0; document.getElementById("prevPanel").disabled = currentPanelIndex === 0 || panels.length === 0;
document.getElementById("nextPanel").disabled = currentPanelIndex >= panels.length - 1 || panels.length === 0; document.getElementById("nextPanel").disabled =
currentPanelIndex >= panels.length - 1 || panels.length === 0;
document.getElementById("downloadCurrentBtn").disabled = panels.length === 0; document.getElementById("downloadCurrentBtn").disabled = panels.length === 0;
document.getElementById("downloadAllBtn").disabled = panels.length === 0; document.getElementById("downloadAllBtn").disabled = panels.length === 0;
} }
@@ -498,7 +550,10 @@ function drawPanel(panel, canvasId) {
if (backgroundImage) { if (backgroundImage) {
ctx.filter = `brightness(${settings.bgBrightness}%) contrast(${settings.bgContrast}%)`; ctx.filter = `brightness(${settings.bgBrightness}%) contrast(${settings.bgContrast}%)`;
const scale = Math.max(canvas.width / backgroundImage.width, canvas.height / backgroundImage.height); const scale = Math.max(
canvas.width / backgroundImage.width,
canvas.height / backgroundImage.height,
);
const scaledWidth = backgroundImage.width * scale; const scaledWidth = backgroundImage.width * scale;
const scaledHeight = backgroundImage.height * scale; const scaledHeight = backgroundImage.height * scale;
const x = (canvas.width - scaledWidth) / 2; const x = (canvas.width - scaledWidth) / 2;
+2 -2
View File
@@ -52,7 +52,7 @@
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
@@ -444,7 +444,7 @@ body {
} }
.color-value { .color-value {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--text-secondary); color: var(--text-secondary);
+1 -1
View File
@@ -48,7 +48,7 @@
--text-main: oklch(from var(--brand-main) 0.94 0.01 h); --text-main: oklch(from var(--brand-main) 0.94 0.01 h);
--text-muted: oklch(from var(--brand-main) 0.75 0.02 h); --text-muted: oklch(from var(--brand-main) 0.75 0.02 h);
--text-action: oklch(from var(--brand-main) 0.12 0.02 h); /* --text-action: oklch(from var(--brand-main) 0.12 0.02 h); */
--border-main: oklch(from var(--brand-main) 0.22 0.04 h); --border-main: oklch(from var(--brand-main) 0.22 0.04 h);
--border-strong: oklch(from var(--brand-main) 0.3 0.06 h); --border-strong: oklch(from var(--brand-main) 0.3 0.06 h);
-1
View File
@@ -10,7 +10,6 @@
let { title, children, titleSnippet = emptySnippet }: Props = $props(); let { title, children, titleSnippet = emptySnippet }: Props = $props();
</script> </script>
<!-- svelte-ignore block_empty -->
{#snippet emptySnippet()}{/snippet} {#snippet emptySnippet()}{/snippet}
<section class="card"> <section class="card">
+7 -1
View File
@@ -26,7 +26,13 @@
} }
</script> </script>
<Button icon={ChevronLeft} ariaLabel="Previous slide" type="mini" onclick={prev} disabled={isFirst} /> <Button
icon={ChevronLeft}
ariaLabel="Previous slide"
type="mini"
onclick={prev}
disabled={isFirst}
/>
<span class="panel-indicator">{current + 1} / {max}</span> <span class="panel-indicator">{current + 1} / {max}</span>
<Button icon={ChevronRight} ariaLabel="Next slide" type="mini" onclick={next} disabled={isLast} /> <Button icon={ChevronRight} ariaLabel="Next slide" type="mini" onclick={next} disabled={isLast} />
+10 -2
View File
@@ -17,7 +17,9 @@
let current: number = $state(0); let current: number = $state(0);
let direction: SlideDirectionType = $state("next"); let direction: SlideDirectionType = $state("next");
let xDirection = $derived(direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH); let xDirection = $derived(
direction == "next" ? PANEL_SETTINGS.PANEL_WIDTH : -PANEL_SETTINGS.PANEL_WIDTH,
);
$effect(() => { $effect(() => {
if (textsState.texts.length === 0) { if (textsState.texts.length === 0) {
@@ -78,7 +80,13 @@
<div class="panel-actions"> <div class="panel-actions">
<Button label="Скачать всё" ariaLabel="Download all" icon={Download} onclick={downloadAll} /> <Button label="Скачать всё" ariaLabel="Download all" icon={Download} onclick={downloadAll} />
<Button label="Скачать" ariaLabel="Download current" type="secondary" icon={Download} onclick={downloadCurrent} /> <Button
label="Скачать"
ariaLabel="Download current"
type="secondary"
icon={Download}
onclick={downloadCurrent}
/>
</div> </div>
</div> </div>
</Card> </Card>
+1 -1
View File
@@ -56,7 +56,7 @@
font-family: inherit; font-family: inherit;
white-space: nowrap; white-space: nowrap;
background-color: var(--btn-main); background-color: var(--btn-main);
color: oklch(from var(--btn-main) 99% 0.05 h); color: var(--text-action);
} }
.btn :global(svg) { .btn :global(svg) {
+1 -1
View File
@@ -41,7 +41,7 @@ export class DownloadService {
async downloadAll(panels: Array<DownloadItem>): Promise<DownloadResult> { async downloadAll(panels: Array<DownloadItem>): Promise<DownloadResult> {
try { try {
const zip = new JSZip(); const zip = new JSZip();
for (let panel of panels) { for (const panel of panels) {
const blob = await this.stageToBlob(panel.stage); const blob = await this.stageToBlob(panel.stage);
zip.file(`${panel.filename}.png`, blob); zip.file(`${panel.filename}.png`, blob);
} }
-4
View File
@@ -3,10 +3,6 @@ import { browser } from "$app/environment";
export const STATE_DATA = Symbol("state-data"); export const STATE_DATA = Symbol("state-data");
export const DEBOUNCE_DURATION = import.meta.env.MODE === "test" ? 0 : 500; export const DEBOUNCE_DURATION = import.meta.env.MODE === "test" ? 0 : 500;
type OnlyData<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
export interface Persistable<D> { export interface Persistable<D> {
[STATE_DATA]: D; [STATE_DATA]: D;
} }
+1 -1
View File
@@ -20,7 +20,7 @@ function createState() {
paddingX: 10, paddingX: 10,
offsetY: 0, offsetY: 0,
}; };
let state: TextConfig = $state({ ...defaults }); const state: TextConfig = $state({ ...defaults });
return { return {
get fontSize() { get fontSize() {
+4 -1
View File
@@ -5,7 +5,10 @@ export interface TextItem {
id: number; id: number;
} }
const defaultTexts: Array<TextItem> = ["About me", "Links", "Projects"].map((text, idx) => ({ text, id: idx })); const defaultTexts: Array<TextItem> = ["About me", "Links", "Projects"].map((text, idx) => ({
text,
id: idx,
}));
export function createState() { export function createState() {
let texts: Array<TextItem> = $state(defaultTexts); let texts: Array<TextItem> = $state(defaultTexts);
@@ -5,6 +5,7 @@ import { STATE_DATA } from "$states/persisted.svelte";
import { textsState } from "$states/texts.svelte"; import { textsState } from "$states/texts.svelte";
import { render, screen, waitFor } from "@testing-library/svelte"; import { render, screen, waitFor } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import type { Stage } from "svelte-konva";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("$services/downloadService", () => ({ vi.mock("$services/downloadService", () => ({
@@ -57,7 +58,7 @@ describe("PreviewManager Integration", () => {
it("should call downloadPanel with current active text", async () => { it("should call downloadPanel with current active text", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
textsState[STATE_DATA] = ["First", "Second"]; textsState[STATE_DATA] = ["First", "Second"];
konvaStageState.stage = { node: { id: "stage-ref" } } as any; konvaStageState.stage = { node: { id: "stage-ref" } } as unknown as Stage;
render(PreviewManager); render(PreviewManager);
@@ -30,7 +30,6 @@ describe("Alignment.svelte", () => {
}); });
it("should sync with initial state and change state", async () => { it("should sync with initial state and change state", async () => {
const user = userEvent.setup();
const { rerender } = render(AlignmentTest, { props: { align: TextAlign.RIGHT } }); const { rerender } = render(AlignmentTest, { props: { align: TextAlign.RIGHT } });
const stateDisplay = screen.getByTestId("align-value"); const stateDisplay = screen.getByTestId("align-value");
+3 -1
View File
@@ -13,7 +13,9 @@ describe("Application Constants Logic", () => {
expect(TYPOGRAPHY.FONT_FAMILIES).toContain(TYPOGRAPHY.FONT_FAMILY_DEFAULT); expect(TYPOGRAPHY.FONT_FAMILIES).toContain(TYPOGRAPHY.FONT_FAMILY_DEFAULT);
expect(PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT).toBeLessThanOrEqual(PANEL_SETTINGS.PANEL_HEIGHT_MAX); expect(PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT).toBeLessThanOrEqual(
PANEL_SETTINGS.PANEL_HEIGHT_MAX,
);
expect(TYPOGRAPHY.VERTICAL_OFFSET_MAX).toBeGreaterThan(TYPOGRAPHY.VERTICAL_OFFSET_MIN); expect(TYPOGRAPHY.VERTICAL_OFFSET_MAX).toBeGreaterThan(TYPOGRAPHY.VERTICAL_OFFSET_MIN);
}); });
+13 -7
View File
@@ -1,6 +1,7 @@
import { DownloadService, type DownloadItem } from "$services/downloadService"; import { DownloadService, type DownloadItem } from "$services/downloadService";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import JSZip from "jszip"; import JSZip from "jszip";
import type { Stage } from "konva/lib/Stage";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("file-saver", () => { vi.mock("file-saver", () => {
@@ -24,17 +25,17 @@ vi.mock("jszip", () => {
describe("DownloadService", () => { describe("DownloadService", () => {
let service: DownloadService; let service: DownloadService;
let mockKonvaStage: any; let mockKonvaStage: Stage;
beforeEach(() => { beforeEach(() => {
service = new DownloadService(); service = new DownloadService();
vi.clearAllMocks(); vi.clearAllMocks();
mockKonvaStage = { mockKonvaStage = {
toBlob: vi.fn(({ callback }) => { toBlob: vi.fn(async ({ callback }) => {
callback(new Blob(["test image data"], { type: "image/png" })); callback(new Blob(["test image data"], { type: "image/png" }));
}), }),
}; } as unknown as Stage;
}); });
describe("downloadPanel", () => { describe("downloadPanel", () => {
@@ -52,7 +53,10 @@ describe("DownloadService", () => {
it("should handle Konva stage without toBlob method", async () => { it("should handle Konva stage without toBlob method", async () => {
const invalidStage = { toBlob: undefined }; const invalidStage = { toBlob: undefined };
const result = await service.downloadPanel(invalidStage as any, "test-panel-1.png"); const result = await service.downloadPanel(
invalidStage as unknown as Stage,
"test-panel-1.png",
);
expect(result.success).toBe(false); expect(result.success).toBe(false);
if (!result.success) { if (!result.success) {
@@ -61,7 +65,7 @@ describe("DownloadService", () => {
}); });
it("should handle blob creation failure", async () => { it("should handle blob creation failure", async () => {
mockKonvaStage.toBlob = vi.fn(({ callback }) => { mockKonvaStage.toBlob = vi.fn(async ({ callback }) => {
callback(null); callback(null);
}); });
@@ -98,7 +102,7 @@ describe("DownloadService", () => {
{ filename: "test-panel-5", stage: mockKonvaStage }, { filename: "test-panel-5", stage: mockKonvaStage },
]; ];
const result = await service.downloadAll(panels); const result = await service.downloadAll(panels);
const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0]; // const [blobArg, fileNameArg] = vi.mocked(saveAs).mock.calls[0];
const zipInstance = vi.mocked(JSZip).mock.instances[0]; const zipInstance = vi.mocked(JSZip).mock.instances[0];
expect(result.success).toBe(true); expect(result.success).toBe(true);
@@ -113,7 +117,9 @@ describe("DownloadService", () => {
it("should handle failure", async () => { it("should handle failure", async () => {
const invalidStage = { toBlob: undefined }; const invalidStage = { toBlob: undefined };
const panels: Array<DownloadItem> = [{ filename: "test-panel-1", stage: invalidStage as any }]; const panels: Array<DownloadItem> = [
{ filename: "test-panel-1", stage: invalidStage as unknown as Stage },
];
const result = await service.downloadAll(panels); const result = await service.downloadAll(panels);
expect(result.success).toBe(false); expect(result.success).toBe(false);
+35 -11
View File
@@ -2,30 +2,48 @@ import { PANEL_SETTINGS } from "$lib/constants";
import { imageConfigState, ImageConfigState } from "$states/imageConfig.svelte"; import { imageConfigState, ImageConfigState } from "$states/imageConfig.svelte";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
let lastOnload: (() => void) | null = null; type ImageEventHandler = ((this: HTMLImageElement, ev?: Event) => void) | null;
let lastOnerror: (() => void) | null = 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( vi.stubGlobal(
"Image", "Image",
class { class {
_onload: (() => void) | null = null; _onload: ImageEventHandler = null;
_onerror: (() => void) | null = null; _onerror: ImageErrorEventHandler = null;
_src: string = ""; _src: string = "";
crossOrigin: string = ""; crossOrigin: string = "";
set src(val: string) { set src(val: string) {
this._src = val; this._src = val;
if (val.includes("error")) { if (val.includes("error")) {
setTimeout(() => this._onerror?.(), 1); setTimeout(
() => this._onerror?.call(this as unknown as HTMLImageElement, new Event("error")),
1,
);
} else { } else {
setTimeout(() => this._onload?.(), 1); setTimeout(
() => this._onload?.call(this as unknown as HTMLImageElement, new Event("load")),
1,
);
} }
} }
get src() { get src() {
return this._src; return this._src;
} }
set onload(val: any) { set onload(val: ImageEventHandler) {
this._onload = val; this._onload = val;
if (val) lastOnload = val; if (val) lastOnload = val;
} }
@@ -33,7 +51,7 @@ vi.stubGlobal(
return this._onload; return this._onload;
} }
set onerror(val: any) { set onerror(val: ImageErrorEventHandler) {
this._onerror = val; this._onerror = val;
if (val) lastOnerror = val; if (val) lastOnerror = val;
} }
@@ -94,7 +112,9 @@ describe("ImageConfigState", () => {
}); });
it("should handle image loading error", async () => { it("should handle image loading error", async () => {
await expect(imageConfigState.uploadImageByLink("error-link")).rejects.toThrow("Failed to load image"); await expect(imageConfigState.uploadImageByLink("error-link")).rejects.toThrow(
"Failed to load image",
);
expect(imageConfigState.imageReady).toBe(false); expect(imageConfigState.imageReady).toBe(false);
}); });
@@ -173,7 +193,9 @@ describe("ImageConfigState", () => {
imageConfigState.destroy(); imageConfigState.destroy();
if (lastOnload) lastOnload(); if (lastOnload) {
lastOnload.call(new Image() as HTMLImageElement, new Event("load"));
}
await expect(promise).rejects.toThrow(); await expect(promise).rejects.toThrow();
expect(imageConfigState.imageReady).toBe(false); expect(imageConfigState.imageReady).toBe(false);
@@ -184,7 +206,9 @@ describe("ImageConfigState", () => {
imageConfigState.destroy(); imageConfigState.destroy();
if (lastOnerror) lastOnerror(); if (lastOnerror) {
lastOnerror.call(new Image() as HTMLImageElement, new Event("load"));
}
await expect(promise).rejects.toThrow(); await expect(promise).rejects.toThrow();
expect(imageConfigState.imageReady).toBe(false); expect(imageConfigState.imageReady).toBe(false);
+7 -47
View File
@@ -1,54 +1,14 @@
import { konvaStageState } from "$states/konvaStage.svelte"; import { konvaStageState } from "$states/konvaStage.svelte";
import type { Stage } from "svelte-konva";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
describe("konvaStage.svelte", () => { describe("konvaStageState integration", () => {
describe("initial state", () => { it("should work", () => {
it("should have undefined stage initially", () => { expect(konvaStageState.stage).toBeUndefined();
expect(konvaStageState.stage).toBeUndefined();
});
it("should have stage getter", () => { const mock = { name: "stage" } as unknown as Stage;
expect(konvaStageState).toHaveProperty("stage"); konvaStageState.stage = mock;
});
it("should have stage setter", () => { expect(konvaStageState.stage).toStrictEqual(mock);
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);
});
}); });
}); });
+12 -3
View File
@@ -92,7 +92,10 @@ describe("persisted.svelte", () => {
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 }),
);
vi.useRealTimers(); vi.useRealTimers();
}); });
@@ -108,7 +111,10 @@ describe("persisted.svelte", () => {
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 }),
);
}); });
it("should cleanup timeout on state change during debounce", async () => { it("should cleanup timeout on state change during debounce", async () => {
@@ -136,7 +142,10 @@ describe("persisted.svelte", () => {
await Promise.resolve(); await Promise.resolve();
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 }),
);
vi.useRealTimers(); vi.useRealTimers();
}); });