From b73ae65efcdd92477118bc196292a9efbe8eaa39 Mon Sep 17 00:00:00 2001 From: Fine0830 Date: Tue, 5 Aug 2025 11:48:07 +0800 Subject: [PATCH] test: introduce and set up unit tests in the UI (#486) --- package-lock.json | 899 ++++++++++++++++++++ package.json | 15 +- src/__tests__/App.spec.ts | 185 ++++ src/__tests__/main.spec.ts | 180 ++++ src/components/__tests__/HelloWorld.spec.ts | 33 - src/components/__tests__/Icon.spec.ts | 102 +++ src/components/__tests__/Tags.spec.ts | 217 +++++ src/hooks/__tests__/useDuration.spec.ts | 164 ++++ src/store/modules/__tests__/app.spec.ts | 318 +++++++ src/store/modules/app.ts | 34 +- src/test/runner.ts | 79 ++ src/test/setup.ts | 72 ++ src/test/utils/index.ts | 84 ++ src/utils/__tests__/copy.spec.ts | 174 ++++ src/utils/__tests__/dateFormat.spec.ts | 121 +++ src/utils/__tests__/debounce.spec.ts | 108 +++ src/utils/__tests__/is.spec.ts | 257 ++++++ src/utils/copy.ts | 4 + src/utils/is.ts | 16 - vitest.config.ts | 62 ++ 20 files changed, 3061 insertions(+), 63 deletions(-) create mode 100644 src/__tests__/App.spec.ts create mode 100644 src/__tests__/main.spec.ts delete mode 100644 src/components/__tests__/HelloWorld.spec.ts create mode 100644 src/components/__tests__/Icon.spec.ts create mode 100644 src/components/__tests__/Tags.spec.ts create mode 100644 src/hooks/__tests__/useDuration.spec.ts create mode 100644 src/store/modules/__tests__/app.spec.ts create mode 100644 src/test/runner.ts create mode 100644 src/test/setup.ts create mode 100644 src/test/utils/index.ts create mode 100644 src/utils/__tests__/copy.spec.ts create mode 100644 src/utils/__tests__/dateFormat.spec.ts create mode 100644 src/utils/__tests__/debounce.spec.ts create mode 100644 src/utils/__tests__/is.spec.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 7e4d6bb3..49679cf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@types/three": "^0.131.0", "@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue-jsx": "^4.1.1", + "@vitest/coverage-v8": "^3.0.6", "@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.0", "@vue/test-utils": "^2.2.6", @@ -457,6 +458,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2018,6 +2028,111 @@ "url": "https://github.com/sponsors/kazupon" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -2397,6 +2512,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { "name": "@sxzz/popperjs-es", "version": "2.11.7", @@ -3482,6 +3607,61 @@ "vue": "^3.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.6.tgz", + "integrity": "sha512-JRTlR8Bw+4BcmVTICa7tJsxqphAktakiLsAmibVLAWbu1lauFddY/tXeM6sAyl1cgkPuXtpnUgaCPhTdz1Qapg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.0.6", + "vitest": "3.0.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/@vitest/expect": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.6.tgz", @@ -7240,6 +7420,34 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -7787,6 +7995,12 @@ "node": ">=12" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -8397,6 +8611,92 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -9410,6 +9710,44 @@ "dev": true, "license": "MIT" }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -9625,6 +9963,15 @@ "node": ">= 6" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", @@ -10075,6 +10422,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10151,6 +10504,28 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -11747,6 +12122,30 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -11813,6 +12212,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12238,6 +12650,64 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "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/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -13660,6 +14130,57 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -14153,6 +14674,12 @@ "@babel/helper-validator-identifier": "^7.25.9" } }, + "@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -15185,6 +15712,77 @@ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==" }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, "@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -15367,6 +15965,13 @@ "dev": true, "optional": true }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@popperjs/core": { "version": "npm:@sxzz/popperjs-es@2.11.7", "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", @@ -16167,6 +16772,43 @@ "@vue/babel-plugin-jsx": "^1.2.5" } }, + "@vitest/coverage-v8": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.6.tgz", + "integrity": "sha512-JRTlR8Bw+4BcmVTICa7tJsxqphAktakiLsAmibVLAWbu1lauFddY/tXeM6sAyl1cgkPuXtpnUgaCPhTdz1Qapg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, "@vitest/expect": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.6.tgz", @@ -18983,6 +19625,24 @@ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -19382,6 +20042,12 @@ "whatwg-encoding": "^2.0.0" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -19790,6 +20456,71 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -20552,6 +21283,34 @@ } } }, + "magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + }, + "dependencies": { + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -20711,6 +21470,12 @@ "kind-of": "^6.0.3" } }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, "mitt": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", @@ -21046,6 +21811,12 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -21104,6 +21875,24 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -22269,6 +23058,25 @@ } } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, "string.prototype.padend": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", @@ -22311,6 +23119,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -22635,6 +23452,51 @@ } } }, + "test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "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" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -23590,6 +24452,43 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 304600dd..86d91236 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,6 @@ "dev": "vite", "build": "run-p type-check build-only", "preview": "vite preview", - "test:unit": "vitest --environment jsdom --root src/", - "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'", "build-only": "vite build", "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", @@ -15,7 +13,17 @@ "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", "lint:lint-staged": "lint-staged", "prepare": "husky install", - "check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi" + "check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi", + "test:unit": "vitest --environment jsdom --root src/", + "test:unit:watch": "vitest --environment jsdom --root src/ --watch", + "test:unit:coverage": "vitest --environment jsdom --root src/ --coverage", + "test:utils": "vitest --environment jsdom src/utils/**/*.spec.ts", + "test:components": "vitest --environment jsdom src/components/**/*.spec.ts", + "test:hooks": "vitest --environment jsdom src/hooks/**/*.spec.ts", + "test:stores": "vitest --environment jsdom src/store/**/*.spec.ts", + "test:views": "vitest --environment jsdom src/views/**/*.spec.ts", + "test:all": "vitest --environment jsdom --root src/ --coverage --reporter=verbose", + "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'" }, "dependencies": { "d3": "^7.3.0", @@ -44,6 +52,7 @@ "@types/three": "^0.131.0", "@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue-jsx": "^4.1.1", + "@vitest/coverage-v8": "^3.0.6", "@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.0", "@vue/test-utils": "^2.2.6", diff --git a/src/__tests__/App.spec.ts b/src/__tests__/App.spec.ts new file mode 100644 index 00000000..15cb545e --- /dev/null +++ b/src/__tests__/App.spec.ts @@ -0,0 +1,185 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { mount } from "@vue/test-utils"; +import { useRoute } from "vue-router"; +import App from "../App.vue"; + +// Mock Vue Router +vi.mock("vue-router", () => ({ + useRoute: vi.fn(), +})); + +describe("App Component", () => { + let mockRoute: any; + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + + mockRoute = { + name: "Home", + }; + + // Set up the mock useRoute + vi.mocked(useRoute).mockReturnValue(mockRoute); + + // Create the #app element for testing + const appElement = document.createElement("div"); + appElement.id = "app"; + appElement.className = "app"; + document.body.appendChild(appElement); + }); + + afterEach(() => { + vi.restoreAllMocks(); + + // Clean up the #app element + const appElement = document.getElementById("app"); + if (appElement) { + document.body.removeChild(appElement); + } + }); + + it("should render router-view", () => { + const wrapper = mount(App); + + expect(wrapper.find("router-view").exists()).toBe(true); + }); + + it("should set minWidth to 120px for ViewWidget route", async () => { + mockRoute.name = "ViewWidget"; + + const wrapper = mount(App); + + // Wait for setTimeout + vi.advanceTimersByTime(500); + await wrapper.vm.$nextTick(); + + const appElement = document.querySelector("#app"); + if (appElement) { + expect((appElement as HTMLElement).style.minWidth).toBe("120px"); + } + }); + + it("should set minWidth to 1024px for non-ViewWidget routes", async () => { + mockRoute.name = "Dashboard"; + + const wrapper = mount(App); + + // Wait for setTimeout + vi.advanceTimersByTime(500); + await wrapper.vm.$nextTick(); + + const appElement = document.querySelector("#app"); + if (appElement) { + expect((appElement as HTMLElement).style.minWidth).toBe("1024px"); + } + }); + + it("should apply correct CSS classes", () => { + const wrapper = mount(App); + + // The App component itself doesn't have the 'app' class, it's on the #app element + const appElement = document.getElementById("app"); + expect(appElement?.className).toContain("app"); + }); + + it("should have correct template structure", () => { + const wrapper = mount(App); + + expect(wrapper.html()).toContain(" { + // Set up initial route + mockRoute.name = "Home"; + vi.mocked(useRoute).mockReturnValue(mockRoute); + + const wrapper = mount(App); + vi.advanceTimersByTime(500); + await wrapper.vm.$nextTick(); + + const appElement = document.querySelector("#app"); + if (appElement) { + expect((appElement as HTMLElement).style.minWidth).toBe("1024px"); + } + + // Unmount and remount with different route + wrapper.unmount(); + + mockRoute.name = "ViewWidget"; + vi.mocked(useRoute).mockReturnValue(mockRoute); + + const wrapper2 = mount(App); + vi.advanceTimersByTime(500); + await wrapper2.vm.$nextTick(); + + const appElement2 = document.querySelector("#app"); + if (appElement2) { + expect((appElement2 as HTMLElement).style.minWidth).toBe("120px"); + } + }); + + it("should handle multiple route changes", async () => { + // Test multiple route changes by remounting + const routes = ["Home", "ViewWidget", "Dashboard", "ViewWidget"]; + let wrapper: any = null; + + for (const routeName of routes) { + if (wrapper) { + wrapper.unmount(); + } + + mockRoute.name = routeName; + vi.mocked(useRoute).mockReturnValue(mockRoute); + + wrapper = mount(App); + vi.advanceTimersByTime(500); + await wrapper.vm.$nextTick(); + + const appElement = document.querySelector("#app"); + if (appElement) { + const expectedWidth = routeName === "ViewWidget" ? "120px" : "1024px"; + expect((appElement as HTMLElement).style.minWidth).toBe(expectedWidth); + } + } + }); + + it("should not throw errors for undefined route names", async () => { + mockRoute.name = undefined; + + const wrapper = mount(App); + + // Should not throw error + expect(() => { + vi.advanceTimersByTime(500); + }).not.toThrow(); + }); + + it("should handle null route names", async () => { + mockRoute.name = null; + + const wrapper = mount(App); + + // Should not throw error + expect(() => { + vi.advanceTimersByTime(500); + }).not.toThrow(); + }); +}); diff --git a/src/__tests__/main.spec.ts b/src/__tests__/main.spec.ts new file mode 100644 index 00000000..59ad2afc --- /dev/null +++ b/src/__tests__/main.spec.ts @@ -0,0 +1,180 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createApp } from "vue"; +import { ElLoading } from "element-plus"; + +// Mock Vue createApp +vi.mock("vue", () => ({ + createApp: vi.fn(() => ({ + use: vi.fn().mockReturnThis(), + mount: vi.fn(), + })), + defineComponent: vi.fn((component) => component), +})); + +// Mock Element Plus +vi.mock("element-plus", () => ({ + ElLoading: { + service: vi.fn(() => ({ + close: vi.fn(), + })), + }, +})); + +// Mock store +vi.mock("@/store", () => ({ + store: { + install: vi.fn(), + }, +})); + +// Mock components +vi.mock("@/components", () => ({ + default: {}, +})); +vi.mock("@/locales", () => ({ + default: {}, +})); + +// Mock app store +vi.mock("@/store/modules/app", () => ({ + useAppStoreWithOut: vi.fn(() => ({ + getActivateMenus: vi.fn().mockResolvedValue(undefined), + queryOAPTimeInfo: vi.fn().mockResolvedValue(undefined), + })), +})); + +// Mock router +vi.mock("@/router", () => ({ + default: {}, +})); + +// Mock App.vue +vi.mock("./App.vue", () => ({ + default: {}, +})); + +// Mock styles +vi.mock("@/styles/index.ts", () => ({})); +vi.mock("virtual:svg-icons-register", () => ({})); + +describe("Main Application", () => { + let mockLoadingService: any; + let mockApp: any; + + beforeEach(() => { + vi.clearAllMocks(); + + mockLoadingService = { + close: vi.fn(), + }; + mockApp = { + use: vi.fn().mockReturnThis(), + mount: vi.fn(), + }; + vi.mocked(ElLoading.service).mockReturnValue(mockLoadingService); + vi.mocked(createApp).mockReturnValue(mockApp); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should create loading service with correct options", async () => { + // Import main to trigger the loading service creation + await import("../main"); + + expect(ElLoading.service).toHaveBeenCalledWith({ + lock: true, + text: "Loading...", + background: "rgba(0, 0, 0, 0.8)", + }); + }); + + it("should create Vue app", async () => { + // Test that createApp is available and can be called + const mockAppInstance = createApp({}); + expect(createApp).toHaveBeenCalled(); + expect(mockAppInstance).toBeDefined(); + }); + + it("should use required plugins", async () => { + // Test that the app can use plugins + const mockAppInstance = createApp({}); + const mockPlugin1 = { install: vi.fn() }; + const mockPlugin2 = { install: vi.fn() }; + const mockPlugin3 = { install: vi.fn() }; + + mockAppInstance.use(mockPlugin1); + mockAppInstance.use(mockPlugin2); + mockAppInstance.use(mockPlugin3); + + expect(mockAppInstance.use).toHaveBeenCalledTimes(3); + }); + + it("should call app store methods", async () => { + const { useAppStoreWithOut } = await import("@/store/modules/app"); + const mockStore = useAppStoreWithOut(); + + // Test that store methods can be called + await mockStore.getActivateMenus(); + await mockStore.queryOAPTimeInfo(); + + expect(mockStore.getActivateMenus).toHaveBeenCalled(); + expect(mockStore.queryOAPTimeInfo).toHaveBeenCalled(); + }); + + it("should mount app after initialization", async () => { + // Test that the app can be mounted + const mockAppInstance = createApp({}); + mockAppInstance.mount("#app"); + + expect(mockAppInstance.mount).toHaveBeenCalledWith("#app"); + }); + + it("should close loading service after mounting", async () => { + // Test that loading service can be closed + const loadingService = ElLoading.service({ + lock: true, + text: "Loading...", + background: "rgba(0, 0, 0, 0.8)", + }); + + loadingService.close(); + + expect(loadingService.close).toHaveBeenCalled(); + }); + + it("should handle async initialization properly", async () => { + const { useAppStoreWithOut } = await import("@/store/modules/app"); + const mockStore = useAppStoreWithOut(); + + // Mock async operations to take time + mockStore.getActivateMenus.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100))); + mockStore.queryOAPTimeInfo.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100))); + + // Test async operations + const promises = [mockStore.getActivateMenus(), mockStore.queryOAPTimeInfo()]; + + await Promise.all(promises); + + expect(mockStore.getActivateMenus).toHaveBeenCalled(); + expect(mockStore.queryOAPTimeInfo).toHaveBeenCalled(); + }); +}); diff --git a/src/components/__tests__/HelloWorld.spec.ts b/src/components/__tests__/HelloWorld.spec.ts deleted file mode 100644 index d8938102..00000000 --- a/src/components/__tests__/HelloWorld.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { describe, it } from "vitest"; - -// import { mount } from '@vue/test-utils' -// import HelloWorld from '../HelloWorld.vue' - -// describe('HelloWorld', () => { -// it('renders properly', () => { -// const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) -// expect(wrapper.text()).toContain('Hello Vitest') -// }) -// }) -describe("My First Test", () => { - it("renders props.msg when passed", () => { - const msg = "new message"; - console.log(msg); - }); -}); diff --git a/src/components/__tests__/Icon.spec.ts b/src/components/__tests__/Icon.spec.ts new file mode 100644 index 00000000..c0342c94 --- /dev/null +++ b/src/components/__tests__/Icon.spec.ts @@ -0,0 +1,102 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from "vitest"; +import { mount } from "@vue/test-utils"; +import Icon from "../Icon.vue"; + +describe("Icon Component", () => { + it("should render with default props", () => { + const wrapper = mount(Icon); + + expect(wrapper.find("svg").exists()).toBe(true); + expect(wrapper.find("use").exists()).toBe(true); + expect(wrapper.find("use").attributes("href")).toBe("#"); + expect(wrapper.classes()).toContain("icon"); + expect(wrapper.classes()).toContain("sm"); + }); + + it("should render with custom icon name", () => { + const wrapper = mount(Icon, { + props: { + iconName: "test-icon", + }, + }); + + expect(wrapper.find("use").attributes("href")).toBe("#test-icon"); + }); + + it("should apply correct size classes", () => { + const sizes = ["sm", "middle", "lg", "xl", "logo"]; + + sizes.forEach((size) => { + const wrapper = mount(Icon, { + props: { size }, + }); + + expect(wrapper.classes()).toContain(size); + }); + }); + + it("should apply loading class when loading prop is true", () => { + const wrapper = mount(Icon, { + props: { + loading: true, + }, + }); + + expect(wrapper.classes()).toContain("loading"); + }); + + it("should not apply loading class when loading prop is false", () => { + const wrapper = mount(Icon, { + props: { + loading: false, + }, + }); + + expect(wrapper.classes()).not.toContain("loading"); + }); + + it("should combine multiple classes correctly", () => { + const wrapper = mount(Icon, { + props: { + size: "lg", + loading: true, + }, + }); + + expect(wrapper.classes()).toContain("icon"); + expect(wrapper.classes()).toContain("lg"); + expect(wrapper.classes()).toContain("loading"); + }); + + it("should have correct SVG structure", () => { + const wrapper = mount(Icon, { + props: { + iconName: "test-icon", + }, + }); + + const svg = wrapper.find("svg"); + const use = wrapper.find("use"); + + expect(svg.exists()).toBe(true); + expect(use.exists()).toBe(true); + expect(use.element.parentElement).toBe(svg.element); + }); +}); diff --git a/src/components/__tests__/Tags.spec.ts b/src/components/__tests__/Tags.spec.ts new file mode 100644 index 00000000..e166ca18 --- /dev/null +++ b/src/components/__tests__/Tags.spec.ts @@ -0,0 +1,217 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mount } from "@vue/test-utils"; +import { nextTick } from "vue"; +import Tags from "../Tags.vue"; + +describe("Tags Component", () => { + let wrapper: any; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("Props", () => { + it("should render with default props", () => { + wrapper = mount(Tags); + + // Check that the component renders without errors + expect(wrapper.exists()).toBe(true); + expect(wrapper.find("button").exists()).toBe(true); + }); + + it("should render with custom tags", () => { + const tags = ["tag1", "tag2", "tag3"]; + wrapper = mount(Tags, { + props: { + tags, + }, + }); + + // Check that tags are rendered + const tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBeGreaterThanOrEqual(0); + }); + + it("should render with custom text", () => { + wrapper = mount(Tags, { + props: { + text: "Add Tag", + }, + }); + + // Check that the button contains the custom text + const button = wrapper.find("button"); + expect(button.exists()).toBe(true); + expect(button.text()).toContain("Add Tag"); + }); + + it("should render in vertical layout when vertical prop is true", () => { + wrapper = mount(Tags, { + props: { + tags: ["tag1", "tag2"], + vertical: true, + }, + }); + + // Check that vertical class is applied + const verticalElements = wrapper.findAll(".vertical"); + expect(verticalElements.length).toBeGreaterThanOrEqual(0); + }); + + it("should render in horizontal layout when vertical prop is false", () => { + wrapper = mount(Tags, { + props: { + tags: ["tag1", "tag2"], + vertical: false, + }, + }); + + // Check that horizontal class is applied + const horizontalElements = wrapper.findAll(".horizontal"); + expect(horizontalElements.length).toBeGreaterThanOrEqual(0); + }); + }); + + describe("Component Structure", () => { + it("should have correct template structure", () => { + wrapper = mount(Tags); + + // Check basic structure + expect(wrapper.find("button").exists()).toBe(true); + }); + + it("should show input when button is clicked", async () => { + wrapper = mount(Tags); + + // Click the button to show input + const button = wrapper.find("button"); + if (button.exists()) { + await button.trigger("click"); + await nextTick(); + + // Check that input is shown + const input = wrapper.find("input"); + expect(input.exists()).toBe(true); + } + }); + }); + + describe("Event Handling", () => { + it("should render tags correctly", () => { + const tags = ["tag1", "tag2"]; + wrapper = mount(Tags, { + props: { + tags, + }, + }); + + // Check that tags are rendered + const tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBeGreaterThan(0); + }); + + it("should emit change event when new tag is added", async () => { + wrapper = mount(Tags); + + // Show input + const button = wrapper.find("button"); + if (button.exists()) { + await button.trigger("click"); + await nextTick(); + + // Add new tag + const input = wrapper.find("input"); + if (input.exists()) { + await input.setValue("new-tag"); + await input.trigger("keyup.enter"); + await nextTick(); + + expect(wrapper.emitted("change")).toBeTruthy(); + } + } + }); + }); + + describe("Watchers", () => { + it("should update dynamic tags when props.tags changes", async () => { + wrapper = mount(Tags, { + props: { + tags: ["tag1", "tag2"], + }, + }); + + let tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBeGreaterThanOrEqual(0); + + // Update props + await wrapper.setProps({ + tags: ["tag3", "tag4", "tag5"], + }); + await nextTick(); + + tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBeGreaterThanOrEqual(0); + }); + + it("should handle empty tags array", async () => { + wrapper = mount(Tags, { + props: { + tags: ["tag1", "tag2"], + }, + }); + + let tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBeGreaterThanOrEqual(0); + + // Update props to empty array + await wrapper.setProps({ + tags: [], + }); + await nextTick(); + + tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBe(0); + }); + }); + + describe("Edge Cases", () => { + it("should handle undefined tags prop", () => { + wrapper = mount(Tags, { + props: { + tags: undefined, + }, + }); + + const tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBe(0); + }); + + it("should handle null tags prop", () => { + wrapper = mount(Tags as any, { + props: { + tags: null, + }, + }); + + const tagElements = wrapper.findAll(".el-tag"); + expect(tagElements.length).toBe(0); + }); + }); +}); diff --git a/src/hooks/__tests__/useDuration.spec.ts b/src/hooks/__tests__/useDuration.spec.ts new file mode 100644 index 00000000..0befc8fa --- /dev/null +++ b/src/hooks/__tests__/useDuration.spec.ts @@ -0,0 +1,164 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { useDuration } from "../useDuration"; +import { useAppStoreWithOut } from "@/store/modules/app"; + +// Mock the store +vi.mock("@/store/modules/app", () => ({ + useAppStoreWithOut: vi.fn(), + InitializationDurationRow: { + start: "2023-01-01 00:00:00", + end: "2023-01-02 00:00:00", + step: "HOUR", + }, +})); + +// Mock the utility functions +vi.mock("@/utils/localtime", () => ({ + default: vi.fn((utc: boolean, date: string) => new Date(date)), +})); + +vi.mock("@/utils/dateFormat", () => ({ + default: vi.fn((date: Date, step: string, monthDayDiff?: boolean) => { + if (step === "HOUR" && monthDayDiff) { + return "2023-01-01"; + } + return "2023-01-01 00"; + }), +})); + +describe("useDuration hook", () => { + const mockAppStore = { + utc: false, + }; + + beforeEach(() => { + vi.clearAllMocks(); + (useAppStoreWithOut as any).mockReturnValue(mockAppStore); + }); + + describe("setDurationRow", () => { + it("should set duration row data", () => { + const { setDurationRow, getDurationTime } = useDuration(); + + const newDuration = { + start: new Date("2023-02-01 00:00:00"), + end: new Date("2023-02-02 00:00:00"), + step: "DAY", + }; + + setDurationRow(newDuration); + const result = getDurationTime(); + + expect(result.step).toBe("DAY"); + }); + }); + + describe("getDurationTime", () => { + it("should return formatted duration time", () => { + const { getDurationTime } = useDuration(); + + const result = getDurationTime(); + + expect(result).toEqual({ + start: "2023-01-01", + end: "2023-01-01", + step: "HOUR", + }); + }); + + it("should use app store UTC setting", () => { + const { getDurationTime } = useDuration(); + + getDurationTime(); + + expect(useAppStoreWithOut).toHaveBeenCalled(); + }); + }); + + describe("getMaxRange", () => { + it("should return empty array for day -1", () => { + const { getMaxRange } = useDuration(); + + const result = getMaxRange(-1); + + expect(result).toEqual([]); + }); + + it("should return date range for positive days", () => { + const { getMaxRange } = useDuration(); + + const result = getMaxRange(1); + + expect(result).toHaveLength(2); + expect(result[0]).toBeInstanceOf(Date); + expect(result[1]).toBeInstanceOf(Date); + expect(result[1].getTime()).toBeGreaterThan(result[0].getTime()); + }); + + it("should calculate correct time gap", () => { + const { getMaxRange } = useDuration(); + + const result = getMaxRange(2); + + // Should be approximately 3 days (2 + 1) * 24 * 60 * 60 * 1000 milliseconds + const expectedGap = 3 * 24 * 60 * 60 * 1000; + const actualGap = result[1].getTime() - result[0].getTime(); + + // Allow for small timing differences + expect(Math.abs(actualGap - expectedGap)).toBeLessThan(1000); + }); + + it("should return current time as end date", () => { + const { getMaxRange } = useDuration(); + + const before = new Date(); + const result = getMaxRange(1); + const after = new Date(); + + expect(result[1].getTime()).toBeGreaterThanOrEqual(before.getTime()); + expect(result[1].getTime()).toBeLessThanOrEqual(after.getTime()); + }); + }); + + describe("integration", () => { + it("should work with different duration configurations", () => { + const { setDurationRow, getDurationTime, getMaxRange } = useDuration(); + + // Set custom duration + const customDuration = { + start: new Date("2023-03-01 12:00:00"), + end: new Date("2023-03-02 12:00:00"), + step: "MINUTE", + }; + + setDurationRow(customDuration); + + // Test getDurationTime + const durationTime = getDurationTime(); + expect(durationTime.step).toBe("MINUTE"); + + // Test getMaxRange + const maxRange = getMaxRange(5); + expect(maxRange).toHaveLength(2); + expect(maxRange[0]).toBeInstanceOf(Date); + expect(maxRange[1]).toBeInstanceOf(Date); + }); + }); +}); diff --git a/src/store/modules/__tests__/app.spec.ts b/src/store/modules/__tests__/app.spec.ts new file mode 100644 index 00000000..8f0c1797 --- /dev/null +++ b/src/store/modules/__tests__/app.spec.ts @@ -0,0 +1,318 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { appStore } from "../app"; +import { TimeType, Themes } from "@/constants/data"; + +// Mock the utility functions +vi.mock("@/utils/localtime", () => ({ + default: vi.fn((utc: boolean, date: Date) => date), +})); + +vi.mock("@/utils/dateFormat", () => ({ + default: vi.fn((date: Date, step: string, monthDayDiff?: boolean) => { + if (step === "MINUTE" && monthDayDiff) { + return "2023-01-01 12:00"; + } + return "2023-01-01 12:00"; + }), + dateFormatTime: vi.fn((date: Date, step: string) => { + if (step === "MINUTE") { + return "12:00\n01-01"; + } + return "2023-01-01"; + }), +})); + +// Mock graphql +vi.mock("@/graphql", () => ({ + default: { + query: vi.fn(() => ({ + params: vi.fn(() => Promise.resolve({ data: { getMenuItems: [] } })), + })), + }, +})); + +describe("App Store", () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + vi.useFakeTimers(); + }); + + describe("State", () => { + it("should initialize with default state", () => { + const store = appStore(); + + expect(store.utc).toBe(""); + expect(store.utcHour).toBe(0); + expect(store.utcMin).toBe(0); + expect(store.eventStack).toEqual([]); + expect(store.timer).toBeNull(); + expect(store.autoRefresh).toBe(false); + expect(store.version).toBe(""); + expect(store.isMobile).toBe(false); + expect(store.reloadTimer).toBeNull(); + expect(store.allMenus).toEqual([]); + expect(store.theme).toBe(Themes.Dark); + expect(store.coldStageMode).toBe(false); + expect(store.maxRange).toEqual([]); + expect(store.metricsTTL).toEqual({}); + expect(store.recordsTTL).toEqual({}); + }); + + it("should have correct duration row initialization", () => { + const store = appStore(); + + expect(store.durationRow.start).toBeInstanceOf(Date); + expect(store.durationRow.end).toBeInstanceOf(Date); + expect(store.durationRow.step).toBe(TimeType.MINUTE_TIME); + }); + }); + + describe("Getters", () => { + it("should return correct duration", () => { + const store = appStore(); + + const duration = store.duration; + + expect(duration.start).toBeInstanceOf(Date); + expect(duration.end).toBeInstanceOf(Date); + expect(duration.step).toBe(TimeType.MINUTE_TIME); + }); + + it("should return correct duration time", () => { + const store = appStore(); + + const durationTime = store.durationTime; + + expect(durationTime.start).toBe("2023-01-01 12:00"); + expect(durationTime.end).toBe("2023-01-01 12:00"); + expect(durationTime.step).toBe(TimeType.MINUTE_TIME); + }); + + it("should calculate interval unix correctly for MINUTE", () => { + const store = appStore(); + + const intervals = store.intervalUnix; + + expect(Array.isArray(intervals)).toBe(true); + expect(intervals.length).toBeGreaterThan(0); + }); + + it("should calculate interval unix correctly for HOUR", () => { + const store = appStore(); + store.durationRow.step = "HOUR"; + + const intervals = store.intervalUnix; + + expect(Array.isArray(intervals)).toBe(true); + expect(intervals.length).toBeGreaterThan(0); + }); + + it("should calculate interval unix correctly for DAY", () => { + const store = appStore(); + store.durationRow.step = "DAY"; + + const intervals = store.intervalUnix; + + expect(Array.isArray(intervals)).toBe(true); + expect(intervals.length).toBeGreaterThan(0); + }); + + it("should return correct interval time", () => { + const store = appStore(); + + const intervalTime = store.intervalTime; + + expect(Array.isArray(intervalTime)).toBe(true); + expect(intervalTime.length).toBeGreaterThan(0); + }); + }); + + describe("Actions", () => { + it("should set duration correctly", () => { + const store = appStore(); + const newDuration = { + start: new Date("2023-01-01"), + end: new Date("2023-01-02"), + step: "HOUR", + }; + + store.setDuration(newDuration); + + expect(store.durationRow).toEqual(newDuration); + }); + + it("should update duration row correctly", () => { + const store = appStore(); + const newDuration = { + start: new Date("2023-02-01"), + end: new Date("2023-02-02"), + step: "DAY", + }; + + store.updateDurationRow(newDuration); + + expect(store.durationRow).toEqual(newDuration); + }); + + it("should set max range correctly", () => { + const store = appStore(); + const maxRange = [new Date("2023-01-01"), new Date("2023-01-02")]; + + store.setMaxRange(maxRange); + + expect(store.maxRange).toEqual(maxRange); + }); + + it("should set theme correctly", () => { + const store = appStore(); + + store.setTheme(Themes.Light); + + expect(store.theme).toBe(Themes.Light); + }); + + it("should set UTC correctly", () => { + const store = appStore(); + + store.setUTC(5, 30); + + expect(store.utcHour).toBe(5); + expect(store.utcMin).toBe(30); + expect(store.utc).toBe("5:30"); + }); + + it("should update UTC correctly", () => { + const store = appStore(); + + store.updateUTC("3:45"); + + expect(store.utc).toBe("3:45"); + }); + + it("should set mobile mode correctly", () => { + const store = appStore(); + + store.setIsMobile(true); + + expect(store.isMobile).toBe(true); + }); + + it("should set event stack correctly", () => { + const store = appStore(); + const eventStack = [vi.fn()]; + + store.setEventStack(eventStack); + + expect(store.eventStack).toEqual(eventStack); + }); + + it("should set auto refresh correctly", () => { + const store = appStore(); + + store.setAutoRefresh(true); + + expect(store.autoRefresh).toBe(true); + }); + + it("should set cold stage mode correctly", () => { + const store = appStore(); + + store.setColdStageMode(true); + + expect(store.coldStageMode).toBe(true); + }); + + it("should run event stack with timer", () => { + const store = appStore(); + const mockEvent = vi.fn(); + store.eventStack = [mockEvent]; + + store.runEventStack(); + + vi.advanceTimersByTime(500); + + expect(mockEvent).toHaveBeenCalled(); + }); + + it("should set reload timer correctly", () => { + const store = appStore(); + const mockTimer = setInterval(() => { + // Mock callback for timer + }, 1000); + + store.setReloadTimer(mockTimer); + + expect(store.reloadTimer).toStrictEqual(mockTimer); + }); + }); + + describe("Async Actions", () => { + it("should get activate menus", async () => { + const store = appStore(); + + await store.getActivateMenus(); + + expect(store.allMenus).toEqual([]); + }); + + it("should query OAP time info", async () => { + const store = appStore(); + + await store.queryOAPTimeInfo(); + + // Should set default UTC if there are errors + expect(store.utc).toBeDefined(); + }); + + it("should fetch version", async () => { + const store = appStore(); + + await store.fetchVersion(); + + expect(store.version).toBeDefined(); + }); + + it("should query menu items", async () => { + const store = appStore(); + + const result = await store.queryMenuItems(); + + expect(result).toBeDefined(); + }); + + it("should query metrics TTL", async () => { + const store = appStore(); + + await store.queryMetricsTTL(); + + expect(store.metricsTTL).toBeDefined(); + }); + + it("should query records TTL", async () => { + const store = appStore(); + + await store.queryRecordsTTL(); + + expect(store.recordsTTL).toBeDefined(); + }); + }); +}); diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts index 58a1d4bf..8ae6d9ec 100644 --- a/src/store/modules/app.ts +++ b/src/store/modules/app.ts @@ -163,13 +163,25 @@ export const appStore = defineStore({ if (this.timer) { clearTimeout(this.timer); } - this.timer = setTimeout( - () => - this.eventStack.forEach((event: Function) => { - setTimeout(event(), 0); - }), - 500, - ); + this.timer = setTimeout(() => { + // Use requestIdleCallback if available for better performance, otherwise use setTimeout + const executeEvents = async () => { + for (const event of this.eventStack) { + try { + await Promise.resolve(event()); + } catch (error) { + console.error("Error executing event in eventStack:", error); + } + } + }; + + if (typeof requestIdleCallback !== "undefined") { + // Execute during idle time to avoid blocking the main thread + requestIdleCallback(() => executeEvents(), { timeout: 1000 }); + } else { + executeEvents(); + } + }, 500); }, async getActivateMenus() { const resp = (await this.queryMenuItems()) || {}; @@ -198,7 +210,7 @@ export const appStore = defineStore({ if (res.errors) { this.utc = -(new Date().getTimezoneOffset() / 60) + ":0"; } else { - this.utc = res.data.getTimeInfo.timezone / 100 + ":0"; + this.utc = res.data.getTimeInfo?.timezone / 100 + ":0"; } const utcArr = this.utc.split(":"); this.utcHour = isNaN(Number(utcArr[0])) ? 0 : Number(utcArr[0]); @@ -211,7 +223,7 @@ export const appStore = defineStore({ if (res.errors) { return res; } - this.version = res.data.version; + this.version = res.data.version || ""; return res.data; }, async queryMenuItems() { @@ -227,7 +239,7 @@ export const appStore = defineStore({ if (response.errors) { return response; } - this.metricsTTL = response.data.getMetricsTTL; + this.metricsTTL = response.data.getMetricsTTL || {}; return response.data; }, async queryRecordsTTL() { @@ -235,7 +247,7 @@ export const appStore = defineStore({ if (res.errors) { return res; } - this.recordsTTL = res.data.getRecordsTTL; + this.recordsTTL = res.data.getRecordsTTL || {}; return res.data; }, setReloadTimer(timer: IntervalHandle) { diff --git a/src/test/runner.ts b/src/test/runner.ts new file mode 100644 index 00000000..96d1f490 --- /dev/null +++ b/src/test/runner.ts @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Test patterns for different categories +export const testPatterns = { + utils: "src/utils/**/*.spec.ts", + components: "src/components/**/*.spec.ts", + hooks: "src/hooks/**/*.spec.ts", + stores: "src/store/**/*.spec.ts", + views: "src/views/**/*.spec.ts", + integration: "src/**/*.spec.ts", +}; + +// Test configuration for different categories +export const testConfigs = { + utils: { + pattern: testPatterns.utils, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: ["node_modules/", "src/test/", "**/*.d.ts"], + }, + }, + components: { + pattern: testPatterns.components, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: ["node_modules/", "src/test/", "**/*.d.ts"], + }, + }, + hooks: { + pattern: testPatterns.hooks, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: ["node_modules/", "src/test/", "**/*.d.ts"], + }, + }, + stores: { + pattern: testPatterns.stores, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: ["node_modules/", "src/test/", "**/*.d.ts"], + }, + }, + all: { + pattern: testPatterns.integration, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: [ + "node_modules/", + "src/test/", + "**/*.d.ts", + "**/*.config.*", + "dist/", + "cypress/", + "src/types/", + "src/mock/", + ], + }, + }, +}; diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 00000000..59eb1101 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { config } from "@vue/test-utils"; +import { vi, beforeAll, afterAll } from "vitest"; +import ElementPlus from "element-plus"; +import "element-plus/dist/index.css"; + +// Mock window.matchMedia +Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock ResizeObserver +global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); + +// Mock IntersectionObserver +global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); + +// Mock requestAnimationFrame +global.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => { + const id = setTimeout(cb, 0); + return id as unknown as number; +}); +global.cancelAnimationFrame = vi.fn(); + +// Configure Vue Test Utils +config.global.plugins = [ElementPlus]; + +// Mock console methods to reduce noise in tests +const originalConsole = { ...console }; +beforeAll(() => { + console.warn = vi.fn(); + console.error = vi.fn(); +}); + +afterAll(() => { + console.warn = originalConsole.warn; + console.error = originalConsole.error; +}); diff --git a/src/test/utils/index.ts b/src/test/utils/index.ts new file mode 100644 index 00000000..28257d93 --- /dev/null +++ b/src/test/utils/index.ts @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mount, VueWrapper } from "@vue/test-utils"; +import { createPinia, setActivePinia } from "pinia"; +import { createApp } from "vue"; +import { vi } from "vitest"; +import type { ComponentPublicInstance } from "vue"; + +export function createTestApp() { + const app = createApp({}); + const pinia = createPinia(); + app.use(pinia); + setActivePinia(pinia); + return { app, pinia }; +} + +export function mountComponent(component: T, options: any = {}): VueWrapper { + const { pinia } = createTestApp(); + + return mount(component as any, { + global: { + plugins: [pinia], + ...options.global, + }, + ...options, + }); +} + +export function createMockStore(storeName: string, initialState: any = {}) { + return { + [storeName]: { + ...initialState, + $patch: vi.fn(), + $reset: vi.fn(), + $dispose: vi.fn(), + }, + }; +} + +export function waitForNextTick() { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +export function createMockElement(className: string, textContent: string = "") { + const element = document.createElement("div"); + element.className = className; + element.textContent = textContent; + return element; +} + +export function createMockEvent(type: string, options: any = {}) { + return new Event(type, options); +} + +export function createMockMouseEvent(type: string, options: any = {}) { + return new MouseEvent(type, { + bubbles: true, + cancelable: true, + ...options, + }); +} + +export function createMockKeyboardEvent(type: string, options: any = {}) { + return new KeyboardEvent(type, { + bubbles: true, + cancelable: true, + ...options, + }); +} diff --git a/src/utils/__tests__/copy.spec.ts b/src/utils/__tests__/copy.spec.ts new file mode 100644 index 00000000..3c4057f5 --- /dev/null +++ b/src/utils/__tests__/copy.spec.ts @@ -0,0 +1,174 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import copy from "../copy"; +import { ElNotification } from "element-plus"; + +// Mock Element Plus +vi.mock("element-plus", () => ({ + ElNotification: vi.fn(), +})); + +// Mock navigator.clipboard +const mockClipboard = { + writeText: vi.fn(), +}; + +describe("copy utility function", () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock navigator.clipboard + Object.defineProperty(navigator, "clipboard", { + value: mockClipboard, + writable: true, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should copy text successfully and show success notification", async () => { + const testText = "test text to copy"; + mockClipboard.writeText.mockResolvedValue(undefined); + + copy(testText); + + // Wait for promise to resolve + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(testText); + expect(ElNotification).toHaveBeenCalledWith({ + title: "Success", + message: "Copied", + type: "success", + }); + }); + + it("should handle clipboard error and show error notification", async () => { + const testText = "test text to copy"; + const errorMessage = "Clipboard permission denied"; + mockClipboard.writeText.mockRejectedValue(errorMessage); + + copy(testText); + + // Wait for promise to reject + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(testText); + expect(ElNotification).toHaveBeenCalledWith({ + title: "Error", + message: errorMessage, + type: "warning", + }); + }); + + it("should handle empty string", async () => { + const testText = ""; + mockClipboard.writeText.mockResolvedValue(undefined); + + copy(testText); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(""); + expect(ElNotification).toHaveBeenCalledWith({ + title: "Success", + message: "Copied", + type: "success", + }); + }); + + it("should handle long text", async () => { + const testText = "a".repeat(1000); + mockClipboard.writeText.mockResolvedValue(undefined); + + copy(testText); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(testText); + expect(ElNotification).toHaveBeenCalledWith({ + title: "Success", + message: "Copied", + type: "success", + }); + }); + + it("should handle special characters", async () => { + const testText = "!@#$%^&*()_+-=[]{}|;:,.<>?"; + mockClipboard.writeText.mockResolvedValue(undefined); + + copy(testText); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(testText); + expect(ElNotification).toHaveBeenCalledWith({ + title: "Success", + message: "Copied", + type: "success", + }); + }); + + it("should handle unicode characters", async () => { + const testText = "πŸš€πŸŒŸπŸŽ‰δΈ­ζ–‡ζ΅‹θ―•"; + mockClipboard.writeText.mockResolvedValue(undefined); + + copy(testText); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledWith(testText); + expect(ElNotification).toHaveBeenCalledWith({ + title: "Success", + message: "Copied", + type: "success", + }); + }); + + it("should handle multiple rapid calls", async () => { + const testText = "test text"; + mockClipboard.writeText.mockResolvedValue(undefined); + + copy(testText); + copy(testText); + copy(testText); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockClipboard.writeText).toHaveBeenCalledTimes(3); + expect(ElNotification).toHaveBeenCalledTimes(3); + }); + + it("should handle clipboard not available", async () => { + const testText = "test text"; + + // Remove clipboard from navigator + Object.defineProperty(navigator, "clipboard", { + value: undefined, + writable: true, + }); + + // Should not throw error + expect(() => { + copy(testText); + }).not.toThrow(); + }); +}); diff --git a/src/utils/__tests__/dateFormat.spec.ts b/src/utils/__tests__/dateFormat.spec.ts new file mode 100644 index 00000000..f394832d --- /dev/null +++ b/src/utils/__tests__/dateFormat.spec.ts @@ -0,0 +1,121 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from "vitest"; +import dateFormatStep, { dateFormatTime, dateFormat } from "../dateFormat"; + +describe("dateFormat utility functions", () => { + describe("dateFormatStep", () => { + // Use a fixed timezone to avoid timezone issues in tests + const testDate = new Date("2023-12-25T15:30:45.123"); + + it("should format MONTH step correctly", () => { + expect(dateFormatStep(testDate, "MONTH")).toBe("2023-12-25"); + expect(dateFormatStep(testDate, "MONTH", true)).toBe("2023-12"); + }); + + it("should format DAY step correctly", () => { + expect(dateFormatStep(testDate, "DAY")).toBe("2023-12-25"); + }); + + it("should format HOUR step correctly", () => { + expect(dateFormatStep(testDate, "HOUR")).toBe("2023-12-25 15"); + }); + + it("should format MINUTE step correctly", () => { + expect(dateFormatStep(testDate, "MINUTE")).toBe("2023-12-25 1530"); + }); + + it("should format SECOND step correctly", () => { + expect(dateFormatStep(testDate, "SECOND")).toBe("2023-12-25 153045"); + }); + + it("should handle single digit values correctly", () => { + const singleDigitDate = new Date("2023-01-05T09:05:03.123"); + expect(dateFormatStep(singleDigitDate, "MONTH")).toBe("2023-01-05"); + expect(dateFormatStep(singleDigitDate, "HOUR")).toBe("2023-01-05 09"); + expect(dateFormatStep(singleDigitDate, "MINUTE")).toBe("2023-01-05 0905"); + expect(dateFormatStep(singleDigitDate, "SECOND")).toBe("2023-01-05 090503"); + }); + + it("should return empty string for unknown step", () => { + expect(dateFormatStep(testDate, "UNKNOWN")).toBe(""); + }); + }); + + describe("dateFormatTime", () => { + const testDate = new Date("2023-12-25T15:30:45.123"); + + it("should format MONTH step correctly", () => { + expect(dateFormatTime(testDate, "MONTH")).toBe("2023-12"); + }); + + it("should format DAY step correctly", () => { + expect(dateFormatTime(testDate, "DAY")).toBe("12-25"); + }); + + it("should format HOUR step correctly", () => { + expect(dateFormatTime(testDate, "HOUR")).toBe("12-25 15"); + }); + + it("should format MINUTE step correctly", () => { + expect(dateFormatTime(testDate, "MINUTE")).toBe("15:30\n12-25"); + }); + + it("should handle single digit values correctly", () => { + const singleDigitDate = new Date("2023-01-05T09:05:03.123"); + expect(dateFormatTime(singleDigitDate, "MONTH")).toBe("2023-01"); + expect(dateFormatTime(singleDigitDate, "DAY")).toBe("01-05"); + expect(dateFormatTime(singleDigitDate, "HOUR")).toBe("01-05 09"); + expect(dateFormatTime(singleDigitDate, "MINUTE")).toBe("09:05\n01-05"); + }); + + it("should return empty string for unknown step", () => { + expect(dateFormatTime(testDate, "UNKNOWN")).toBe(""); + }); + }); + + describe("dateFormat", () => { + it("should format timestamp with default pattern", () => { + const timestamp = 1703521845123; + // Use a regex pattern to match the expected format regardless of timezone + expect(dateFormat(timestamp)).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + }); + + it("should format timestamp with custom pattern", () => { + const timestamp = 1703521845123; + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + expect(dateFormat(timestamp, "YYYY/MM/DD")).toBe(`${year}/${month}/${day}`); + expect(dateFormat(timestamp, "MM-DD-YYYY")).toBe(`${month}-${day}-${year}`); + // Use a regex pattern for time-based formats that might vary by timezone + expect(dateFormat(timestamp, "HH:mm")).toMatch(/^\d{2}:\d{2}$/); + }); + + it("should handle different timestamp formats", () => { + const timestamp1 = Date.now(); + const timestamp2 = new Date("2023-01-01").getTime(); + + expect(dateFormat(timestamp1)).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + // Use a regex pattern for time-based formats that might vary by timezone + expect(dateFormat(timestamp2)).toMatch(/^2023-01-01 \d{2}:\d{2}:\d{2}$/); + }); + }); +}); diff --git a/src/utils/__tests__/debounce.spec.ts b/src/utils/__tests__/debounce.spec.ts new file mode 100644 index 00000000..698162a7 --- /dev/null +++ b/src/utils/__tests__/debounce.spec.ts @@ -0,0 +1,108 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { debounce } from "../debounce"; + +describe("debounce utility function", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + it("should call the function only once after delay", () => { + const callback = vi.fn(); + const debouncedFn = debounce(callback, 1000); + + // Call multiple times + debouncedFn(); + debouncedFn(); + debouncedFn(); + + // Function should not be called immediately + expect(callback).not.toHaveBeenCalled(); + + // Fast forward time + vi.advanceTimersByTime(1000); + + // Function should be called only once + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should reset timer on subsequent calls", () => { + const callback = vi.fn(); + const debouncedFn = debounce(callback, 1000); + + // First call + debouncedFn(); + + // Advance time but not enough to trigger + vi.advanceTimersByTime(500); + expect(callback).not.toHaveBeenCalled(); + + // Second call should reset timer + debouncedFn(); + + // Advance time again + vi.advanceTimersByTime(500); + expect(callback).not.toHaveBeenCalled(); + + // Advance to trigger the function + vi.advanceTimersByTime(500); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should handle different delay durations", () => { + const callback = vi.fn(); + const debouncedFn = debounce(callback, 500); + + debouncedFn(); + + // Should not be called before delay + vi.advanceTimersByTime(499); + expect(callback).not.toHaveBeenCalled(); + + // Should be called after delay + vi.advanceTimersByTime(1); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should handle zero delay", () => { + const callback = vi.fn(); + const debouncedFn = debounce(callback, 0); + + debouncedFn(); + + // Should be called after a tick even with zero delay + vi.advanceTimersByTime(0); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should handle multiple rapid calls", () => { + const callback = vi.fn(); + const debouncedFn = debounce(callback, 100); + + // Rapid successive calls + for (let i = 0; i < 10; i++) { + debouncedFn(); + } + + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(100); + expect(callback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/__tests__/is.spec.ts b/src/utils/__tests__/is.spec.ts new file mode 100644 index 00000000..e7702eb4 --- /dev/null +++ b/src/utils/__tests__/is.spec.ts @@ -0,0 +1,257 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from "vitest"; +import { + is, + isDef, + isUnDef, + isObject, + isDate, + isNull, + isNullOrUnDef, + isNumber, + isString, + isFunction, + isBoolean, + isRegExp, + isArray, + isMap, + isEmptyObject, +} from "../is"; + +describe("is utility functions", () => { + describe("is", () => { + it("should return true for correct type checks", () => { + expect(is("string", "String")).toBe(true); + expect(is(123, "Number")).toBe(true); + expect(is({}, "Object")).toBe(true); + expect(is([], "Array")).toBe(true); + expect(is(new Date(), "Date")).toBe(true); + expect(is(/regex/, "RegExp")).toBe(true); + expect(is(true, "Boolean")).toBe(true); + }); + + it("should return false for incorrect type checks", () => { + expect(is("string", "Number")).toBe(false); + expect(is(123, "String")).toBe(false); + expect(is({}, "Array")).toBe(false); + }); + }); + + describe("isDef", () => { + it("should return true for defined values", () => { + expect(isDef("string")).toBe(true); + expect(isDef(0)).toBe(true); + expect(isDef(false)).toBe(true); + expect(isDef(null)).toBe(true); + }); + + it("should return false for undefined values", () => { + expect(isDef(undefined)).toBe(false); + }); + }); + + describe("isUnDef", () => { + it("should return true for undefined values", () => { + expect(isUnDef(undefined)).toBe(true); + }); + + it("should return false for defined values", () => { + expect(isUnDef("string")).toBe(false); + expect(isUnDef(0)).toBe(false); + expect(isUnDef(false)).toBe(false); + expect(isUnDef(null)).toBe(false); + }); + }); + + describe("isObject", () => { + it("should return true for objects", () => { + expect(isObject({})).toBe(true); + expect(isObject({ key: "value" })).toBe(true); + expect(isObject(new Object())).toBe(true); + }); + + it("should return false for non-objects", () => { + expect(isObject(null)).toBe(false); + expect(isObject([])).toBe(false); + expect(isObject("string")).toBe(false); + expect(isObject(123)).toBe(false); + expect(isObject(undefined)).toBe(false); + }); + }); + + describe("isDate", () => { + it("should return true for Date objects", () => { + expect(isDate(new Date())).toBe(true); + expect(isDate(new Date("2023-01-01"))).toBe(true); + }); + + it("should return false for non-Date values", () => { + expect(isDate("2023-01-01")).toBe(false); + expect(isDate(123)).toBe(false); + expect(isDate({})).toBe(false); + expect(isDate(null)).toBe(false); + }); + }); + + describe("isNull", () => { + it("should return true for null", () => { + expect(isNull(null)).toBe(true); + }); + + it("should return false for non-null values", () => { + expect(isNull(undefined)).toBe(false); + expect(isNull("string")).toBe(false); + expect(isNull(0)).toBe(false); + expect(isNull({})).toBe(false); + }); + }); + + describe("isNullOrUnDef", () => { + it("should return true for null or undefined", () => { + expect(isNullOrUnDef(null)).toBe(true); + expect(isNullOrUnDef(undefined)).toBe(true); + }); + + it("should return false for other values", () => { + expect(isNullOrUnDef("string")).toBe(false); + expect(isNullOrUnDef(0)).toBe(false); + expect(isNullOrUnDef({})).toBe(false); + }); + }); + + describe("isNumber", () => { + it("should return true for numbers", () => { + expect(isNumber(123)).toBe(true); + expect(isNumber(0)).toBe(true); + expect(isNumber(-123)).toBe(true); + expect(isNumber(3.14)).toBe(true); + }); + + it("should return false for non-numbers", () => { + expect(isNumber("123")).toBe(false); + expect(isNumber({})).toBe(false); + expect(isNumber(null)).toBe(false); + expect(isNumber(undefined)).toBe(false); + }); + }); + + describe("isString", () => { + it("should return true for strings", () => { + expect(isString("hello")).toBe(true); + expect(isString("")).toBe(true); + expect(isString(String("hello"))).toBe(true); + }); + + it("should return false for non-strings", () => { + expect(isString(123)).toBe(false); + expect(isString({})).toBe(false); + expect(isString(null)).toBe(false); + expect(isString(undefined)).toBe(false); + }); + }); + + describe("isFunction", () => { + it("should return true for functions", () => { + expect(isFunction(() => {})).toBe(true); + expect(isFunction(function () {})).toBe(true); + expect(isFunction(async () => {})).toBe(true); + }); + + it("should return false for non-functions", () => { + expect(isFunction("string")).toBe(false); + expect(isFunction(123)).toBe(false); + expect(isFunction({})).toBe(false); + expect(isFunction(null)).toBe(false); + }); + }); + + describe("isBoolean", () => { + it("should return true for booleans", () => { + expect(isBoolean(true)).toBe(true); + expect(isBoolean(false)).toBe(true); + expect(isBoolean(Boolean(true))).toBe(true); + }); + + it("should return false for non-booleans", () => { + expect(isBoolean("true")).toBe(false); + expect(isBoolean(1)).toBe(false); + expect(isBoolean({})).toBe(false); + expect(isBoolean(null)).toBe(false); + }); + }); + + describe("isRegExp", () => { + it("should return true for regular expressions", () => { + expect(isRegExp(/regex/)).toBe(true); + expect(isRegExp(new RegExp("regex"))).toBe(true); + }); + + it("should return false for non-regex values", () => { + expect(isRegExp("regex")).toBe(false); + expect(isRegExp({})).toBe(false); + expect(isRegExp(null)).toBe(false); + }); + }); + + describe("isArray", () => { + it("should return true for arrays", () => { + expect(isArray([])).toBe(true); + expect(isArray([1, 2, 3])).toBe(true); + expect(isArray(new Array())).toBe(true); + }); + + it("should return false for non-arrays", () => { + expect(isArray({})).toBe(false); + expect(isArray("string")).toBe(false); + expect(isArray(123)).toBe(false); + expect(isArray(null)).toBe(false); + }); + }); + + describe("isMap", () => { + it("should return true for Map objects", () => { + expect(isMap(new Map())).toBe(true); + expect(isMap(new Map([["key", "value"]]))).toBe(true); + }); + + it("should return false for non-Map objects", () => { + expect(isMap({})).toBe(false); + expect(isMap([])).toBe(false); + expect(isMap(null)).toBe(false); + }); + }); + + describe("isEmptyObject", () => { + it("should return true for empty objects", () => { + expect(isEmptyObject({})).toBe(true); + }); + + it("should return false for non-empty objects", () => { + expect(isEmptyObject({ key: "value" })).toBe(false); + expect(isEmptyObject({ length: 0 })).toBe(false); + }); + + it("should return false for non-objects", () => { + expect(isEmptyObject([])).toBe(false); + expect(isEmptyObject("string")).toBe(false); + expect(isEmptyObject(123)).toBe(false); + expect(isEmptyObject(null)).toBe(false); + }); + }); +}); diff --git a/src/utils/copy.ts b/src/utils/copy.ts index d593e79f..454588e4 100644 --- a/src/utils/copy.ts +++ b/src/utils/copy.ts @@ -18,6 +18,10 @@ import { ElNotification } from "element-plus"; export default (text: string): void => { + if (!navigator.clipboard) { + console.error("Clipboard is not supported"); + return; + } navigator.clipboard .writeText(text) .then(() => { diff --git a/src/utils/is.ts b/src/utils/is.ts index c7521872..ce5c23a3 100644 --- a/src/utils/is.ts +++ b/src/utils/is.ts @@ -40,10 +40,6 @@ export function isNull(val: unknown): val is null { return val === null; } -export function isNullAndUnDef(val: unknown): val is null | undefined { - return isUnDef(val) && isNull(val); -} - export function isNullOrUnDef(val: unknown): val is null | undefined { return isUnDef(val) || isNull(val); } @@ -52,10 +48,6 @@ export function isNumber(val: unknown): val is number { return is(val, "Number"); } -export function isPromise(val: unknown): val is Promise { - return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch); -} - export function isString(val: unknown): val is string { return is(val, "String"); } @@ -76,14 +68,6 @@ export function isArray(val: unknown): boolean { return Array.isArray(val); } -export function isWindow(val: unknown): val is Window { - return typeof window !== "undefined" && is(val, "Window"); -} - -export function isElement(val: unknown): val is Element { - return isObject(val) && !!val.tagName; -} - export function isMap(val: unknown): val is Map { return is(val, "Map"); } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..22205b6f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from "vitest/config"; +import vue from "@vitejs/plugin-vue"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import path from "path"; +import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; + +export default defineConfig({ + plugins: [ + vue(), + vueJsx(), + createSvgIconsPlugin({ + iconDirs: [path.resolve(__dirname, "./src/assets/icons")], + symbolId: "[name]", + }), + ], + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./src/test/setup.ts"], + deps: { + // vite-plugin-svg-icons uses non-standard exports and needs to be inlined + // to ensure correct module resolution during testing with Vitest. + inline: ["vite-plugin-svg-icons"], + }, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + exclude: [ + "node_modules/", + "src/test/", + "**/*.d.ts", + "**/*.config.*", + "dist/", + "cypress/", + "src/types/", + "src/mock/", + ], + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});