From aa375bfff0da0faf45c026a1a4fef83fb33287f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=B6=9B?= Date: Sat, 18 Apr 2026 03:03:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=A1=86=E6=9E=B6=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 743 +++++++++++++++++++++++++++ package.json | 11 +- screenshots/theme-dark.png | Bin 0 -> 22391 bytes screenshots/theme-solarized-dark.png | Bin 0 -> 22593 bytes screenshots/theme-xcode.png | Bin 0 -> 22585 bytes src/components/Footer.test.js | 80 +++ src/components/SideBar.test.js | 164 ++++++ src/components/TitleBar.test.js | 131 +++++ src/styles/global.less | 49 +- src/views/ApiConfig.test.js | 344 +++++++++++++ src/views/GeneralSettings.test.js | 157 ++++++ src/views/McpServers.test.js | 256 +++++++++ vitest.config.js | 29 ++ 13 files changed, 1962 insertions(+), 2 deletions(-) create mode 100644 screenshots/theme-dark.png create mode 100644 screenshots/theme-solarized-dark.png create mode 100644 screenshots/theme-xcode.png create mode 100644 src/components/Footer.test.js create mode 100644 src/components/SideBar.test.js create mode 100644 src/components/TitleBar.test.js create mode 100644 src/views/ApiConfig.test.js create mode 100644 src/views/GeneralSettings.test.js create mode 100644 src/views/McpServers.test.js create mode 100644 vitest.config.js 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 0000000000000000000000000000000000000000..658f787a50486005c321145ede4fac2b9360550a GIT binary patch literal 22391 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

>7XWvTUwq=)#chzt7Rh9E~ZF%ypYj-w+l&J_T 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 Lq^Ve;VE*o}WSvv~ literal 0 HcmV?d00001 diff --git a/screenshots/theme-solarized-dark.png b/screenshots/theme-solarized-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9ca96d2bbf270122f9cf47fbacdd7d1ddefbfcf1 GIT binary patch literal 22593 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 literal 0 HcmV?d00001 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 + } +});