diff --git a/package-lock.json b/package-lock.json index 617de010..3eb6ba3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@vee-validate/zod": "^4.15.1", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", "@vue/test-utils": "^2.4.6", "@vueuse/core": "^14.2.1", "animate.css": "^4.1.1", @@ -70,7 +71,7 @@ "tw-animate-css": "^1.4.0", "vee-validate": "^4.15.1", "vite": "^7.3.1", - "vitest": "^3.2.4", + "vitest": "^4.0.18", "vue": "^3.5.29", "vue-advanced-cropper": "^2.8.9", "vue-i18n": "^11.2.8", @@ -590,6 +591,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -3755,6 +3766,13 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -4381,40 +4399,72 @@ "vue": "^3.0.0" } }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", "dev": true, "license": "MIT", "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -4446,42 +4496,41 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -4499,28 +4548,24 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5160,6 +5205,35 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -5526,16 +5600,6 @@ "node": ">= 10.0.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -5650,18 +5714,11 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -5683,16 +5740,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -6272,16 +6319,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8272,6 +8309,13 @@ ], "license": "MIT" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -8595,6 +8639,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -9234,13 +9317,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -9284,6 +9360,34 @@ "node": ">=12" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-fetch-happen": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", @@ -9886,6 +9990,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -10173,16 +10288,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", @@ -11701,26 +11806,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -11957,11 +12042,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -12011,30 +12099,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -12483,29 +12551,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -12538,51 +12583,50 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, @@ -12590,13 +12634,19 @@ "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { diff --git a/package.json b/package.json index 25221d67..91a0cd0a 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@vee-validate/zod": "^4.15.1", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", "@vue/test-utils": "^2.4.6", "@vueuse/core": "^14.2.1", "animate.css": "^4.1.1", @@ -90,7 +91,7 @@ "tw-animate-css": "^1.4.0", "vee-validate": "^4.15.1", "vite": "^7.3.1", - "vitest": "^3.2.4", + "vitest": "^4.0.18", "vue": "^3.5.29", "vue-advanced-cropper": "^2.8.9", "vue-i18n": "^11.2.8", diff --git a/src/components/ui/data-table/DataTableLayout.vue b/src/components/ui/data-table/DataTableLayout.vue index 7b1b10b7..27197da4 100644 --- a/src/components/ui/data-table/DataTableLayout.vue +++ b/src/components/ui/data-table/DataTableLayout.vue @@ -264,6 +264,13 @@ } from '../pagination'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select'; + import { + getColStyle, + getToggleableColumns, + isReorderable as isReorderableHelper, + isSpacer, + resolveHeaderLabel + } from './dataTableHelpers.js'; import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuTrigger } from '../context-menu'; import DataTableEmpty from './DataTableEmpty.vue'; @@ -396,20 +403,7 @@ return null; }; - const isSpacer = (col) => col?.id === '__spacer'; - - const isStretch = (col) => { - return !!col?.columnDef?.meta?.stretch; - }; - - const isReorderable = (header) => { - const col = header?.column; - if (!col) return false; - if (isSpacer(col)) return false; - if (getPinnedState(col)) return false; - if (col.columnDef?.meta?.disableReorder) return false; - return true; - }; + const isReorderable = (header) => isReorderableHelper(header, getPinnedState); const reorderableIndex = (headers, actualIndex) => { let sortableIdx = 0; @@ -463,33 +457,9 @@ const toggleableColumns = computed(() => { const cols = props.table?.getAllLeafColumns?.() ?? []; - return cols.filter((col) => { - if (isSpacer(col)) return false; - if (isStretch(col)) return false; - if (col.columnDef?.meta?.disableVisibilityToggle) return false; - if (!col.columnDef?.meta?.label) return false; - return true; - }); + return getToggleableColumns(cols); }); - const resolveHeaderLabel = (col) => { - const label = col?.columnDef?.meta?.label; - if (typeof label === 'function') return label(); - return label ?? col?.id ?? ''; - }; - - const getColStyle = (col) => { - if (isSpacer(col)) return { width: '0px' }; - - if (isStretch(col)) { - return null; - } - - const size = col?.getSize?.(); - if (!Number.isFinite(size)) return null; - return { width: `${size}px` }; - }; - const getHeaderClass = (header) => { const columnDef = header?.column?.columnDef; const meta = columnDef?.meta ?? {}; diff --git a/src/components/ui/data-table/__tests__/dataTableHelpers.test.js b/src/components/ui/data-table/__tests__/dataTableHelpers.test.js new file mode 100644 index 00000000..d8d6bb35 --- /dev/null +++ b/src/components/ui/data-table/__tests__/dataTableHelpers.test.js @@ -0,0 +1,184 @@ +import { describe, expect, it } from 'vitest'; + +import { + getColStyle, + getToggleableColumns, + isReorderable, + isSpacer, + isStretch, + resolveHeaderLabel +} from '../dataTableHelpers'; + +// Helper to create a mock TanStack column instance +const mockCol = (id, meta = {}, overrides = {}) => ({ + id, + columnDef: { meta }, + ...overrides +}); + +describe('isSpacer', () => { + it('returns true for __spacer column', () => { + expect(isSpacer({ id: '__spacer' })).toBe(true); + }); + + it('returns false for regular column', () => { + expect(isSpacer({ id: 'name' })).toBe(false); + }); + + it('returns false for null/undefined', () => { + expect(isSpacer(null)).toBe(false); + expect(isSpacer(undefined)).toBe(false); + }); +}); + +describe('isStretch', () => { + it('returns true when meta.stretch is true', () => { + expect(isStretch(mockCol('detail', { stretch: true }))).toBe(true); + }); + + it('returns false when meta.stretch is absent', () => { + expect(isStretch(mockCol('name'))).toBe(false); + }); + + it('returns false for null column', () => { + expect(isStretch(null)).toBe(false); + }); +}); + +describe('resolveHeaderLabel', () => { + it('returns string label from meta', () => { + expect(resolveHeaderLabel(mockCol('name', { label: 'Name' }))).toBe( + 'Name' + ); + }); + + it('calls function label and returns result', () => { + const col = mockCol('name', { label: () => 'Translated Name' }); + expect(resolveHeaderLabel(col)).toBe('Translated Name'); + }); + + it('falls back to column id when no label', () => { + expect(resolveHeaderLabel(mockCol('displayName'))).toBe('displayName'); + }); + + it('returns empty string for null column', () => { + expect(resolveHeaderLabel(null)).toBe(''); + }); + + it('returns empty string for undefined column', () => { + expect(resolveHeaderLabel(undefined)).toBe(''); + }); +}); + +describe('getToggleableColumns', () => { + it('includes columns with meta.label', () => { + const cols = [mockCol('name', { label: 'Name' })]; + expect(getToggleableColumns(cols)).toHaveLength(1); + }); + + it('excludes spacer columns', () => { + const cols = [ + mockCol('name', { label: 'Name' }), + { id: '__spacer', columnDef: { meta: { label: 'Spacer' } } } + ]; + expect(getToggleableColumns(cols)).toHaveLength(1); + expect(getToggleableColumns(cols)[0].id).toBe('name'); + }); + + it('excludes stretch columns', () => { + const cols = [ + mockCol('name', { label: 'Name' }), + mockCol('detail', { stretch: true, label: 'Detail' }) + ]; + expect(getToggleableColumns(cols)).toHaveLength(1); + }); + + it('excludes columns with disableVisibilityToggle', () => { + const cols = [ + mockCol('name', { label: 'Name' }), + mockCol('hidden', { + label: 'Hidden', + disableVisibilityToggle: true + }) + ]; + expect(getToggleableColumns(cols)).toHaveLength(1); + }); + + it('excludes columns without meta.label', () => { + const cols = [ + mockCol('name', { label: 'Name' }), + mockCol('icon'), + mockCol('expand', {}) + ]; + expect(getToggleableColumns(cols)).toHaveLength(1); + }); + + it('returns empty array for non-array input', () => { + expect(getToggleableColumns(null)).toEqual([]); + }); + + it('returns empty array when all columns are excluded', () => { + const cols = [ + { id: '__spacer', columnDef: { meta: {} } }, + mockCol('icon') + ]; + expect(getToggleableColumns(cols)).toEqual([]); + }); +}); + +describe('getColStyle', () => { + it('returns width 0px for spacer column', () => { + expect(getColStyle({ id: '__spacer' })).toEqual({ width: '0px' }); + }); + + it('returns null for stretch column', () => { + expect(getColStyle(mockCol('detail', { stretch: true }))).toBeNull(); + }); + + it('returns width from getSize()', () => { + const col = { ...mockCol('name'), getSize: () => 200 }; + expect(getColStyle(col)).toEqual({ width: '200px' }); + }); + + it('returns null when getSize returns non-finite', () => { + const col = { ...mockCol('name'), getSize: () => NaN }; + expect(getColStyle(col)).toBeNull(); + }); + + it('returns null when getSize is missing', () => { + expect(getColStyle(mockCol('name'))).toBeNull(); + }); +}); + +describe('isReorderable', () => { + const noPinning = () => false; + + it('returns true for normal column', () => { + const header = { column: mockCol('name') }; + expect(isReorderable(header, noPinning)).toBe(true); + }); + + it('returns false for spacer column', () => { + const header = { column: { id: '__spacer', columnDef: { meta: {} } } }; + expect(isReorderable(header, noPinning)).toBe(false); + }); + + it('returns false for pinned column', () => { + const header = { column: mockCol('name') }; + const isPinned = () => true; + expect(isReorderable(header, isPinned)).toBe(false); + }); + + it('returns false for columns with disableReorder', () => { + const header = { column: mockCol('name', { disableReorder: true }) }; + expect(isReorderable(header, noPinning)).toBe(false); + }); + + it('returns false for null header', () => { + expect(isReorderable(null, noPinning)).toBe(false); + }); + + it('returns false for header without column', () => { + expect(isReorderable({}, noPinning)).toBe(false); + }); +}); diff --git a/src/components/ui/data-table/dataTableHelpers.js b/src/components/ui/data-table/dataTableHelpers.js new file mode 100644 index 00000000..be4b27e7 --- /dev/null +++ b/src/components/ui/data-table/dataTableHelpers.js @@ -0,0 +1,77 @@ +/** + * Pure helper functions for DataTableLayout. + * Extracted for testability. + */ + +/** + * @param {object} col - TanStack column instance + * @returns {boolean} + */ +export function isSpacer(col) { + return col?.id === '__spacer'; +} + +/** + * @param {object} col - TanStack column instance + * @returns {boolean} + */ +export function isStretch(col) { + return !!col?.columnDef?.meta?.stretch; +} + +/** + * Resolves a column's display label for the visibility menu. + * Supports both string and function labels (for lazy i18n). + * @param {object} col - TanStack column instance + * @returns {string} + */ +export function resolveHeaderLabel(col) { + const label = col?.columnDef?.meta?.label; + if (typeof label === 'function') return label(); + return label ?? col?.id ?? ''; +} + +/** + * Filters columns to determine which are toggleable in the visibility menu. + * @param {Array} cols - Array of TanStack column instances + * @returns {Array} + */ +export function getToggleableColumns(cols) { + if (!Array.isArray(cols)) return []; + return cols.filter((col) => { + if (isSpacer(col)) return false; + if (isStretch(col)) return false; + if (col.columnDef?.meta?.disableVisibilityToggle) return false; + if (!col.columnDef?.meta?.label) return false; + return true; + }); +} + +/** + * Computes the style object for a column's element. + * @param {object} col - TanStack column instance + * @returns {object|null} + */ +export function getColStyle(col) { + if (isSpacer(col)) return { width: '0px' }; + if (isStretch(col)) return null; + + const size = col?.getSize?.(); + if (!Number.isFinite(size)) return null; + return { width: `${size}px` }; +} + +/** + * Determines if a header can be reordered via drag-and-drop. + * @param {object} header - TanStack header instance + * @param {function} getPinnedState - function to check if column is pinned + * @returns {boolean} + */ +export function isReorderable(header, getPinnedState) { + const col = header?.column; + if (!col) return false; + if (isSpacer(col)) return false; + if (getPinnedState?.(col)) return false; + if (col.columnDef?.meta?.disableReorder) return false; + return true; +} diff --git a/src/lib/table/__tests__/useVrcxVueTable.utils.test.js b/src/lib/table/__tests__/useVrcxVueTable.utils.test.js new file mode 100644 index 00000000..311059b5 --- /dev/null +++ b/src/lib/table/__tests__/useVrcxVueTable.utils.test.js @@ -0,0 +1,234 @@ +import { describe, expect, it } from 'vitest'; + +import { + filterOrderByColumns, + filterSizingByColumns, + filterSortingByColumns, + filterVisibilityByColumns, + findStretchColumnId, + getColumnId, + safeJsonParse, + withSpacerColumn +} from '../useVrcxVueTable'; + +const cols = (...ids) => ids.map((id) => ({ id })); + +describe('safeJsonParse', () => { + it('parses valid JSON', () => { + expect(safeJsonParse('{"a":1}')).toEqual({ a: 1 }); + }); + + it('returns null for invalid JSON', () => { + expect(safeJsonParse('not json')).toBeNull(); + }); + + it('returns null for empty string', () => { + expect(safeJsonParse('')).toBeNull(); + }); + + it('returns null for null/undefined', () => { + expect(safeJsonParse(null)).toBeNull(); + expect(safeJsonParse(undefined)).toBeNull(); + }); +}); + +describe('filterSizingByColumns', () => { + it('keeps only keys matching column IDs', () => { + const sizing = { name: 200, date: 150, removed: 100 }; + expect(filterSizingByColumns(sizing, cols('name', 'date'))).toEqual({ + name: 200, + date: 150 + }); + }); + + it('returns empty object for null sizing', () => { + expect(filterSizingByColumns(null, cols('a'))).toEqual({}); + }); + + it('returns empty object for non-object sizing', () => { + expect(filterSizingByColumns('bad', cols('a'))).toEqual({}); + }); + + it('returns empty object for null columns', () => { + expect(filterSizingByColumns({ a: 1 }, null)).toEqual({}); + }); +}); + +describe('filterSortingByColumns', () => { + it('keeps entries with valid column IDs', () => { + const sorting = [ + { id: 'name', desc: false }, + { id: 'removed', desc: true } + ]; + expect(filterSortingByColumns(sorting, cols('name', 'date'))).toEqual([ + { id: 'name', desc: false } + ]); + }); + + it('returns empty array for non-array input', () => { + expect(filterSortingByColumns(null, cols('a'))).toEqual([]); + expect(filterSortingByColumns('bad', cols('a'))).toEqual([]); + }); + + it('returns empty array for null columns', () => { + expect( + filterSortingByColumns([{ id: 'a', desc: false }], null) + ).toEqual([]); + }); +}); + +describe('filterOrderByColumns', () => { + it('keeps IDs present in columns', () => { + expect( + filterOrderByColumns( + ['date', 'removed', 'name'], + cols('name', 'date') + ) + ).toEqual(['date', 'name']); + }); + + it('returns empty array for non-array input', () => { + expect(filterOrderByColumns(null, cols('a'))).toEqual([]); + expect(filterOrderByColumns({}, cols('a'))).toEqual([]); + }); + + it('returns empty array for null columns', () => { + expect(filterOrderByColumns(['a'], null)).toEqual([]); + }); +}); + +describe('filterVisibilityByColumns', () => { + it('keeps keys matching column IDs', () => { + const vis = { name: false, removed: true, date: false }; + expect(filterVisibilityByColumns(vis, cols('name', 'date'))).toEqual({ + name: false, + date: false + }); + }); + + it('returns empty object for null visibility', () => { + expect(filterVisibilityByColumns(null, cols('a'))).toEqual({}); + }); + + it('returns empty object for non-object visibility', () => { + expect(filterVisibilityByColumns(42, cols('a'))).toEqual({}); + }); + + it('returns empty object for null columns', () => { + expect(filterVisibilityByColumns({ a: true }, null)).toEqual({}); + }); +}); + +describe('getColumnId', () => { + it('returns id when present', () => { + expect(getColumnId({ id: 'foo' })).toBe('foo'); + }); + + it('falls back to accessorKey', () => { + expect(getColumnId({ accessorKey: 'bar' })).toBe('bar'); + }); + + it('prefers id over accessorKey', () => { + expect(getColumnId({ id: 'foo', accessorKey: 'bar' })).toBe('foo'); + }); + + it('returns null for null/undefined', () => { + expect(getColumnId(null)).toBeNull(); + expect(getColumnId(undefined)).toBeNull(); + }); + + it('returns null for empty object', () => { + expect(getColumnId({})).toBeNull(); + }); +}); + +describe('findStretchColumnId', () => { + it('returns the ID of the stretch column', () => { + const columns = [ + { id: 'a' }, + { id: 'b', meta: { stretch: true } }, + { id: 'c' } + ]; + expect(findStretchColumnId(columns)).toBe('b'); + }); + + it('returns first stretch column when multiple exist', () => { + const columns = [ + { id: 'x', meta: { stretch: true } }, + { id: 'y', meta: { stretch: true } } + ]; + expect(findStretchColumnId(columns)).toBe('x'); + }); + + it('returns null when no stretch column exists', () => { + expect(findStretchColumnId([{ id: 'a' }, { id: 'b' }])).toBeNull(); + }); + + it('returns null for non-array input', () => { + expect(findStretchColumnId(null)).toBeNull(); + expect(findStretchColumnId('bad')).toBeNull(); + }); + + it('falls back to accessorKey for stretch column', () => { + const columns = [{ accessorKey: 'detail', meta: { stretch: true } }]; + expect(findStretchColumnId(columns)).toBe('detail'); + }); +}); + +describe('withSpacerColumn', () => { + const baseCols = [{ id: 'a' }, { id: 'b' }]; + + it('appends spacer column at the end when no stretchAfterId', () => { + const result = withSpacerColumn(baseCols, true); + expect(result).toHaveLength(3); + expect(result[2].id).toBe('__spacer'); + }); + + it('uses custom spacerId', () => { + const result = withSpacerColumn(baseCols, true, 'custom_spacer'); + expect(result[2].id).toBe('custom_spacer'); + }); + + it('inserts spacer after stretchAfterId column', () => { + const columns = [{ id: 'x' }, { id: 'stretch' }, { id: 'y' }]; + const result = withSpacerColumn(columns, true, '__spacer', 'stretch'); + expect(result.map((c) => c.id)).toEqual([ + 'x', + 'stretch', + '__spacer', + 'y' + ]); + }); + + it('returns original columns when disabled', () => { + const result = withSpacerColumn(baseCols, false); + expect(result).toBe(baseCols); + }); + + it('does not add duplicate spacer', () => { + const columns = [{ id: 'a' }, { id: '__spacer' }]; + const result = withSpacerColumn(columns, true); + expect(result).toBe(columns); + expect(result).toHaveLength(2); + }); + + it('returns non-array input as-is', () => { + expect(withSpacerColumn(null, true)).toBeNull(); + }); + + it('appends spacer when stretchAfterId not found', () => { + const result = withSpacerColumn(baseCols, true, '__spacer', 'missing'); + expect(result).toHaveLength(3); + expect(result[2].id).toBe('__spacer'); + }); + + it('spacer column has correct defaults', () => { + const result = withSpacerColumn(baseCols, true); + const spacer = result[2]; + expect(spacer.enableSorting).toBe(false); + expect(spacer.size).toBe(0); + expect(spacer.minSize).toBe(0); + expect(spacer.header()).toBeNull(); + expect(spacer.cell()).toBeNull(); + }); +}); diff --git a/src/lib/table/useVrcxVueTable.js b/src/lib/table/useVrcxVueTable.js index 610cb422..243c283b 100644 --- a/src/lib/table/useVrcxVueTable.js +++ b/src/lib/table/useVrcxVueTable.js @@ -13,7 +13,7 @@ import { computed, ref, unref, watch } from 'vue'; * * @param str */ -function safeJsonParse(str) { +export function safeJsonParse(str) { if (!str) { return null; } @@ -44,7 +44,7 @@ function debounce(fn, wait) { * @param sizing * @param columns */ -function filterSizingByColumns(sizing, columns) { +export function filterSizingByColumns(sizing, columns) { if (!sizing || typeof sizing !== 'object') { return {}; } @@ -63,7 +63,7 @@ function filterSizingByColumns(sizing, columns) { * @param sorting * @param columns */ -function filterSortingByColumns(sorting, columns) { +export function filterSortingByColumns(sorting, columns) { if (!Array.isArray(sorting)) { return []; } @@ -76,7 +76,7 @@ function filterSortingByColumns(sorting, columns) { * @param order * @param columns */ -function filterOrderByColumns(order, columns) { +export function filterOrderByColumns(order, columns) { if (!Array.isArray(order)) { return []; } @@ -89,7 +89,7 @@ function filterOrderByColumns(order, columns) { * @param visibility * @param columns */ -function filterVisibilityByColumns(visibility, columns) { +export function filterVisibilityByColumns(visibility, columns) { if (!visibility || typeof visibility !== 'object') { return {}; } @@ -107,7 +107,7 @@ function filterVisibilityByColumns(visibility, columns) { * * @param col */ -function getColumnId(col) { +export function getColumnId(col) { return col?.id ?? col?.accessorKey ?? null; } @@ -115,7 +115,7 @@ function getColumnId(col) { * * @param columns */ -function findStretchColumnId(columns) { +export function findStretchColumnId(columns) { if (!Array.isArray(columns)) { return null; } @@ -153,7 +153,7 @@ function resolveMaybeGetter(func) { * @param spacerId * @param stretchAfterId */ -function withSpacerColumn(columns, enabled, spacerId, stretchAfterId) { +export function withSpacerColumn(columns, enabled, spacerId, stretchAfterId) { if (!enabled) { return columns; } diff --git a/src/shared/utils/base/__tests__/ui.test.js b/src/shared/utils/base/__tests__/ui.test.js new file mode 100644 index 00000000..f3a1bc91 --- /dev/null +++ b/src/shared/utils/base/__tests__/ui.test.js @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { HSVtoRGB } from '../ui'; + +describe('HSVtoRGB', () => { + it('converts pure red (h=0, s=1, v=1)', () => { + expect(HSVtoRGB(0, 1, 1)).toBe('#ff0000'); + }); + + it('converts pure green (h=0.333, s=1, v=1)', () => { + const result = HSVtoRGB(1 / 3, 1, 1); + expect(result).toBe('#00ff00'); + }); + + it('converts pure blue (h=0.667, s=1, v=1)', () => { + const result = HSVtoRGB(2 / 3, 1, 1); + expect(result).toBe('#0000ff'); + }); + + it('converts white (s=0, v=1)', () => { + expect(HSVtoRGB(0, 0, 1)).toBe('#ffffff'); + }); + + it('converts black (v=0)', () => { + expect(HSVtoRGB(0, 1, 0)).toBe('#000000'); + }); + + it('converts yellow (h=1/6, s=1, v=1)', () => { + expect(HSVtoRGB(1 / 6, 1, 1)).toBe('#ffff00'); + }); + + it('converts cyan (h=0.5, s=1, v=1)', () => { + expect(HSVtoRGB(0.5, 1, 1)).toBe('#00ffff'); + }); + + it('handles object argument { h, s, v }', () => { + expect(HSVtoRGB({ h: 0, s: 1, v: 1 })).toBe('#ff0000'); + }); + + it('converts a mid-range value', () => { + const result = HSVtoRGB(0, 0, 0.5); + // gray: rgb(128,128,128) + expect(result).toBe('#808080'); + }); +}); diff --git a/src/views/MyAvatars/columns.jsx b/src/views/MyAvatars/columns.jsx index fa02913c..8f8e620f 100644 --- a/src/views/MyAvatars/columns.jsx +++ b/src/views/MyAvatars/columns.jsx @@ -75,7 +75,7 @@ export function getColumns({ 'h-4 w-4', isActive ? 'text-primary' - : 'text-muted-foreground/0 group-hover/row:text-muted-foreground/40' + : 'text-muted-foreground/0 group-hover/row:text-muted-foreground' ]} /> diff --git a/vitest.setup.js b/vitest.setup.js index 63007e46..080161f0 100644 --- a/vitest.setup.js +++ b/vitest.setup.js @@ -20,6 +20,13 @@ globalThis.LogWatcher = new Proxy({}, { get: () => noopAsync }); globalThis.Discord = new Proxy({}, { get: () => noopAsync }); globalThis.AssetBundleManager = new Proxy({}, { get: () => noopAsync }); +// ResizeObserver polyfill (needed by @dnd-kit/vue at import time) +globalThis.ResizeObserver ??= class { + observe() {} + unobserve() {} + disconnect() {} +}; + // Browser API stubs not available in jsdom globalThis.speechSynthesis = { getVoices: () => [],