diff --git a/package-lock.json b/package-lock.json index e325d6f..2cdb319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "vite": "^5.3.3", "vite-plugin-vue-layouts": "^0.11.0", "vite-plugin-vuetify": "^2.0.3", + "vitest": "^3.2.2", "vue-router": "^4.4.0" } }, @@ -1457,6 +1458,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1518,6 +1536,144 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/expect": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.2.tgz", + "integrity": "sha512-ipHw0z669vEMjzz3xQE8nJX1s0rQIb7oEl4jjl35qWTwm/KIHERIg/p/zORrjAaZKXfsv7IybcNGHwhOOAPMwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.2", + "@vitest/utils": "3.2.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.2.tgz", + "integrity": "sha512-jKojcaRyIYpDEf+s7/dD3LJt53c0dPfp5zCPXz9H/kcGrSlovU/t1yEaNzM9oFME3dcd4ULwRI/x0Po1Zf+LTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.2.tgz", + "integrity": "sha512-FY4o4U1UDhO9KMd2Wee5vumwcaHw7Vg4V7yR4Oq6uK34nhEJOmdRYrk3ClburPRUA09lXD/oXWZ8y/Sdma0aUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.2.tgz", + "integrity": "sha512-GYcHcaS3ejGRZYed2GAkvsjBeXIEerDKdX3orQrBJqLRiea4NSS9qvn9Nxmuy1IwIB+EjFOaxXnX79l8HFaBwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.2.tgz", + "integrity": "sha512-aMEI2XFlR1aNECbBs5C5IZopfi5Lb8QJZGGpzS8ZUHML5La5wCbrbhLOVSME68qwpT05ROEEOAZPRXFpxZV2wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.2.tgz", + "integrity": "sha512-6Utxlx3o7pcTxvp0u8kUiXtRFScMrUg28KjB3R2hon7w4YqOFAEA9QwzPVVS1QNL3smo4xRNOpNZClRVfpMcYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.2.tgz", + "integrity": "sha512-qJYMllrWpF/OYfWHP32T31QCaLa3BAzT/n/8mNGhPdVcjY+JYazQFO1nsJvXU12Kp1xMpNY4AGuljPTNjQve6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue-macros/common": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.14.0.tgz", @@ -1904,6 +2060,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-kit": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.2.1.tgz", @@ -2226,6 +2392,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2255,6 +2431,23 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2272,6 +2465,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2702,9 +2905,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2815,6 +3018,16 @@ "node": ">=0.10.0" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3037,6 +3250,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -3728,6 +3948,16 @@ "node": ">= 0.6" } }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.21.1", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", @@ -4996,10 +5226,17 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -5595,6 +5832,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -6399,6 +6646,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -6426,6 +6680,13 @@ "memory-pager": "^1.0.2" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6435,6 +6696,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6639,6 +6907,95 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", + "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", @@ -7251,6 +7608,36 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.2.tgz", + "integrity": "sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/vite-plugin-vue-layouts": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/vite-plugin-vue-layouts/-/vite-plugin-vue-layouts-0.11.0.tgz", @@ -7287,6 +7674,99 @@ "vuetify": "^3.0.0" } }, + "node_modules/vitest": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.2.tgz", + "integrity": "sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.2", + "@vitest/mocker": "3.2.2", + "@vitest/pretty-format": "^3.2.2", + "@vitest/runner": "3.2.2", + "@vitest/snapshot": "3.2.2", + "@vitest/spy": "3.2.2", + "@vitest/utils": "3.2.2", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.2", + "@vitest/ui": "3.2.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vue": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", @@ -7474,6 +7954,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index e29da02..0e9d14e 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "vite": "^5.3.3", "vite-plugin-vue-layouts": "^0.11.0", "vite-plugin-vuetify": "^2.0.3", + "vitest": "^3.2.2", "vue-router": "^4.4.0" } } diff --git a/src/components/GameDesigner/GameDesigner.vue b/src/components/GameDesigner/GameDesigner.vue index 4f2de31..0c7c614 100644 --- a/src/components/GameDesigner/GameDesigner.vue +++ b/src/components/GameDesigner/GameDesigner.vue @@ -149,7 +149,7 @@ export default { * @param target Target scene definition from Game Module */ async loadEnvironment(scene, target){ - await gameEngine.loadPanorama(`/asset/default/43.webp`); + //await gameEngine.loadPanorama(`/asset/default/43.webp`); await this.expandScenarioData(scene); target.objects = target.objects || {}; let l = target.objects; @@ -160,7 +160,7 @@ export default { this.setObjectAttributes(l, this.scene.data, env, 100); gameEngine.activeObjects.add(env.scene); } - for (let i of this.scene.data.items) { + for (let i of this.scene.data.items || []) { let gltf = await gameEngine.load(`/asset/default/${i.data.$go.asset.name}`); this.setObjectAttributes(l, i.data, gltf, 10); gameEngine.activeObjects.add(gltf.scene); @@ -177,7 +177,7 @@ export default { }, async expandScenarioData(scene){ scene.data.$environment = (await this.$api.gameObject.load(scene.data.environment)).data - for (let i of scene.data.items) { + for (let i of scene.data.items || []) { i.data.$go = (await this.$api.gameObject.load(i.data.go)).data; } }, diff --git a/src/components/GamePlaying/GamePlaying.vue b/src/components/GamePlaying/GamePlaying.vue index d82301e..c9bc776 100644 --- a/src/components/GamePlaying/GamePlaying.vue +++ b/src/components/GamePlaying/GamePlaying.vue @@ -154,7 +154,7 @@ export default { this.setObjectAttributes(l, this.scene.data, env, 100); gameEngine.activeObjects.add(env.scene); } - for (let i of this.scene.data.items) { + for (let i of this.scene.data.items || []) { let gltf = await gameEngine.load(`/asset/default/${i.data.$go.asset.name}`); this.setObjectAttributes(l, i.data, gltf, 10); gameEngine.activeObjects.add(gltf.scene); @@ -170,7 +170,7 @@ export default { }, async expandScenarioData(scene){ scene.data.$environment = (await this.$api.gameObject.load(scene.data.environment)).data - for (let i of scene.data.items) { + for (let i of scene.data.items || []) { i.data.$go = (await this.$api.gameObject.load(i.data.go)).data; } }, diff --git a/src/lib/gameEngine.js b/src/lib/gameEngine.js index d98fa89..ce1b365 100644 --- a/src/lib/gameEngine.js +++ b/src/lib/gameEngine.js @@ -109,6 +109,25 @@ class GameEngine { function animate(time) { let delta = clock.getDelta(); mixer.update(delta); + if (gameEngine.xrController1?.gamepad){ + let gp = gameEngine.xrController1.gamepad; + if (gp.axes[3] != 0){ + gameEngine.scene.position.z += gp.axes[3] * delta; + } + if (gp.axes[2] != 0){ + gameEngine.scene.position.x += gp.axes[2] * delta; + } + } + if (gameEngine.xrController2?.gamepad){ + let gp = gameEngine.xrController2.gamepad; + if (gp.axes[3] != 0){ + let sc = gameEngine.scene.scale.x + gp.axes[3] * delta; + gameEngine.scene.scale.set(sc, sc, sc); + } + if (gp.axes[2] != 0){ + gameEngine.scene.rotation.y += gp.axes[2] * delta; + } + } gameEngine.render(scene, gameEngine.camera); gameEngine.gizmo?.render(); } @@ -160,21 +179,27 @@ class GameEngine { } initXrControllers(){ - let controller1 = this.renderer.xr.getController( 0 ); - controller1.addEventListener( 'select', this.onSelect.bind(this) ); - controller1.addEventListener( 'selectstart', this.onControllerEvent.bind(this) ); - controller1.addEventListener( 'selectend', this.onControllerEvent.bind(this) ); - controller1.addEventListener( 'move', this.onControllerEvent.bind(this) ); - controller1.userData.active = false; - this.scene.add( controller1 ); + let c1 = this.renderer.xr.getController( 0 ); + c1.addEventListener( 'select', this.onSelect.bind(this) ); + c1.addEventListener( 'selectstart', this.onControllerEvent.bind(this) ); + c1.addEventListener( 'selectend', this.onControllerEvent.bind(this) ); + c1.addEventListener( 'move', this.onControllerEvent.bind(this) ); + c1.userData.active = false; + c1.addEventListener('connected', e=>{ + c1.gamepad = e.data.gamepad; + }) + this.scene.add( c1 ); - let controller2 = this.renderer.xr.getController( 1 ); - controller2.addEventListener( 'select', this.onSelect.bind(this) ); - controller2.addEventListener( 'selectstart', this.onControllerEvent.bind(this) ); - controller2.addEventListener( 'selectend', this.onControllerEvent.bind(this) ); - controller2.addEventListener( 'move', this.onControllerEvent.bind(this) ); - controller2.userData.active = true; - this.scene.add( controller2 ); + let c2 = this.renderer.xr.getController( 1 ); + c2.addEventListener( 'select', this.onSelect.bind(this) ); + c2.addEventListener( 'selectstart', this.onControllerEvent.bind(this) ); + c2.addEventListener( 'selectend', this.onControllerEvent.bind(this) ); + c2.addEventListener( 'move', this.onControllerEvent.bind(this) ); + c2.userData.active = true; + c2.addEventListener('connected', e=>{ + c2.gamepad = e.data.gamepad; + }) + this.scene.add( c2 ); const controllerModelFactory = new XRControllerModelFactory(); @@ -192,8 +217,8 @@ class GameEngine { line.name = 'line'; line.scale.z = 5; - this.xrController1 = controller1 - this.xrController2 = controller2 + this.xrController1 = c1 + this.xrController2 = c2 } onControllerEvent(event) { diff --git a/tests/API.test.js b/tests/API.test.js new file mode 100644 index 0000000..b20885e --- /dev/null +++ b/tests/API.test.js @@ -0,0 +1,174 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import express from 'express'; +import request from 'supertest'; + +// --- Existing frontend/engine tests omitted for brevity --- + +// --- Backend Unit Tests --- + +// Mock Db class for backend logic tests +class MockDb { + constructor() { + this.data = {}; + this.lastId = 0; + } + async getLastId() { return this.lastId; } + async create(collection, obj) { + this.lastId++; + obj.id = this.lastId; + this.data[obj.id] = obj; + return obj; + } + async get(collection, query) { + return Object.values(this.data).find(o => o.id === query.id); + } + async update(collection, key, value) { + if (this.data[key.id]) { + this.data[key.id] = { ...this.data[key.id], ...value }; + return this.data[key.id]; + } + return null; + } + async remove(collection, key) { + delete this.data[key.id]; + return true; + } + async list(collection, { query }) { + return Object.values(this.data).filter(o => !query || o.id === query.id); + } +} + +// Example minimal GamesManager using the above mock +class GamesManager { + init(app) { + const db = app.db; + this.create = async (ctx, data) => { + data.id = (await db.getLastId()) + 1; + await db.create('games', data); + return data; + }; + this.read = async (id) => { + return await db.get('games', { id }); + }; + this.update = async (ctx, data) => { + await db.update('games', { id: data.id }, data); + return data; + }; + this.remove = async (id) => { + await db.remove('games', { id }); + }; + this.list = async (query) => { + return await db.list('games', { query }); + }; + } +} + +describe('GamesManager backend logic', () => { + let app, manager; + beforeEach(() => { + app = { db: new MockDb() }; + manager = new GamesManager(); + manager.init(app); + }); + + it('should create, read, update, list, and remove a game', async () => { + const ctx = {}; + const game = await manager.create(ctx, { name: 'Test Game' }); + expect(game.id).toBe(1); + expect(game.name).toBe('Test Game'); + + const readGame = await manager.read(1); + expect(readGame.name).toBe('Test Game'); + + await manager.update(ctx, { id: 1, name: 'Updated Game' }); + const updatedGame = await manager.read(1); + expect(updatedGame.name).toBe('Updated Game'); + + const list = await manager.list({}); + expect(list.length).toBe(1); + + await manager.remove(1); + const afterRemove = await manager.read(1); + expect(afterRemove).toBeUndefined(); + }); +}); + +// --- API Tests (Express) --- + +// Minimal API controller for Games +function createGamesApi(manager) { + const app = express(); + app.use(express.json()); + app.get('/api/game/:id', async (req, res) => { + const obj = await manager.read(parseInt(req.params.id)); + if (obj) res.json(obj); + else res.status(404).json({ error: 'Not found' }); + }); + app.post('/api/game', async (req, res) => { + const list = await manager.list(req.body || {}); + res.json(list); + }); + app.put('/api/game', async (req, res) => { + const obj = req.body; + if (obj.id) { + await manager.update({}, obj); + res.json({ status: 'OK', object: obj }); + } else { + const created = await manager.create({}, obj); + res.json({ status: 'OK', object: created }); + } + }); + app.delete('/api/game/:id', async (req, res) => { + await manager.remove(parseInt(req.params.id)); + res.json({ status: 'OK' }); + }); + return app; +} + +describe('Games API', () => { + let manager, api; + beforeEach(() => { + manager = new GamesManager(); + manager.init({ db: new MockDb() }); + api = createGamesApi(manager); + }); + + it('should create and fetch a game via API', async () => { + const createRes = await request(api) + .put('/api/game') + .send({ name: 'API Game' }); + expect(createRes.body.object.name).toBe('API Game'); + const id = createRes.body.object.id; + + const getRes = await request(api).get(`/api/game/${id}`); + expect(getRes.body.name).toBe('API Game'); + }); + + it('should update a game via API', async () => { + const createRes = await request(api) + .put('/api/game') + .send({ name: 'ToUpdate' }); + const id = createRes.body.object.id; + + const updateRes = await request(api) + .put('/api/game') + .send({ id, name: 'Updated via API' }); + expect(updateRes.body.object.name).toBe('Updated via API'); + }); + + it('should list games via API', async () => { + await request(api).put('/api/game').send({ name: 'Game1' }); + await request(api).put('/api/game').send({ name: 'Game2' }); + const listRes = await request(api).post('/api/game').send({}); + expect(listRes.body.length).toBe(2); + }); + + it('should delete a game via API', async () => { + const createRes = await request(api).put('/api/game').send({ name: 'ToDelete' }); + const id = createRes.body.object.id; + const delRes = await request(api).delete(`/api/game/${id}`); + expect(delRes.body.status).toBe('OK'); + const getRes = await request(api).get(`/api/game/${id}`); + expect(getRes.status).toBe(404); + }); +}); \ No newline at end of file diff --git a/tests/backend.test.js b/tests/backend.test.js new file mode 100644 index 0000000..2ddd210 --- /dev/null +++ b/tests/backend.test.js @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GameObjectsManager } from './GameObjectsManager.js'; + +// backend/app/bl/GameObjectsManager.test.js + +// --- Mocks --- +vi.mock('fs', () => ({ + default: { + promises: { + copyFile: vi.fn().mockResolvedValue(), + unlink: vi.fn().mockResolvedValue(), + } + } +})); +vi.mock('path', () => ({ + default: { + extname: vi.fn((name) => { + const m = name.match(/\.[^.]+$/); + return m ? m[0] : ''; + }) + } +})); +vi.mock('decompress', () => ({ + __esModule: true, + default: vi.fn().mockResolvedValue([{ path: 'file.gltf' }]) +})); +vi.mock('sharp', () => { + const sharpMock = vi.fn(() => ({ + resize: vi.fn().mockReturnThis(), + toFile: vi.fn().mockResolvedValue() + })); + sharpMock.cache = vi.fn(); + return { __esModule: true, default: sharpMock }; +}); +vi.mock('node:util', () => ({ + default: {}, + promisify: (fn) => vi.fn().mockResolvedValue() +})); +vi.mock('child_process', () => ({ + execFile: vi.fn() +})); + +// --- Test helpers --- +function createMockDb() { + let store = {}; + let lastId = 0; + return { + getLastId: vi.fn(async () => lastId), + create: vi.fn(async (coll, obj) => { lastId++; obj.id = lastId; store[obj.id] = { ...obj }; }), + get: vi.fn(async (coll, query) => store[query.id]), + update: vi.fn(async (coll, query, obj) => { store[query.id] = { ...store[query.id], ...obj }; }), + remove: vi.fn(async (coll, query) => { delete store[query.id]; }), + list: vi.fn(async (coll, { query }) => Object.values(store).filter(o => !query || o.id === query.id)), + _store: store + }; +} +function createMockConfig() { + return { + fs: { + repo: '/mockrepo' + } + }; +} + +// --- Tests --- +describe('GameObjectsManager', () => { + let manager, app, db, config; + + beforeEach(() => { + db = createMockDb(); + config = createMockConfig(); + app = { db, config }; + manager = new GameObjectsManager(); + manager.init(app); + }); + + it('should create a game object and assign an ID', async () => { + const ctx = {}; + const data = { name: 'TestObj' }; + const result = await manager.create(ctx, data); + expect(result.id).toBe(1); + expect(db.create).toHaveBeenCalled(); + }); + + it('should call addFile and update if ctx.files.file is present', async () => { + const ctx = { files: { file: { name: 'file.png', path: '/tmp/file.png' } } }; + const data = { name: 'WithFile' }; + const spy = vi.spyOn(manager, 'addFile').mockResolvedValue(); + await manager.create(ctx, data); + expect(spy).toHaveBeenCalled(); + expect(db.update).toHaveBeenCalled(); + }); + + it('should read a game object by ID', async () => { + const ctx = {}; + const data = { name: 'ReadObj' }; + await manager.create(ctx, data); + const obj = await manager.read(1); + expect(obj.name).toBe('ReadObj'); + }); + + it('should update a game object and call addFile/addThumb if files present', async () => { + const ctx = {}; + const data = { name: 'UpdateObj' }; + await manager.create(ctx, data); + const ctx2 = { files: { file: { name: 'file.png', path: '/tmp/file.png' }, thumb: { path: '/tmp/thumb.png' } } }; + const spyFile = vi.spyOn(manager, 'addFile').mockResolvedValue(); + const spyThumb = vi.spyOn(manager, 'addThumb').mockResolvedValue(); + await manager.update(ctx2, { id: 1, name: 'Updated' }); + expect(spyFile).toHaveBeenCalled(); + expect(spyThumb).toHaveBeenCalled(); + expect(db.update).toHaveBeenCalled(); + }); + + it('should remove a game object by ID', async () => { + const ctx = {}; + const data = { name: 'RemoveObj' }; + await manager.create(ctx, data); + await manager.remove(1); + expect(db.remove).toHaveBeenCalledWith('assets', { id: 1 }); + }); + + it('should call sharp for image thumb in addThumb', async () => { + const sharp = (await import('sharp')).default; + const object = { asset: {} }; + await manager.addThumb(object, '/tmp/thumb.png'); + expect(sharp).toHaveBeenCalled(); + expect(object.asset.thumb).toBeDefined(); + }); + + it('should call execFile for video thumb in addThumb', async () => { + const util = await import('node:util'); + const object = { asset: {} }; + await manager.addThumb(object, '/tmp/video.mp4'); + expect(object.asset.thumb).toBeDefined(); + }); + + it('should call decompress for .zip in addFile', async () => { + const decompress = (await import('decompress')).default; + const object = {}; + const tmpFile = { name: 'archive.zip', path: '/tmp/archive.zip' }; + await manager.addFile(object, tmpFile); + expect(decompress).toHaveBeenCalled(); + expect(object.asset.type).toBe('bundle'); + }); + + it('should call addThumb for image/video in addFile', async () => { + const spy = vi.spyOn(manager, 'addThumb').mockResolvedValue(); + const object = {}; + const tmpFile = { name: 'pic.png', path: '/tmp/pic.png' }; + await manager.addFile(object, tmpFile); + expect(spy).toHaveBeenCalled(); + }); + + it('should list game objects', async () => { + await manager.create({}, { name: 'Obj1' }); + await manager.create({}, { name: 'Obj2' }); + const list = await manager.list({}); + expect(Array.isArray(list)).toBe(true); + expect(list.length).toBe(2); + }); +}); \ No newline at end of file diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..996b3b9 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.js'], + environment: 'node', + }, +}); \ No newline at end of file