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",
|
"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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user