diff --git a/package-lock.json b/package-lock.json index e862980..2b173ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,17 +12,29 @@ "vue-i18n": "^9.14.5" }, "devDependencies": { + "-": "^0.0.1", "@icon-park/vue-next": "^1.4.2", "@vitejs/plugin-vue": "^6.0.6", + "@vitest/ui": "^4.1.4", + "@vue/test-utils": "^2.4.6", "concurrently": "^8.2.2", "electron": "^28.0.0", "electron-builder": "^24.13.3", + "happy-dom": "^20.9.0", "less": "^4.6.4", "less-loader": "^12.3.2", "vite": "^8.0.8", + "vitest": "^4.1.4", "vue": "^3.4.0" } }, + "node_modules/-": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/-/-/--0.0.1.tgz", + "integrity": "sha512-3HfneK3DGAm05fpyj20sT3apkNcvPpCuccOThOPdzz8sY7GgQGe0l93XH9bt+YzibcTIgUAIMoyVJI740RtgyQ==", + "dev": true, + "license": "UNLICENSED" + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -668,6 +680,13 @@ "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-project/types": { "version": "0.124.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", @@ -689,6 +708,13 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", @@ -966,6 +992,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/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -1013,6 +1046,17 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1023,6 +1067,20 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -1097,6 +1155,23 @@ "license": "MIT", "optional": true }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -1125,6 +1200,151 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/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/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.4.tgz", + "integrity": "sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "fflate": "^0.8.2", + "flatted": "^3.4.2", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.4" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.32", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", @@ -1231,6 +1451,17 @@ "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.12", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", @@ -1248,6 +1479,16 @@ "dev": true, "license": "MIT" }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1512,6 +1753,16 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1809,6 +2060,16 @@ "node": ">= 0.4" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2023,6 +2284,17 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/config-file-ts": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", @@ -2082,6 +2354,13 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -2463,6 +2742,64 @@ "dev": true, "license": "MIT" }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2769,6 +3106,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2836,6 +3180,16 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -2910,6 +3264,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -2920,6 +3281,13 @@ "minimatch": "^5.0.1" } }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3243,6 +3611,24 @@ "dev": true, "license": "ISC" }, + "node_modules/happy-dom": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz", + "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3457,6 +3843,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -3555,6 +3948,86 @@ "node": ">=10" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -4256,6 +4729,16 @@ "node": ">=10" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4307,6 +4790,22 @@ "license": "MIT", "optional": true }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4342,6 +4841,17 @@ "node": ">= 0.4" } }, + "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/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4423,6 +4933,13 @@ "dev": true, "license": "ISC" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4535,6 +5052,13 @@ "node": ">=10" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -4852,6 +5376,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4891,6 +5422,21 @@ "node": ">=10" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -4963,6 +5509,13 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -4973,6 +5526,13 @@ "node": ">= 6" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5157,6 +5717,23 @@ "node": ">= 10.0.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -5174,6 +5751,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -5194,6 +5781,16 @@ "tmp": "^0.2.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -5385,6 +5982,96 @@ } } }, + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "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 + }, + "vite": { + "optional": false + } + } + }, "node_modules/vue": { "version": "3.5.32", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", @@ -5406,6 +6093,13 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-i18n": { "version": "9.14.5", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", @@ -5427,6 +6121,16 @@ "vue": "^3.0.0" } }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5443,6 +6147,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5487,6 +6208,28 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 8166142..b454141 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,11 @@ "build:win32": "vite build && electron-builder --win --ia32", "build:win-portable": "vite build && electron-builder --win portable", "build:win-installer": "vite build && electron-builder --win nsis", - "dist": "vite build && electron-builder" + "dist": "vite build && electron-builder", + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest --coverage", + "test:run": "vitest run" }, "build": { "appId": "com.iflow.settings-editor", @@ -84,14 +88,19 @@ } }, "devDependencies": { + "-": "^0.0.1", "@icon-park/vue-next": "^1.4.2", "@vitejs/plugin-vue": "^6.0.6", + "@vitest/ui": "^4.1.4", + "@vue/test-utils": "^2.4.6", "concurrently": "^8.2.2", "electron": "^28.0.0", "electron-builder": "^24.13.3", + "happy-dom": "^20.9.0", "less": "^4.6.4", "less-loader": "^12.3.2", "vite": "^8.0.8", + "vitest": "^4.1.4", "vue": "^3.4.0" }, "dependencies": { diff --git a/screenshots/theme-dark.png b/screenshots/theme-dark.png new file mode 100644 index 0000000..658f787 Binary files /dev/null and b/screenshots/theme-dark.png differ diff --git a/screenshots/theme-solarized-dark.png b/screenshots/theme-solarized-dark.png new file mode 100644 index 0000000..9ca96d2 Binary files /dev/null and b/screenshots/theme-solarized-dark.png differ diff --git a/screenshots/theme-xcode.png b/screenshots/theme-xcode.png new file mode 100644 index 0000000..6f596b8 Binary files /dev/null and b/screenshots/theme-xcode.png differ diff --git a/src/components/Footer.test.js b/src/components/Footer.test.js new file mode 100644 index 0000000..45b8ff3 --- /dev/null +++ b/src/components/Footer.test.js @@ -0,0 +1,80 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Footer from './Footer.vue'; + +describe('Footer.vue', () => { + it('renders correctly with default props', () => { + const wrapper = mount(Footer, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('.footer').exists()).toBe(true); + expect(wrapper.find('.footer-status').exists()).toBe(true); + }); + + it('displays current profile correctly', () => { + const wrapper = mount(Footer, { + props: { + currentProfile: 'dev', + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const statusText = wrapper.find('.footer-status').text(); + expect(statusText).toContain('dev'); + }); + + it('displays default profile when no prop provided', () => { + const wrapper = mount(Footer, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const statusText = wrapper.find('.footer-status').text(); + expect(statusText).toContain('default'); + }); + + it('displays status dot', () => { + const wrapper = mount(Footer, { + props: { + currentProfile: 'production', + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.find('.footer-status-dot').exists()).toBe(true); + }); + + it('applies translation correctly', () => { + const wrapper = mount(Footer, { + props: { + currentProfile: 'test-profile', + }, + global: { + mocks: { + $t: (key) => `translated-${key}`, + }, + }, + }); + + const statusText = wrapper.find('.footer-status').text(); + expect(statusText).toContain('translated-api.currentConfig'); + expect(statusText).toContain('test-profile'); + }); +}); diff --git a/src/components/SideBar.test.js b/src/components/SideBar.test.js new file mode 100644 index 0000000..d15dfc1 --- /dev/null +++ b/src/components/SideBar.test.js @@ -0,0 +1,164 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import SideBar from './SideBar.vue'; + +describe('SideBar.vue', () => { + it('renders correctly with default props', () => { + const wrapper = mount(SideBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('.sidebar').exists()).toBe(true); + }); + + it('has three nav items', () => { + const wrapper = mount(SideBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const navItems = wrapper.findAll('.nav-item'); + expect(navItems.length).toBe(3); + }); + + it('has two sections', () => { + const wrapper = mount(SideBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const sections = wrapper.findAll('.sidebar-section'); + expect(sections.length).toBe(2); + }); + + it('highlights active section correctly', () => { + const wrapper = mount(SideBar, { + props: { + currentSection: 'api', + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const navItems = wrapper.findAll('.nav-item'); + expect(navItems[0].classes('active')).toBe(false); + expect(navItems[1].classes('active')).toBe(true); + expect(navItems[2].classes('active')).toBe(false); + }); + + it('emits navigate event when nav item is clicked', async () => { + const wrapper = mount(SideBar, { + props: { + currentSection: 'general', + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const navItems = wrapper.findAll('.nav-item'); + await navItems[1].trigger('click'); + + expect(wrapper.emitted('navigate')).toBeTruthy(); + expect(wrapper.emitted('navigate')[0][0]).toBe('api'); + }); + + it('displays server count badge correctly', () => { + const wrapper = mount(SideBar, { + props: { + currentSection: 'general', + serverCount: 5 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const badges = wrapper.findAll('.nav-item-badge'); + expect(badges.length).toBe(1); + expect(badges[0].text()).toBe('5'); + }); + + it('displays zero server count', () => { + const wrapper = mount(SideBar, { + props: { + currentSection: 'general', + serverCount: 0 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const badges = wrapper.findAll('.nav-item-badge'); + expect(badges.length).toBe(1); + expect(badges[0].text()).toBe('0'); + }); + + it('applies translation to section titles', () => { + const wrapper = mount(SideBar, { + global: { + mocks: { + $t: (key) => `translated-${key}`, + }, + }, + }); + + const sectionTitles = wrapper.findAll('.sidebar-title'); + expect(sectionTitles[0].text()).toBe('translated-sidebar.general'); + expect(sectionTitles[1].text()).toBe('translated-sidebar.advanced'); + }); + + it('applies translation to nav item texts', () => { + const wrapper = mount(SideBar, { + global: { + mocks: { + $t: (key) => `translated-${key}`, + }, + }, + }); + + const navItems = wrapper.findAll('.nav-item-text'); + expect(navItems[0].text()).toBe('translated-sidebar.basicSettings'); + expect(navItems[1].text()).toBe('translated-sidebar.apiConfig'); + expect(navItems[2].text()).toBe('translated-sidebar.mcpServers'); + }); + + it('handles null currentSection', () => { + const wrapper = mount(SideBar, { + props: { + currentSection: null, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const navItems = wrapper.findAll('.nav-item'); + expect(navItems[0].classes('active')).toBe(false); + expect(navItems[1].classes('active')).toBe(false); + expect(navItems[2].classes('active')).toBe(false); + }); +}); diff --git a/src/components/TitleBar.test.js b/src/components/TitleBar.test.js new file mode 100644 index 0000000..3e9d166 --- /dev/null +++ b/src/components/TitleBar.test.js @@ -0,0 +1,131 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import TitleBar from './TitleBar.vue'; + +describe('TitleBar.vue', () => { + beforeEach(() => { + // Mock window.electronAPI + global.window.electronAPI = { + minimize: vi.fn(), + maximize: vi.fn(), + close: vi.fn() + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders correctly', () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('.titlebar').exists()).toBe(true); + expect(wrapper.find('.titlebar-title').exists()).toBe(true); + expect(wrapper.find('.titlebar-controls').exists()).toBe(true); + }); + + it('displays app title', () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.find('.titlebar-title').text()).toBe('app.title'); + }); + + it('has three window control buttons', () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const buttons = wrapper.findAll('.titlebar-btn'); + expect(buttons.length).toBe(3); + }); + + it('calls minimize when minimize button is clicked', async () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const minimizeButton = wrapper.findAll('.titlebar-btn')[0]; + await minimizeButton.trigger('click'); + + expect(window.electronAPI.minimize).toHaveBeenCalledOnce(); + }); + + it('calls maximize when maximize button is clicked', async () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const maximizeButton = wrapper.findAll('.titlebar-btn')[1]; + await maximizeButton.trigger('click'); + + expect(window.electronAPI.maximize).toHaveBeenCalledOnce(); + }); + + it('calls close when close button is clicked', async () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const closeButton = wrapper.findAll('.titlebar-btn')[2]; + await closeButton.trigger('click'); + + expect(window.electronAPI.close).toHaveBeenCalledOnce(); + }); + + it('has close button with close class', () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const closeButton = wrapper.findAll('.titlebar-btn')[2]; + expect(closeButton.classes()).toContain('close'); + }); + + it('applies translation to button tooltips', () => { + const wrapper = mount(TitleBar, { + global: { + mocks: { + $t: (key) => `translated-${key}`, + }, + }, + }); + + const buttons = wrapper.findAll('.titlebar-btn'); + expect(buttons[0].attributes('title')).toBe('translated-window.minimize'); + expect(buttons[1].attributes('title')).toBe('translated-window.maximize'); + expect(buttons[2].attributes('title')).toBe('translated-window.close'); + }); +}); diff --git a/src/styles/global.less b/src/styles/global.less index 7839cb9..6b12b3a 100644 --- a/src/styles/global.less +++ b/src/styles/global.less @@ -280,6 +280,11 @@ body { letter-spacing: -0.01em; } +.form-required { + color: var(--danger); + margin-left: 3px; +} + .form-input { width: 100%; padding: 10px 14px; @@ -434,6 +439,12 @@ body { transform: translateY(0) scale(0.98); } +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-secondary); + border: 1px solid var(--border); +} + .btn-secondary:hover { background: var(--bg-tertiary); color: var(--text-primary); @@ -447,7 +458,7 @@ body { .btn-danger { background: var(--danger); color: white; - box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); + border: 1px solid var(--danger); } .btn-danger:hover { @@ -470,6 +481,32 @@ body { font-size: 12px; } +// Side panel close button (used by ServerPanel and ApiProfileDialog) +.side-panel-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + border-radius: var(--radius); + transition: all 0.2s ease; +} +.side-panel-close:hover { + background: var(--bg-hover); + color: var(--text-primary); +} +.side-panel-close svg { + width: 14px; + height: 14px; + stroke: currentColor; + stroke-width: 1.5; + fill: none; +} + // Empty state .empty-state { display: flex; @@ -518,6 +555,10 @@ body { animation: fadeIn 0.15s ease; } +.dialog-overlay-top { + z-index: 1400; +} + .dialog { background: var(--bg-secondary); border-radius: var(--radius-lg); @@ -542,6 +583,12 @@ body { line-height: 1.5; } +.dialog-body { + padding: 20px 24px; + max-height: 60vh; + overflow-y: auto; +} + .dialog-actions { display: flex; justify-content: flex-end; diff --git a/src/views/ApiConfig.test.js b/src/views/ApiConfig.test.js new file mode 100644 index 0000000..01e27f9 --- /dev/null +++ b/src/views/ApiConfig.test.js @@ -0,0 +1,344 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import ApiConfig from './ApiConfig.vue'; + +describe('ApiConfig.vue', () => { + const mockSettings = { + apiProfiles: { + 'default': { + baseUrl: 'https://api.default.com', + selectedAuthType: 'openai-compatible', + apiKey: '', + modelName: '', + searchApiKey: '', + cna: '' + }, + 'dev': { + baseUrl: 'https://api.dev.com', + selectedAuthType: 'openai-compatible', + apiKey: 'dev-key', + modelName: 'gpt-4', + searchApiKey: '', + cna: '' + }, + 'prod': { + baseUrl: 'https://api.prod.com', + selectedAuthType: 'openai-compatible', + apiKey: 'prod-key', + modelName: 'gpt-4', + searchApiKey: '', + cna: '' + } + }, + currentApiProfile: 'default' + }; + + const mockProfiles = [ + { name: 'default' }, + { name: 'dev' }, + { name: 'prod' } + ]; + + it('renders correctly with props', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('.content-title').exists()).toBe(true); + expect(wrapper.find('.card').exists()).toBe(true); + expect(wrapper.find('.profile-list').exists()).toBe(true); + }); + + it('displays all profiles', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileItems = wrapper.findAll('.profile-item'); + expect(profileItems.length).toBe(3); + }); + + it('highlights current profile', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'dev', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileItems = wrapper.findAll('.profile-item'); + expect(profileItems[0].classes('active')).toBe(false); + expect(profileItems[1].classes('active')).toBe(true); + expect(profileItems[2].classes('active')).toBe(false); + }); + + it('shows status badge only for current profile', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const statusBadges = wrapper.findAll('.status-badge'); + expect(statusBadges.length).toBe(1); + expect(wrapper.findAll('.profile-item')[0].find('.status-badge').exists()).toBe(true); + expect(wrapper.findAll('.profile-item')[1].find('.status-badge').exists()).toBe(false); + }); + + it('emits create-profile event when create button is clicked', async () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + await wrapper.find('.btn-primary').trigger('click'); + expect(wrapper.emitted('create-profile')).toBeTruthy(); + }); + + it('emits select-profile event when profile is clicked', async () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileItems = wrapper.findAll('.profile-item'); + await profileItems[1].trigger('click'); + + expect(wrapper.emitted('select-profile')).toBeTruthy(); + expect(wrapper.emitted('select-profile')[0][0]).toBe('dev'); + }); + + it('emits edit-profile event when edit button is clicked', async () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const editButtons = wrapper.findAll('.action-btn'); + await editButtons[0].trigger('click'); + + expect(wrapper.emitted('edit-profile')).toBeTruthy(); + expect(wrapper.emitted('edit-profile')[0][0]).toBe('default'); + }); + + it('emits duplicate-profile event when duplicate button is clicked', async () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const duplicateButtons = wrapper.findAll('.action-btn'); + await duplicateButtons[1].trigger('click'); + + expect(wrapper.emitted('duplicate-profile')).toBeTruthy(); + expect(wrapper.emitted('duplicate-profile')[0][0]).toBe('default'); + }); + + it('shows delete button only for non-default profiles', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileItems = wrapper.findAll('.profile-item'); + const deleteButtons = wrapper.findAll('.action-btn-danger'); + + expect(deleteButtons.length).toBe(2); + expect(profileItems[0].find('.action-btn-danger').exists()).toBe(false); + expect(profileItems[1].find('.action-btn-danger').exists()).toBe(true); + expect(profileItems[2].find('.action-btn-danger').exists()).toBe(true); + }); + + it('emits delete-profile event when delete button is clicked', async () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const deleteButtons = wrapper.findAll('.action-btn-danger'); + await deleteButtons[0].trigger('click'); + + expect(wrapper.emitted('delete-profile')).toBeTruthy(); + expect(wrapper.emitted('delete-profile')[0][0]).toBe('dev'); + }); + + it('displays correct profile names', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileNames = wrapper.findAll('.profile-name'); + expect(profileNames[0].text()).toBe('default'); + expect(profileNames[1].text()).toBe('dev'); + expect(profileNames[2].text()).toBe('prod'); + }); + + it('displays correct profile URLs', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileUrls = wrapper.findAll('.profile-url'); + expect(profileUrls[0].text()).toBe('https://api.default.com'); + expect(profileUrls[1].text()).toBe('https://api.dev.com'); + expect(profileUrls[2].text()).toBe('https://api.prod.com'); + }); + + it('displays correct profile initials', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const iconTexts = wrapper.findAll('.profile-icon-text'); + expect(iconTexts[0].text()).toBe('D'); + expect(iconTexts[1].text()).toBe('D'); + expect(iconTexts[2].text()).toBe('P'); + }); + + it('handles empty profiles array', () => { + const wrapper = mount(ApiConfig, { + props: { + profiles: [], + currentProfile: 'default', + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileItems = wrapper.findAll('.profile-item'); + expect(profileItems.length).toBe(0); + }); + + it('handles missing apiProfiles in settings', () => { + const settingsWithoutProfiles = { currentApiProfile: 'default' }; + + const wrapper = mount(ApiConfig, { + props: { + profiles: mockProfiles, + currentProfile: 'default', + settings: settingsWithoutProfiles, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const profileUrls = wrapper.findAll('.profile-url'); + expect(profileUrls[0].text()).toBe(''); + expect(profileUrls[1].text()).toBe(''); + expect(profileUrls[2].text()).toBe(''); + }); +}); diff --git a/src/views/GeneralSettings.test.js b/src/views/GeneralSettings.test.js new file mode 100644 index 0000000..4d05aea --- /dev/null +++ b/src/views/GeneralSettings.test.js @@ -0,0 +1,157 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import GeneralSettings from './GeneralSettings.vue'; + +describe('GeneralSettings.vue', () => { + const mockSettings = { + language: 'zh-CN', + theme: 'Xcode', + bootAnimationShown: true, + checkpointing: { enabled: true }, + }; + + it('renders correctly with props', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('.content-title').exists()).toBe(true); + expect(wrapper.findAll('.card').length).toBe(2); + }); + + it('displays language options correctly', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const languageOptions = wrapper.findAll('.form-select')[0].findAll('option'); + expect(languageOptions.length).toBe(3); + expect(languageOptions[0].attributes('value')).toBe('zh-CN'); + expect(languageOptions[1].attributes('value')).toBe('en-US'); + expect(languageOptions[2].attributes('value')).toBe('ja-JP'); + }); + + it('displays theme options correctly', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const themeOptions = wrapper.findAll('.form-select')[1].findAll('option'); + expect(themeOptions.length).toBe(4); + expect(themeOptions[0].attributes('value')).toBe('Xcode'); + expect(themeOptions[1].attributes('value')).toBe('Dark'); + expect(themeOptions[2].attributes('value')).toBe('Light'); + expect(themeOptions[3].attributes('value')).toBe('Solarized Dark'); + }); + + it('reflects current settings in form controls', async () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + await nextTick(); + const selectElements = wrapper.findAll('.form-select'); + expect(selectElements[0].element.value).toBe('zh-CN'); + expect(selectElements[1].element.value).toBe('Xcode'); + expect(selectElements[2].element.value).toBe('true'); + expect(selectElements[3].element.value).toBe('true'); + }); + + it('applies translation correctly', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => `translated-${key}`, + }, + }, + }); + + expect(wrapper.find('.content-title').text()).toBe('translated-general.title'); + expect(wrapper.find('.content-desc').text()).toBe('translated-general.description'); + }); + + it('has two cards for settings sections', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const cards = wrapper.findAll('.card'); + expect(cards.length).toBe(2); + }); + + it('displays card titles with icons', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const cardTitles = wrapper.findAll('.card-title'); + expect(cardTitles.length).toBe(2); + expect(cardTitles[0].text()).toContain('general.languageInterface'); + expect(cardTitles[1].text()).toContain('general.otherSettings'); + }); + + it('shows all form controls with proper structure', () => { + const wrapper = mount(GeneralSettings, { + props: { + settings: mockSettings, + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.findAll('.form-row').length).toBe(2); + expect(wrapper.findAll('.form-group').length).toBe(4); + expect(wrapper.findAll('.form-label').length).toBe(4); + expect(wrapper.findAll('.form-select').length).toBe(4); + }); +}); diff --git a/src/views/McpServers.test.js b/src/views/McpServers.test.js new file mode 100644 index 0000000..8dae529 --- /dev/null +++ b/src/views/McpServers.test.js @@ -0,0 +1,256 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import McpServers from './McpServers.vue'; + +describe('McpServers.vue', () => { + const mockServers = { + 'server1': { + description: '第一个服务器', + command: 'node server.js', + args: ['--port', '3000'], + env: {} + }, + 'server2': { + description: '第二个服务器', + command: 'python server.py', + args: [], + env: { 'PYTHONPATH': '/path/to/python' } + }, + 'server3': { + command: 'java -jar server.jar', + args: [], + env: {} + } + }; + + it('renders correctly with props', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('.content-title').exists()).toBe(true); + expect(wrapper.find('.server-list').exists()).toBe(true); + }); + + it('displays all servers', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const serverItems = wrapper.findAll('.server-item'); + expect(serverItems.length).toBe(3); + }); + + it('highlights selected server', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server2', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const serverItems = wrapper.findAll('.server-item'); + expect(serverItems[0].classes('selected')).toBe(false); + expect(serverItems[1].classes('selected')).toBe(true); + expect(serverItems[2].classes('selected')).toBe(false); + }); + + it('shows empty state when no servers', () => { + const wrapper = mount(McpServers, { + props: { + servers: {}, + selectedServer: null, + serverCount: 0 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.find('.empty-state').exists()).toBe(true); + expect(wrapper.find('.empty-state-title').exists()).toBe(true); + expect(wrapper.find('.empty-state-desc').exists()).toBe(true); + expect(wrapper.findAll('.server-item').length).toBe(0); + }); + + it('emits add-server event when add button is clicked', async () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + await wrapper.find('.btn-primary').trigger('click'); + expect(wrapper.emitted('add-server')).toBeTruthy(); + }); + + it('emits select-server event when server is clicked', async () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const serverItems = wrapper.findAll('.server-item'); + await serverItems[1].trigger('click'); + + expect(wrapper.emitted('select-server')).toBeTruthy(); + expect(wrapper.emitted('select-server')[0][0]).toBe('server2'); + }); + + it('displays correct server names', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const serverNames = wrapper.findAll('.server-name'); + expect(serverNames[0].text()).toBe('server1'); + expect(serverNames[1].text()).toBe('server2'); + expect(serverNames[2].text()).toBe('server3'); + }); + + it('displays correct server descriptions', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const serverDescs = wrapper.findAll('.server-desc'); + expect(serverDescs[0].text()).toBe('第一个服务器'); + expect(serverDescs[1].text()).toBe('第二个服务器'); + expect(serverDescs[2].text()).toBe('mcp.noDescription'); + }); + + it('displays status indicators for all servers', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: 'server1', + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const statusIndicators = wrapper.findAll('.server-status'); + expect(statusIndicators.length).toBe(3); + }); + + it('handles null selectedServer prop', () => { + const wrapper = mount(McpServers, { + props: { + servers: mockServers, + selectedServer: null, + serverCount: 3 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + const serverItems = wrapper.findAll('.server-item'); + expect(serverItems.length).toBe(3); + expect(serverItems[0].classes('selected')).toBe(false); + expect(serverItems[1].classes('selected')).toBe(false); + expect(serverItems[2].classes('selected')).toBe(false); + }); + + it('handles zero serverCount with empty servers object', () => { + const wrapper = mount(McpServers, { + props: { + servers: {}, + selectedServer: null, + serverCount: 0 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.find('.empty-state').exists()).toBe(true); + expect(wrapper.findAll('.server-item').length).toBe(0); + }); + + it('displays empty state title correctly', () => { + const wrapper = mount(McpServers, { + props: { + servers: {}, + selectedServer: null, + serverCount: 0 + }, + global: { + mocks: { + $t: (key) => key, + }, + }, + }); + + expect(wrapper.find('.empty-state-title').text()).toBe('mcp.noServers'); + expect(wrapper.find('.empty-state-desc').text()).toBe('mcp.addFirstServer'); + }); +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..117a4f4 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from 'vitest/config'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'happy-dom', + globals: true, + setupFiles: [], + include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + exclude: ['node_modules', 'dist', 'release', '.git'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'dist/', + 'release/', + 'test/', + '**/*.config.js', + 'main.js', + 'preload.js' + ] + } + }, + server: { + port: 5174 + } +});