新增 单元测试框架和测试用例

This commit is contained in:
2026-04-18 03:03:19 +08:00
parent 2d2804ef22
commit aa375bfff0
13 changed files with 1962 additions and 2 deletions

743
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

BIN
screenshots/theme-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
screenshots/theme-xcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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;

344
src/views/ApiConfig.test.js Normal file
View File

@@ -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('');
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

29
vitest.config.js Normal file
View File

@@ -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
}
});