test(frontend): add Vitest unit tests for store helpers and utilities
Wire Vitest into the existing Vite config (scoped to src/**/*.test.ts so it never collides with the Playwright suite in tests/) and add test:unit, test:unit:watch, and test:rust scripts. Covers the pure logic layer: parseSearchValue prefix parsing, all twelve sort orders through mergeImages, merge/dedup/window helpers, filter matching, gallery request tokens, localStorage-backed initial settings, folder-picker path utilities, and the duplicate/lightbox/gallery/video formatters. Fixture factories live in src/test/factories.ts.
This commit is contained in:
+6
-2
@@ -23,7 +23,10 @@
|
|||||||
"format:rust:check": "cd src-tauri && cargo fmt --check",
|
"format:rust:check": "cd src-tauri && cargo fmt --check",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test",
|
||||||
|
"test:rust": "cd src-tauri && cargo test --no-default-features",
|
||||||
|
"test:unit": "vitest run",
|
||||||
|
"test:unit:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-virtual": "^3.13.23",
|
"@tanstack/react-virtual": "^3.13.23",
|
||||||
@@ -53,6 +56,7 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4",
|
||||||
|
"vitest": "^4.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+242
@@ -87,6 +87,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0)
|
version: 7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0)
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.1.9
|
||||||
|
version: 4.1.9(@types/node@26.0.1)(vite@7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0))
|
||||||
|
|
||||||
website:
|
website:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -705,6 +708,9 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.2.2':
|
'@tailwindcss/node@4.2.2':
|
||||||
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
|
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
|
||||||
|
|
||||||
@@ -917,9 +923,15 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
'@types/d3-force@3.0.10':
|
'@types/d3-force@3.0.10':
|
||||||
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
|
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2':
|
||||||
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@@ -940,6 +952,39 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.9':
|
||||||
|
resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.9':
|
||||||
|
resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.9':
|
||||||
|
resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.9':
|
||||||
|
resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.9':
|
||||||
|
resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.9':
|
||||||
|
resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.9':
|
||||||
|
resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.14:
|
baseline-browser-mapping@2.10.14:
|
||||||
resolution: {integrity: sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==}
|
resolution: {integrity: sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -953,6 +998,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001785:
|
caniuse-lite@1.0.30001785:
|
||||||
resolution: {integrity: sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==}
|
resolution: {integrity: sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==}
|
||||||
|
|
||||||
|
chai@6.2.2:
|
||||||
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
@@ -995,6 +1044,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
es-module-lexer@2.3.0:
|
||||||
|
resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==}
|
||||||
|
|
||||||
esbuild@0.27.7:
|
esbuild@0.27.7:
|
||||||
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
|
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1007,6 +1059,13 @@ packages:
|
|||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
|
expect-type@1.4.0:
|
||||||
|
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -1165,6 +1224,13 @@ packages:
|
|||||||
node-releases@2.0.37:
|
node-releases@2.0.37:
|
||||||
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
|
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
|
||||||
|
|
||||||
|
obug@2.1.3:
|
||||||
|
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -1280,10 +1346,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
|
std-env@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||||
|
|
||||||
tailwindcss@4.2.2:
|
tailwindcss@4.2.2:
|
||||||
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
|
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
|
||||||
|
|
||||||
@@ -1291,10 +1366,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
|
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@1.2.4:
|
||||||
|
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0:
|
||||||
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -1358,6 +1444,52 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@4.1.9:
|
||||||
|
resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==}
|
||||||
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
|
'@vitest/browser-playwright': 4.1.9
|
||||||
|
'@vitest/browser-preview': 4.1.9
|
||||||
|
'@vitest/browser-webdriverio': 4.1.9
|
||||||
|
'@vitest/coverage-istanbul': 4.1.9
|
||||||
|
'@vitest/coverage-v8': 4.1.9
|
||||||
|
'@vitest/ui': 4.1.9
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-playwright':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-preview':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-webdriverio':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-istanbul':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@@ -1784,6 +1916,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@tailwindcss/node@4.2.2':
|
'@tailwindcss/node@4.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -1954,8 +2088,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/deep-eql': 4.0.2
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
|
||||||
'@types/d3-force@3.0.10': {}
|
'@types/d3-force@3.0.10': {}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/node@26.0.1':
|
'@types/node@26.0.1':
|
||||||
@@ -1982,6 +2123,49 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 4.1.9
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
chai: 6.2.2
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.9(vite@7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 4.1.9
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0)
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.9
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.9': {}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.9
|
||||||
|
convert-source-map: 2.0.0
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.14: {}
|
baseline-browser-mapping@2.10.14: {}
|
||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
@@ -1994,6 +2178,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001785: {}
|
caniuse-lite@1.0.30001785: {}
|
||||||
|
|
||||||
|
chai@6.2.2: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
@@ -2023,6 +2209,8 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.2
|
tapable: 2.3.2
|
||||||
|
|
||||||
|
es-module-lexer@2.3.0: {}
|
||||||
|
|
||||||
esbuild@0.27.7:
|
esbuild@0.27.7:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.27.7
|
'@esbuild/aix-ppc64': 0.27.7
|
||||||
@@ -2056,6 +2244,12 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
|
||||||
|
expect-type@1.4.0: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -2158,6 +2352,10 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.37: {}
|
node-releases@2.0.37: {}
|
||||||
|
|
||||||
|
obug@2.1.3: {}
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.4: {}
|
||||||
@@ -2259,17 +2457,29 @@ snapshots:
|
|||||||
'@img/sharp-win32-ia32': 0.34.5
|
'@img/sharp-win32-ia32': 0.34.5
|
||||||
'@img/sharp-win32-x64': 0.34.5
|
'@img/sharp-win32-x64': 0.34.5
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
|
std-env@4.1.0: {}
|
||||||
|
|
||||||
tailwindcss@4.2.2: {}
|
tailwindcss@4.2.2: {}
|
||||||
|
|
||||||
tapable@2.3.2: {}
|
tapable@2.3.2: {}
|
||||||
|
|
||||||
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@1.2.4: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
typescript@5.8.3: {}
|
typescript@5.8.3: {}
|
||||||
@@ -2305,6 +2515,38 @@ snapshots:
|
|||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
|
|
||||||
|
vitest@4.1.9(@types/node@26.0.1)(vite@7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0)):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 4.1.9
|
||||||
|
'@vitest/mocker': 4.1.9(vite@7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0))
|
||||||
|
'@vitest/pretty-format': 4.1.9
|
||||||
|
'@vitest/runner': 4.1.9
|
||||||
|
'@vitest/snapshot': 4.1.9
|
||||||
|
'@vitest/spy': 4.1.9
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
es-module-lexer: 2.3.0
|
||||||
|
expect-type: 1.4.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.3
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
std-env: 4.1.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.2.4
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
vite: 7.3.1(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.32.0)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 26.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- msw
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4):
|
zustand@5.0.12(@types/react@19.2.14)(react@19.2.4):
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import type { DuplicateScanProgress } from '../../store'
|
||||||
|
import {
|
||||||
|
duplicateProgressLabel,
|
||||||
|
duplicateProgressPercent,
|
||||||
|
formatBytes,
|
||||||
|
formatRelativeTime,
|
||||||
|
} from './format'
|
||||||
|
|
||||||
|
function progress(overrides: Partial<DuplicateScanProgress> = {}): DuplicateScanProgress {
|
||||||
|
return { phase: 'checking', processed: 0, total: 0, skipped: 0, ...overrides }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('formatBytes', () => {
|
||||||
|
it('formats each size tier', () => {
|
||||||
|
expect(formatBytes(0)).toBe('0 B')
|
||||||
|
expect(formatBytes(512)).toBe('512 B')
|
||||||
|
expect(formatBytes(2048)).toBe('2 KB')
|
||||||
|
expect(formatBytes(1_048_576)).toBe('1.0 MB')
|
||||||
|
expect(formatBytes(1_610_612_736)).toBe('1.5 GB')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatRelativeTime', () => {
|
||||||
|
const now = () => Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
it('formats each time bucket', () => {
|
||||||
|
expect(formatRelativeTime(now())).toBe('just now')
|
||||||
|
expect(formatRelativeTime(now() - 120)).toBe('2m ago')
|
||||||
|
expect(formatRelativeTime(now() - 7200)).toBe('2h ago')
|
||||||
|
expect(formatRelativeTime(now() - 172_800)).toBe('2d ago')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('duplicateProgressLabel', () => {
|
||||||
|
it('returns null without progress', () => {
|
||||||
|
expect(duplicateProgressLabel(null)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('labels each phase', () => {
|
||||||
|
expect(duplicateProgressLabel(progress({ phase: 'checking' }))).toBe('Checking file sizes')
|
||||||
|
expect(duplicateProgressLabel(progress({ phase: 'hashing' }))).toBe(
|
||||||
|
'Hashing duplicate candidates'
|
||||||
|
)
|
||||||
|
expect(duplicateProgressLabel(progress({ phase: 'confirming' }))).toBe(
|
||||||
|
'Confirming exact matches'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('duplicateProgressPercent', () => {
|
||||||
|
it('returns 0 for missing progress or zero totals', () => {
|
||||||
|
expect(duplicateProgressPercent(null)).toBe(0)
|
||||||
|
expect(duplicateProgressPercent(progress({ processed: 5, total: 0 }))).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rounds to whole percent', () => {
|
||||||
|
expect(duplicateProgressPercent(progress({ processed: 50, total: 200 }))).toBe(25)
|
||||||
|
expect(duplicateProgressPercent(progress({ processed: 1, total: 3 }))).toBe(33)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
buildBreadcrumbs,
|
||||||
|
cleanAddressInput,
|
||||||
|
folderName,
|
||||||
|
friendlyDirectoryError,
|
||||||
|
normalizePath,
|
||||||
|
} from './pathUtils'
|
||||||
|
|
||||||
|
describe('normalizePath', () => {
|
||||||
|
it('converts backslashes, strips trailing slashes, and lowercases', () => {
|
||||||
|
expect(normalizePath('C:\\Users\\Me\\Pictures\\')).toBe('c:/users/me/pictures')
|
||||||
|
expect(normalizePath('/home/User/photos///')).toBe('/home/user/photos')
|
||||||
|
expect(normalizePath('relative/path')).toBe('relative/path')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('cleanAddressInput', () => {
|
||||||
|
it('strips matching surrounding quotes', () => {
|
||||||
|
expect(cleanAddressInput('"C:\\Photos"')).toBe('C:\\Photos')
|
||||||
|
expect(cleanAddressInput("'C:\\Photos'")).toBe('C:\\Photos')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves mismatched quotes alone', () => {
|
||||||
|
expect(cleanAddressInput('"C:\\Photos\'')).toBe('"C:\\Photos\'')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('trims whitespace inside and outside quotes', () => {
|
||||||
|
expect(cleanAddressInput(' " C:\\Photos " ')).toBe('C:\\Photos')
|
||||||
|
expect(cleanAddressInput(' C:\\Photos ')).toBe('C:\\Photos')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('friendlyDirectoryError', () => {
|
||||||
|
it('maps not-found style errors to a friendly message', () => {
|
||||||
|
expect(friendlyDirectoryError(new Error('The system cannot find the path specified.'))).toBe(
|
||||||
|
'Folder not found. Check the path and try again.'
|
||||||
|
)
|
||||||
|
expect(friendlyDirectoryError(new Error('os error 3'))).toBe(
|
||||||
|
'Folder not found. Check the path and try again.'
|
||||||
|
)
|
||||||
|
expect(friendlyDirectoryError('No such file or directory')).toBe(
|
||||||
|
'Folder not found. Check the path and try again.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes other messages through', () => {
|
||||||
|
expect(friendlyDirectoryError(new Error('Access denied'))).toBe('Access denied')
|
||||||
|
expect(friendlyDirectoryError(42)).toBe('42')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('folderName', () => {
|
||||||
|
it('returns the final path component', () => {
|
||||||
|
expect(folderName('C:\\Users\\me\\Pictures')).toBe('Pictures')
|
||||||
|
expect(folderName('/home/user/photos/')).toBe('photos')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles roots', () => {
|
||||||
|
expect(folderName('C:\\')).toBe('C:')
|
||||||
|
expect(folderName('/')).toBe('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildBreadcrumbs', () => {
|
||||||
|
it('returns the home crumb for null paths', () => {
|
||||||
|
expect(buildBreadcrumbs(null)).toEqual([{ label: 'This PC / Home', path: null }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds Windows drive breadcrumbs', () => {
|
||||||
|
expect(buildBreadcrumbs('C:\\Users\\me')).toEqual([
|
||||||
|
{ label: 'This PC', path: null },
|
||||||
|
{ label: 'C:', path: 'C:' },
|
||||||
|
{ label: 'Users', path: 'C:\\Users' },
|
||||||
|
{ label: 'me', path: 'C:\\Users\\me' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds Unix breadcrumbs', () => {
|
||||||
|
expect(buildBreadcrumbs('/home/user')).toEqual([
|
||||||
|
{ label: '/', path: null },
|
||||||
|
{ label: 'home', path: '/home' },
|
||||||
|
{ label: 'user', path: '/home/user' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores trailing separators', () => {
|
||||||
|
const crumbs = buildBreadcrumbs('C:\\Users\\')
|
||||||
|
expect(crumbs.map((c) => c.label)).toEqual(['This PC', 'C:', 'Users'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { formatDuration } from './format'
|
||||||
|
|
||||||
|
describe('formatDuration', () => {
|
||||||
|
it('returns null for missing or non-positive durations', () => {
|
||||||
|
expect(formatDuration(null)).toBeNull()
|
||||||
|
expect(formatDuration(0)).toBeNull()
|
||||||
|
expect(formatDuration(-100)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats sub-hour durations as M:SS', () => {
|
||||||
|
expect(formatDuration(1000)).toBe('0:01')
|
||||||
|
expect(formatDuration(59_999)).toBe('0:59')
|
||||||
|
expect(formatDuration(65_000)).toBe('1:05')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats hour-plus durations as H:MM:SS', () => {
|
||||||
|
expect(formatDuration(3_600_000)).toBe('1:00:00')
|
||||||
|
expect(formatDuration(3_661_000)).toBe('1:01:01')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { embeddingLabel, formatBytes, formatDate, formatDuration, ratingPill } from './format'
|
||||||
|
|
||||||
|
describe('formatBytes', () => {
|
||||||
|
it('formats each size tier', () => {
|
||||||
|
expect(formatBytes(512)).toBe('512 B')
|
||||||
|
expect(formatBytes(2048)).toBe('2.0 KB')
|
||||||
|
expect(formatBytes(5 * 1024 * 1024)).toBe('5.0 MB')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatDate', () => {
|
||||||
|
it('returns Unknown for null', () => {
|
||||||
|
expect(formatDate(null)).toBe('Unknown')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats ISO dates with a full year', () => {
|
||||||
|
// Exact output is locale-dependent; assert the stable parts.
|
||||||
|
const formatted = formatDate('2024-03-15T12:00:00Z')
|
||||||
|
expect(formatted).toContain('2024')
|
||||||
|
expect(formatted).not.toBe('Unknown')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('formatDuration', () => {
|
||||||
|
it('reports pending for missing or zero durations', () => {
|
||||||
|
expect(formatDuration(null)).toBe('Pending / unavailable')
|
||||||
|
expect(formatDuration(0)).toBe('Pending / unavailable')
|
||||||
|
expect(formatDuration(-5)).toBe('Pending / unavailable')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats minutes and hours', () => {
|
||||||
|
expect(formatDuration(65_000)).toBe('1:05')
|
||||||
|
expect(formatDuration(3_661_000)).toBe('1:01:01')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('embeddingLabel', () => {
|
||||||
|
it('labels each status', () => {
|
||||||
|
expect(embeddingLabel('ready', 'clip-vit')).toBe('Ready (clip-vit)')
|
||||||
|
expect(embeddingLabel('ready', null)).toBe('Ready')
|
||||||
|
expect(embeddingLabel('failed', null)).toBe('Failed')
|
||||||
|
expect(embeddingLabel('processing', null)).toBe('Processing')
|
||||||
|
expect(embeddingLabel('pending', null)).toBe('Queued')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ratingPill', () => {
|
||||||
|
it('maps each AI rating to a label and tone', () => {
|
||||||
|
expect(ratingPill('general').label).toBe('General')
|
||||||
|
expect(ratingPill('general').className).toContain('emerald')
|
||||||
|
expect(ratingPill('sensitive').label).toBe('Sensitive')
|
||||||
|
expect(ratingPill('sensitive').className).toContain('sky')
|
||||||
|
expect(ratingPill('questionable').label).toBe('Questionable')
|
||||||
|
expect(ratingPill('questionable').className).toContain('amber')
|
||||||
|
expect(ratingPill('explicit').label).toBe('Explicit')
|
||||||
|
expect(ratingPill('explicit').className).toContain('red')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { formatTime } from './format'
|
||||||
|
|
||||||
|
describe('formatTime', () => {
|
||||||
|
it('returns 0:00 for invalid input', () => {
|
||||||
|
expect(formatTime(Number.NaN)).toBe('0:00')
|
||||||
|
expect(formatTime(Number.POSITIVE_INFINITY)).toBe('0:00')
|
||||||
|
expect(formatTime(-5)).toBe('0:00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats sub-hour times as M:SS', () => {
|
||||||
|
expect(formatTime(0)).toBe('0:00')
|
||||||
|
expect(formatTime(59)).toBe('0:59')
|
||||||
|
expect(formatTime(61.7)).toBe('1:01')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats hour-plus times as H:MM:SS', () => {
|
||||||
|
expect(formatTime(3600)).toBe('1:00:00')
|
||||||
|
expect(formatTime(3661)).toBe('1:01:01')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { makeFolderProgress, makeImage } from '../test/factories'
|
||||||
|
import {
|
||||||
|
countNewImages,
|
||||||
|
imagesAffectScope,
|
||||||
|
initialAiCaptionsEnabled,
|
||||||
|
initialBoolSetting,
|
||||||
|
initialNumberSetting,
|
||||||
|
isCurrentGalleryRequest,
|
||||||
|
matchesFilters,
|
||||||
|
matchesSearch,
|
||||||
|
mergeImages,
|
||||||
|
mergeIntoVisibleWindow,
|
||||||
|
nextGalleryRequestToken,
|
||||||
|
parseSearchValue,
|
||||||
|
replaceExistingImages,
|
||||||
|
replaceImage,
|
||||||
|
scopeHasTaggingPending,
|
||||||
|
searchModeLabel,
|
||||||
|
taggingProgressAffectsScope,
|
||||||
|
tileSizeForZoom,
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
describe('parseSearchValue', () => {
|
||||||
|
it('returns empty filename search for blank input', () => {
|
||||||
|
expect(parseSearchValue('')).toEqual({ mode: 'filename', query: '', prefix: null })
|
||||||
|
expect(parseSearchValue(' ')).toEqual({ mode: 'filename', query: '', prefix: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats plain text as filename search', () => {
|
||||||
|
expect(parseSearchValue('sunset')).toEqual({ mode: 'filename', query: 'sunset', prefix: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses slash prefixes', () => {
|
||||||
|
expect(parseSearchValue('/s beach')).toEqual({ mode: 'semantic', query: 'beach', prefix: '/s' })
|
||||||
|
expect(parseSearchValue('/t cat')).toEqual({ mode: 'tag', query: 'cat', prefix: '/t' })
|
||||||
|
expect(parseSearchValue('/f holiday')).toEqual({
|
||||||
|
mode: 'filename',
|
||||||
|
query: 'holiday',
|
||||||
|
prefix: '/f',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses a bare slash prefix with no query yet', () => {
|
||||||
|
expect(parseSearchValue('/s')).toEqual({ mode: 'semantic', query: '', prefix: '/s' })
|
||||||
|
expect(parseSearchValue('/t')).toEqual({ mode: 'tag', query: '', prefix: '/t' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is case-insensitive on slash prefixes', () => {
|
||||||
|
expect(parseSearchValue('/S beach')).toEqual({ mode: 'semantic', query: 'beach', prefix: '/s' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps multi-word queries intact', () => {
|
||||||
|
expect(parseSearchValue('/s beach at sunset').query).toBe('beach at sunset')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to filename for unknown slash prefixes', () => {
|
||||||
|
expect(parseSearchValue('/x foo')).toEqual({ mode: 'filename', query: 'foo', prefix: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses colon prefixes', () => {
|
||||||
|
expect(parseSearchValue('s: beach')).toEqual({ mode: 'semantic', query: 'beach', prefix: 's:' })
|
||||||
|
expect(parseSearchValue('t:cat')).toEqual({ mode: 'tag', query: 'cat', prefix: 't:' })
|
||||||
|
expect(parseSearchValue('f: holiday')).toEqual({
|
||||||
|
mode: 'filename',
|
||||||
|
query: 'holiday',
|
||||||
|
prefix: 'f:',
|
||||||
|
})
|
||||||
|
expect(parseSearchValue('S: beach')).toEqual({ mode: 'semantic', query: 'beach', prefix: 's:' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to filename for unknown colon prefixes', () => {
|
||||||
|
expect(parseSearchValue('x: foo')).toEqual({ mode: 'filename', query: 'foo', prefix: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not treat multi-letter colon words as prefixes', () => {
|
||||||
|
expect(parseSearchValue('note: hello')).toEqual({
|
||||||
|
mode: 'filename',
|
||||||
|
query: 'note: hello',
|
||||||
|
prefix: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searchModeLabel / tileSizeForZoom', () => {
|
||||||
|
it('labels every search mode', () => {
|
||||||
|
expect(searchModeLabel('semantic')).toBe('Semantic Search')
|
||||||
|
expect(searchModeLabel('tag')).toBe('Tag Search')
|
||||||
|
expect(searchModeLabel('filename')).toBe('Filename Search')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maps zoom presets to tile sizes', () => {
|
||||||
|
expect(tileSizeForZoom('compact')).toBe(160)
|
||||||
|
expect(tileSizeForZoom('comfortable')).toBe(220)
|
||||||
|
expect(tileSizeForZoom('detail')).toBe(280)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('matchesSearch', () => {
|
||||||
|
it('matches any image when search is empty', () => {
|
||||||
|
expect(matchesSearch(makeImage(), '')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches filename substrings case-insensitively', () => {
|
||||||
|
const image = makeImage({ filename: 'Beach_Sunset.JPG' })
|
||||||
|
expect(matchesSearch(image, 'sunset')).toBe(true)
|
||||||
|
expect(matchesSearch(image, 'SUNSET')).toBe(true)
|
||||||
|
expect(matchesSearch(image, 'mountain')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('matchesFilters', () => {
|
||||||
|
const pass = (image = makeImage()) =>
|
||||||
|
matchesFilters(image, null, 'all', false, 0, false, false, '')
|
||||||
|
|
||||||
|
it('passes with no filters active', () => {
|
||||||
|
expect(pass()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters by folder', () => {
|
||||||
|
const image = makeImage({ folder_id: 2 })
|
||||||
|
expect(matchesFilters(image, 2, 'all', false, 0, false, false, '')).toBe(true)
|
||||||
|
expect(matchesFilters(image, 3, 'all', false, 0, false, false, '')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters by media kind', () => {
|
||||||
|
const video = makeImage({ media_kind: 'video' })
|
||||||
|
expect(matchesFilters(video, null, 'video', false, 0, false, false, '')).toBe(true)
|
||||||
|
expect(matchesFilters(video, null, 'image', false, 0, false, false, '')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters favorites and minimum rating', () => {
|
||||||
|
const image = makeImage({ favorite: false, rating: 2 })
|
||||||
|
expect(matchesFilters(image, null, 'all', true, 0, false, false, '')).toBe(false)
|
||||||
|
expect(matchesFilters(image, null, 'all', false, 3, false, false, '')).toBe(false)
|
||||||
|
expect(matchesFilters(image, null, 'all', false, 2, false, false, '')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters failed embeddings and failed tagging', () => {
|
||||||
|
const healthy = makeImage({ embedding_status: 'ready', ai_tagger_error: null })
|
||||||
|
const broken = makeImage({ embedding_status: 'failed', ai_tagger_error: 'boom' })
|
||||||
|
expect(matchesFilters(healthy, null, 'all', false, 0, true, false, '')).toBe(false)
|
||||||
|
expect(matchesFilters(broken, null, 'all', false, 0, true, false, '')).toBe(true)
|
||||||
|
expect(matchesFilters(healthy, null, 'all', false, 0, false, true, '')).toBe(false)
|
||||||
|
expect(matchesFilters(broken, null, 'all', false, 0, false, true, '')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies the search term', () => {
|
||||||
|
const image = makeImage({ filename: 'cat.jpg' })
|
||||||
|
expect(matchesFilters(image, null, 'all', false, 0, false, false, 'cat')).toBe(true)
|
||||||
|
expect(matchesFilters(image, null, 'all', false, 0, false, false, 'dog')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mergeImages', () => {
|
||||||
|
it('deduplicates by path, letting new records win', () => {
|
||||||
|
const stale = makeImage({ path: 'C:/media/a.jpg', filename: 'a.jpg', rating: 0 })
|
||||||
|
const fresh = makeImage({ path: 'C:/media/a.jpg', filename: 'a.jpg', rating: 5 })
|
||||||
|
const merged = mergeImages([stale], [fresh], 'name_asc')
|
||||||
|
expect(merged).toHaveLength(1)
|
||||||
|
expect(merged[0].rating).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sorts by name in both directions', () => {
|
||||||
|
const a = makeImage({ path: 'a', filename: 'apple.jpg' })
|
||||||
|
const b = makeImage({ path: 'b', filename: 'banana.jpg' })
|
||||||
|
expect(mergeImages([b], [a], 'name_asc').map((i) => i.filename)).toEqual([
|
||||||
|
'apple.jpg',
|
||||||
|
'banana.jpg',
|
||||||
|
])
|
||||||
|
expect(mergeImages([b], [a], 'name_desc').map((i) => i.filename)).toEqual([
|
||||||
|
'banana.jpg',
|
||||||
|
'apple.jpg',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sorts by modified date, treating null as the epoch', () => {
|
||||||
|
const older = makeImage({ path: 'a', modified_at: '2025-01-01T00:00:00Z' })
|
||||||
|
const newer = makeImage({ path: 'b', modified_at: '2026-01-01T00:00:00Z' })
|
||||||
|
const undated = makeImage({ path: 'c', modified_at: null })
|
||||||
|
const asc = mergeImages([newer, undated], [older], 'date_asc')
|
||||||
|
expect(asc.map((i) => i.path)).toEqual(['c', 'a', 'b'])
|
||||||
|
const desc = mergeImages([newer, undated], [older], 'date_desc')
|
||||||
|
expect(desc.map((i) => i.path)).toEqual(['b', 'a', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sorts by size, rating, and duration', () => {
|
||||||
|
const small = makeImage({ path: 'a', file_size: 10, rating: 1, duration_ms: 100 })
|
||||||
|
const large = makeImage({ path: 'b', file_size: 20, rating: 3, duration_ms: null })
|
||||||
|
expect(mergeImages([large], [small], 'size_asc')[0].path).toBe('a')
|
||||||
|
expect(mergeImages([large], [small], 'size_desc')[0].path).toBe('b')
|
||||||
|
expect(mergeImages([large], [small], 'rating_asc')[0].path).toBe('a')
|
||||||
|
expect(mergeImages([large], [small], 'rating_desc')[0].path).toBe('b')
|
||||||
|
// null duration sorts as 0
|
||||||
|
expect(mergeImages([large], [small], 'duration_asc')[0].path).toBe('b')
|
||||||
|
expect(mergeImages([large], [small], 'duration_desc')[0].path).toBe('a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to modified_at when taken_at is missing', () => {
|
||||||
|
const taken = makeImage({
|
||||||
|
path: 'a',
|
||||||
|
taken_at: '2024-01-01T00:00:00Z',
|
||||||
|
modified_at: '2026-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
const untaken = makeImage({
|
||||||
|
path: 'b',
|
||||||
|
taken_at: null,
|
||||||
|
modified_at: '2025-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
expect(mergeImages([taken], [untaken], 'taken_asc').map((i) => i.path)).toEqual(['a', 'b'])
|
||||||
|
expect(mergeImages([taken], [untaken], 'taken_desc').map((i) => i.path)).toEqual(['b', 'a'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mergeIntoVisibleWindow', () => {
|
||||||
|
it('limits the merged list to the window size', () => {
|
||||||
|
const images = [1, 2, 3, 4].map((n) => makeImage({ path: `p${n}`, filename: `${n}.jpg` }))
|
||||||
|
const windowed = mergeIntoVisibleWindow(images.slice(0, 2), images.slice(2), 'name_asc', 3)
|
||||||
|
expect(windowed).toHaveLength(3)
|
||||||
|
expect(windowed.map((i) => i.filename)).toEqual(['1.jpg', '2.jpg', '3.jpg'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps negative window sizes to zero', () => {
|
||||||
|
expect(mergeIntoVisibleWindow([makeImage()], [], 'name_asc', -1)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('countNewImages', () => {
|
||||||
|
it('counts only paths not already present', () => {
|
||||||
|
const current = [makeImage({ path: 'a' })]
|
||||||
|
const incoming = [makeImage({ path: 'a' }), makeImage({ path: 'b' }), makeImage({ path: 'c' })]
|
||||||
|
expect(countNewImages(current, incoming)).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('counts duplicate new paths once', () => {
|
||||||
|
const incoming = [makeImage({ path: 'b' }), makeImage({ path: 'b' })]
|
||||||
|
expect(countNewImages([], incoming)).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('replaceImage / replaceExistingImages', () => {
|
||||||
|
it('replaceImage swaps the matching record and re-sorts', () => {
|
||||||
|
const a = makeImage({ path: 'a', filename: 'a.jpg' })
|
||||||
|
const b = makeImage({ path: 'b', filename: 'b.jpg' })
|
||||||
|
const renamed = makeImage({ path: 'a', filename: 'z.jpg' })
|
||||||
|
const result = replaceImage([a, b], renamed, 'name_asc')
|
||||||
|
expect(result.map((i) => i.filename)).toEqual(['b.jpg', 'z.jpg'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaceExistingImages swaps in place without re-sorting', () => {
|
||||||
|
const a = makeImage({ path: 'a', rating: 0 })
|
||||||
|
const b = makeImage({ path: 'b', rating: 0 })
|
||||||
|
const updated = makeImage({ path: 'b', rating: 5 })
|
||||||
|
const result = replaceExistingImages([b, a], [updated])
|
||||||
|
expect(result.map((i) => i.path)).toEqual(['b', 'a'])
|
||||||
|
expect(result[0].rating).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaceExistingImages ignores updates for unknown paths and returns the same array', () => {
|
||||||
|
const current = [makeImage({ path: 'a' })]
|
||||||
|
const result = replaceExistingImages(current, [makeImage({ path: 'zzz' })])
|
||||||
|
expect(result).toBe(current)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('localStorage-backed initial settings', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
function stubLocalStorage(entries: Record<string, string>) {
|
||||||
|
const store = new Map(Object.entries(entries))
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
localStorage: {
|
||||||
|
getItem: (key: string) => store.get(key) ?? null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('returns the fallback when window is undefined', () => {
|
||||||
|
expect(initialBoolSetting('missing', true)).toBe(true)
|
||||||
|
expect(initialNumberSetting('missing', 7, 0, 10)).toBe(7)
|
||||||
|
expect(initialAiCaptionsEnabled('missing')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initialAiCaptionsEnabled only enables on the literal string "true"', () => {
|
||||||
|
stubLocalStorage({ on: 'true', off: 'yes' })
|
||||||
|
expect(initialAiCaptionsEnabled('on')).toBe(true)
|
||||||
|
expect(initialAiCaptionsEnabled('off')).toBe(false)
|
||||||
|
expect(initialAiCaptionsEnabled('absent')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads booleans from storage', () => {
|
||||||
|
stubLocalStorage({ on: 'true', off: 'false' })
|
||||||
|
expect(initialBoolSetting('on', false)).toBe(true)
|
||||||
|
expect(initialBoolSetting('off', true)).toBe(false)
|
||||||
|
expect(initialBoolSetting('absent', true)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps stored numbers to the allowed range', () => {
|
||||||
|
stubLocalStorage({ low: '-5', high: '999', ok: '4', junk: 'abc' })
|
||||||
|
expect(initialNumberSetting('low', 5, 0, 10)).toBe(0)
|
||||||
|
expect(initialNumberSetting('high', 5, 0, 10)).toBe(10)
|
||||||
|
expect(initialNumberSetting('ok', 5, 0, 10)).toBe(4)
|
||||||
|
expect(initialNumberSetting('junk', 5, 0, 10)).toBe(5)
|
||||||
|
expect(initialNumberSetting('absent', 5, 0, 10)).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('gallery request tokens', () => {
|
||||||
|
it('only the most recent token is current', () => {
|
||||||
|
const first = nextGalleryRequestToken()
|
||||||
|
expect(isCurrentGalleryRequest(first)).toBe(true)
|
||||||
|
const second = nextGalleryRequestToken()
|
||||||
|
expect(second).toBeGreaterThan(first)
|
||||||
|
expect(isCurrentGalleryRequest(first)).toBe(false)
|
||||||
|
expect(isCurrentGalleryRequest(second)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tagging scope helpers', () => {
|
||||||
|
it('scopeHasTaggingPending checks a single folder scope', () => {
|
||||||
|
const progress = { 1: makeFolderProgress({ folder_id: 1, tagging_pending: 3 }) }
|
||||||
|
expect(scopeHasTaggingPending(progress, 1)).toBe(true)
|
||||||
|
expect(scopeHasTaggingPending(progress, 2)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('scopeHasTaggingPending checks all folders when scope is null', () => {
|
||||||
|
const progress = {
|
||||||
|
1: makeFolderProgress({ folder_id: 1, tagging_pending: 0 }),
|
||||||
|
2: makeFolderProgress({ folder_id: 2, tagging_pending: 1 }),
|
||||||
|
}
|
||||||
|
expect(scopeHasTaggingPending(progress, null)).toBe(true)
|
||||||
|
expect(scopeHasTaggingPending({}, null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('taggingProgressAffectsScope matches null or same-folder scopes', () => {
|
||||||
|
expect(taggingProgressAffectsScope(1, null)).toBe(true)
|
||||||
|
expect(taggingProgressAffectsScope(1, 1)).toBe(true)
|
||||||
|
expect(taggingProgressAffectsScope(1, 2)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('imagesAffectScope checks folder membership', () => {
|
||||||
|
const images = [makeImage({ folder_id: 1 }), makeImage({ folder_id: 2 })]
|
||||||
|
expect(imagesAffectScope(images, null)).toBe(true)
|
||||||
|
expect(imagesAffectScope(images, 2)).toBe(true)
|
||||||
|
expect(imagesAffectScope(images, 3)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Folder, FolderJobProgress, ImageRecord } from '../store/types'
|
||||||
|
|
||||||
|
let nextImageId = 1
|
||||||
|
|
||||||
|
export function makeImage(overrides: Partial<ImageRecord> = {}): ImageRecord {
|
||||||
|
const id = overrides.id ?? nextImageId++
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
folder_id: 1,
|
||||||
|
path: `C:/media/image-${id}.jpg`,
|
||||||
|
filename: `image-${id}.jpg`,
|
||||||
|
thumbnail_path: null,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
file_size: 1024,
|
||||||
|
created_at: null,
|
||||||
|
modified_at: '2026-01-01T00:00:00Z',
|
||||||
|
taken_at: null,
|
||||||
|
mime_type: 'image/jpeg',
|
||||||
|
media_kind: 'image',
|
||||||
|
duration_ms: null,
|
||||||
|
video_codec: null,
|
||||||
|
audio_codec: null,
|
||||||
|
metadata_updated_at: null,
|
||||||
|
metadata_error: null,
|
||||||
|
favorite: false,
|
||||||
|
rating: 0,
|
||||||
|
embedding_status: 'pending',
|
||||||
|
embedding_model: null,
|
||||||
|
embedding_updated_at: null,
|
||||||
|
embedding_error: null,
|
||||||
|
generated_caption: null,
|
||||||
|
caption_model: null,
|
||||||
|
caption_updated_at: null,
|
||||||
|
caption_error: null,
|
||||||
|
ai_rating: null,
|
||||||
|
ai_tagger_model: null,
|
||||||
|
ai_tagged_at: null,
|
||||||
|
ai_tagger_error: null,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeFolder(overrides: Partial<Folder> = {}): Folder {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
path: 'C:/media',
|
||||||
|
name: 'media',
|
||||||
|
image_count: 0,
|
||||||
|
indexed_at: null,
|
||||||
|
scan_error: null,
|
||||||
|
sort_order: 0,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeFolderProgress(overrides: Partial<FolderJobProgress> = {}): FolderJobProgress {
|
||||||
|
return {
|
||||||
|
folder_id: 1,
|
||||||
|
thumbnail_pending: 0,
|
||||||
|
metadata_pending: 0,
|
||||||
|
embedding_pending: 0,
|
||||||
|
embedding_ready: 0,
|
||||||
|
embedding_failed: 0,
|
||||||
|
caption_pending: 0,
|
||||||
|
caption_ready: 0,
|
||||||
|
caption_failed: 0,
|
||||||
|
tagging_pending: 0,
|
||||||
|
tagging_ready: 0,
|
||||||
|
tagging_failed: 0,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// <reference types="vitest/config" />
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
@@ -9,6 +10,11 @@ const host = process.env.TAURI_DEV_HOST
|
|||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
|
|||||||
Reference in New Issue
Block a user