style: add eslint and prettier, fix style errors
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,9 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
Generated
+1474
-8
File diff suppressed because it is too large
Load Diff
+14
-2
@@ -11,6 +11,7 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"lint:css": "stylelint '**/*.{css,svelte}",
|
||||
"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",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
@@ -18,9 +19,13 @@
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:unit": "vitest run tests/unit",
|
||||
"test:integration": "vitest run tests/integration",
|
||||
"test:e2e": "playwright test"
|
||||
"test:e2e": "playwright test",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@iconify-json/lucide": "^1.2.89",
|
||||
"@playwright/test": "^1.58.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
@@ -31,18 +36,25 @@
|
||||
"@testing-library/svelte": "^5.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/node": "^20",
|
||||
"@vitest/coverage-istanbul": "^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",
|
||||
"globals": "^17.3.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"konva": "^10.2.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"stylelint": "^17.2.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended": "^18.0.0",
|
||||
"svelte": "^5.48.2",
|
||||
"svelte-check": "^4.3.5",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"unplugin-icons": "^23.0.1",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.18",
|
||||
|
||||
+44
-6
@@ -12,7 +12,13 @@
|
||||
<div class="header-content">
|
||||
<h1>Twitch Panels</h1>
|
||||
<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>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></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="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</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>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -37,7 +49,12 @@
|
||||
<h2 class="card-title">Тексты панелей</h2>
|
||||
|
||||
<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">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
@@ -124,7 +141,14 @@
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Смещение</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +200,14 @@
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Яркость</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,7 +215,14 @@
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Контраст</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+71
-16
@@ -325,7 +325,12 @@ function handleCropMouseDown(e) {
|
||||
if (clickedHandle) {
|
||||
isResizing = true;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -350,38 +355,74 @@ function handleCropMouseMove(e) {
|
||||
|
||||
switch (resizeHandle) {
|
||||
case "nw":
|
||||
cropBox.x = Math.max(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.x = Math.max(
|
||||
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.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
|
||||
break;
|
||||
case "ne":
|
||||
cropBox.y = Math.max(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.y = Math.max(
|
||||
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);
|
||||
break;
|
||||
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.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;
|
||||
case "se":
|
||||
cropBox.width = Math.max(minSize, Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x));
|
||||
cropBox.height = Math.max(minSize, Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y));
|
||||
cropBox.width = Math.max(
|
||||
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;
|
||||
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);
|
||||
break;
|
||||
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;
|
||||
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);
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -422,7 +463,17 @@ function applyCrop() {
|
||||
tempCanvas.width = cropWidth;
|
||||
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();
|
||||
img.onload = () => {
|
||||
@@ -480,7 +531,8 @@ function updatePanelNavigation() {
|
||||
panels.length > 0 ? `${currentPanelIndex + 1} / ${panels.length}` : "0 / 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("downloadAllBtn").disabled = panels.length === 0;
|
||||
}
|
||||
@@ -498,7 +550,10 @@ function drawPanel(panel, canvasId) {
|
||||
if (backgroundImage) {
|
||||
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 scaledHeight = backgroundImage.height * scale;
|
||||
const x = (canvas.width - scaledWidth) / 2;
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
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);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
@@ -444,7 +444,7 @@ body {
|
||||
}
|
||||
|
||||
.color-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
|
||||
--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-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-strong: oklch(from var(--brand-main) 0.3 0.06 h);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
let { title, children, titleSnippet = emptySnippet }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore block_empty -->
|
||||
{#snippet emptySnippet()}{/snippet}
|
||||
|
||||
<section class="card">
|
||||
|
||||
@@ -26,7 +26,13 @@
|
||||
}
|
||||
</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>
|
||||
<Button icon={ChevronRight} ariaLabel="Next slide" type="mini" onclick={next} disabled={isLast} />
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
let current: number = $state(0);
|
||||
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(() => {
|
||||
if (textsState.texts.length === 0) {
|
||||
@@ -78,7 +80,13 @@
|
||||
|
||||
<div class="panel-actions">
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
background-color: var(--btn-main);
|
||||
color: oklch(from var(--btn-main) 99% 0.05 h);
|
||||
color: var(--text-action);
|
||||
}
|
||||
|
||||
.btn :global(svg) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class DownloadService {
|
||||
async downloadAll(panels: Array<DownloadItem>): Promise<DownloadResult> {
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
for (let panel of panels) {
|
||||
for (const panel of panels) {
|
||||
const blob = await this.stageToBlob(panel.stage);
|
||||
zip.file(`${panel.filename}.png`, blob);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ 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<T> = {
|
||||
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
||||
};
|
||||
|
||||
export interface Persistable<D> {
|
||||
[STATE_DATA]: D;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ function createState() {
|
||||
paddingX: 10,
|
||||
offsetY: 0,
|
||||
};
|
||||
let state: TextConfig = $state({ ...defaults });
|
||||
const state: TextConfig = $state({ ...defaults });
|
||||
|
||||
return {
|
||||
get fontSize() {
|
||||
|
||||
@@ -5,7 +5,10 @@ export interface TextItem {
|
||||
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() {
|
||||
let texts: Array<TextItem> = $state(defaultTexts);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { STATE_DATA } from "$states/persisted.svelte";
|
||||
import { textsState } from "$states/texts.svelte";
|
||||
import { render, screen, waitFor } from "@testing-library/svelte";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { Stage } from "svelte-konva";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("$services/downloadService", () => ({
|
||||
@@ -57,7 +58,7 @@ describe("PreviewManager Integration", () => {
|
||||
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;
|
||||
konvaStageState.stage = { node: { id: "stage-ref" } } as unknown as Stage;
|
||||
|
||||
render(PreviewManager);
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ describe("Alignment.svelte", () => {
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ describe("Application Constants Logic", () => {
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DownloadService, type DownloadItem } from "$services/downloadService";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import type { Stage } from "konva/lib/Stage";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("file-saver", () => {
|
||||
@@ -24,17 +25,17 @@ vi.mock("jszip", () => {
|
||||
|
||||
describe("DownloadService", () => {
|
||||
let service: DownloadService;
|
||||
let mockKonvaStage: any;
|
||||
let mockKonvaStage: Stage;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new DownloadService();
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockKonvaStage = {
|
||||
toBlob: vi.fn(({ callback }) => {
|
||||
toBlob: vi.fn(async ({ callback }) => {
|
||||
callback(new Blob(["test image data"], { type: "image/png" }));
|
||||
}),
|
||||
};
|
||||
} as unknown as Stage;
|
||||
});
|
||||
|
||||
describe("downloadPanel", () => {
|
||||
@@ -52,7 +53,10 @@ describe("DownloadService", () => {
|
||||
it("should handle Konva stage without toBlob method", async () => {
|
||||
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);
|
||||
if (!result.success) {
|
||||
@@ -61,7 +65,7 @@ describe("DownloadService", () => {
|
||||
});
|
||||
|
||||
it("should handle blob creation failure", async () => {
|
||||
mockKonvaStage.toBlob = vi.fn(({ callback }) => {
|
||||
mockKonvaStage.toBlob = vi.fn(async ({ callback }) => {
|
||||
callback(null);
|
||||
});
|
||||
|
||||
@@ -98,7 +102,7 @@ describe("DownloadService", () => {
|
||||
{ filename: "test-panel-5", stage: mockKonvaStage },
|
||||
];
|
||||
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];
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
@@ -113,7 +117,9 @@ describe("DownloadService", () => {
|
||||
|
||||
it("should handle failure", async () => {
|
||||
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);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
@@ -2,30 +2,48 @@ import { PANEL_SETTINGS } from "$lib/constants";
|
||||
import { imageConfigState, ImageConfigState } from "$states/imageConfig.svelte";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let lastOnload: (() => void) | null = null;
|
||||
let lastOnerror: (() => void) | null = null;
|
||||
type ImageEventHandler = ((this: HTMLImageElement, ev?: Event) => void) | null;
|
||||
type ImageErrorEventHandler =
|
||||
| ((
|
||||
this: HTMLImageElement,
|
||||
ev?: string | Event,
|
||||
source?: string,
|
||||
lineno?: number,
|
||||
colno?: number,
|
||||
error?: Error,
|
||||
) => void)
|
||||
| null;
|
||||
|
||||
let lastOnload: ImageEventHandler = null;
|
||||
let lastOnerror: ImageErrorEventHandler = null;
|
||||
|
||||
vi.stubGlobal(
|
||||
"Image",
|
||||
class {
|
||||
_onload: (() => void) | null = null;
|
||||
_onerror: (() => void) | null = null;
|
||||
_onload: ImageEventHandler = null;
|
||||
_onerror: ImageErrorEventHandler = null;
|
||||
_src: string = "";
|
||||
crossOrigin: string = "";
|
||||
|
||||
set src(val: string) {
|
||||
this._src = val;
|
||||
if (val.includes("error")) {
|
||||
setTimeout(() => this._onerror?.(), 1);
|
||||
setTimeout(
|
||||
() => this._onerror?.call(this as unknown as HTMLImageElement, new Event("error")),
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
setTimeout(() => this._onload?.(), 1);
|
||||
setTimeout(
|
||||
() => this._onload?.call(this as unknown as HTMLImageElement, new Event("load")),
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
get src() {
|
||||
return this._src;
|
||||
}
|
||||
|
||||
set onload(val: any) {
|
||||
set onload(val: ImageEventHandler) {
|
||||
this._onload = val;
|
||||
if (val) lastOnload = val;
|
||||
}
|
||||
@@ -33,7 +51,7 @@ vi.stubGlobal(
|
||||
return this._onload;
|
||||
}
|
||||
|
||||
set onerror(val: any) {
|
||||
set onerror(val: ImageErrorEventHandler) {
|
||||
this._onerror = val;
|
||||
if (val) lastOnerror = val;
|
||||
}
|
||||
@@ -94,7 +112,9 @@ describe("ImageConfigState", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -173,7 +193,9 @@ describe("ImageConfigState", () => {
|
||||
|
||||
imageConfigState.destroy();
|
||||
|
||||
if (lastOnload) lastOnload();
|
||||
if (lastOnload) {
|
||||
lastOnload.call(new Image() as HTMLImageElement, new Event("load"));
|
||||
}
|
||||
|
||||
await expect(promise).rejects.toThrow();
|
||||
expect(imageConfigState.imageReady).toBe(false);
|
||||
@@ -184,7 +206,9 @@ describe("ImageConfigState", () => {
|
||||
|
||||
imageConfigState.destroy();
|
||||
|
||||
if (lastOnerror) lastOnerror();
|
||||
if (lastOnerror) {
|
||||
lastOnerror.call(new Image() as HTMLImageElement, new Event("load"));
|
||||
}
|
||||
|
||||
await expect(promise).rejects.toThrow();
|
||||
expect(imageConfigState.imageReady).toBe(false);
|
||||
|
||||
@@ -1,54 +1,14 @@
|
||||
import { konvaStageState } from "$states/konvaStage.svelte";
|
||||
import type { Stage } from "svelte-konva";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("konvaStage.svelte", () => {
|
||||
describe("initial state", () => {
|
||||
it("should have undefined stage initially", () => {
|
||||
expect(konvaStageState.stage).toBeUndefined();
|
||||
});
|
||||
describe("konvaStageState integration", () => {
|
||||
it("should work", () => {
|
||||
expect(konvaStageState.stage).toBeUndefined();
|
||||
|
||||
it("should have stage getter", () => {
|
||||
expect(konvaStageState).toHaveProperty("stage");
|
||||
});
|
||||
const mock = { name: "stage" } as unknown as Stage;
|
||||
konvaStageState.stage = mock;
|
||||
|
||||
it("should have stage setter", () => {
|
||||
expect(() => {
|
||||
konvaStageState.stage = {} as any;
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("stage getter", () => {
|
||||
it("should return current stage", () => {
|
||||
const mockStage = { id: "test" } as any;
|
||||
konvaStageState.stage = mockStage;
|
||||
expect(konvaStageState.stage).toBeDefined();
|
||||
expect(konvaStageState.stage).toStrictEqual(mockStage);
|
||||
});
|
||||
|
||||
it("should be undefined only at initialization", () => {
|
||||
expect(konvaStageState.stage).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("stage setter", () => {
|
||||
it("should set stage", () => {
|
||||
const mockStage = { id: "test" } as any;
|
||||
konvaStageState.stage = mockStage;
|
||||
|
||||
expect(konvaStageState.stage).toStrictEqual(mockStage);
|
||||
});
|
||||
|
||||
it("should allow updating stage", () => {
|
||||
const firstStage = { id: "first" } as any;
|
||||
const secondStage = { id: "second" } as any;
|
||||
|
||||
konvaStageState.stage = firstStage;
|
||||
expect(konvaStageState.stage).toStrictEqual(firstStage);
|
||||
|
||||
konvaStageState.stage = secondStage;
|
||||
expect(konvaStageState.stage).toStrictEqual(secondStage);
|
||||
expect(konvaStageState.stage).not.toStrictEqual(firstStage);
|
||||
});
|
||||
expect(konvaStageState.stage).toStrictEqual(mock);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,7 +92,10 @@ describe("persisted.svelte", () => {
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -108,7 +111,10 @@ describe("persisted.svelte", () => {
|
||||
|
||||
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 () => {
|
||||
@@ -136,7 +142,10 @@ describe("persisted.svelte", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user