From 634184c2be41b86d555ff325c2bc220ec84115f2 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 9 Feb 2026 11:20:28 +0100 Subject: [PATCH] test: configure Vitest and add 54 unit tests (F6.1, F6.2) Set up Vitest with happy-dom, mock Nuxt auto-imports via #imports alias. Add tests for: inventory-types validators (9), apiHelpers (10), modelUtils (18), useConfirm (8), useToast (9). All 54 tests pass. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 550 ++++++++++++++++++++++++++- package.json | 7 +- tests/__mocks__/imports.ts | 35 ++ tests/composables/useConfirm.test.ts | 81 ++++ tests/composables/useToast.test.ts | 83 ++++ tests/shared/apiHelpers.test.ts | 50 +++ tests/shared/inventory-types.test.ts | 118 ++++++ tests/shared/modelUtils.test.ts | 169 ++++++++ vitest.config.ts | 17 + 9 files changed, 1099 insertions(+), 11 deletions(-) create mode 100644 tests/__mocks__/imports.ts create mode 100644 tests/composables/useConfirm.test.ts create mode 100644 tests/composables/useToast.test.ts create mode 100644 tests/shared/apiHelpers.test.ts create mode 100644 tests/shared/inventory-types.test.ts create mode 100644 tests/shared/modelUtils.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 423ec18..215d979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,13 @@ "@types/node": "^25.2.1", "@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/parser": "^8.44.1", + "@vue/test-utils": "^2.4.6", "eslint": "^9.36.0", "eslint-plugin-vue": "^10.5.0", + "happy-dom": "^20.5.3", "typescript": "^5.7.3", "unplugin-icons": "^0.19.3", + "vitest": "^4.0.18", "vue-eslint-parser": "^10.2.0" } }, @@ -2119,6 +2122,13 @@ "node": ">=14.0.0" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-minify/binding-android-arm64": { "version": "0.87.0", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.87.0.tgz", @@ -3747,6 +3757,13 @@ "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==", "license": "CC0-1.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.4.0.tgz", @@ -4040,6 +4057,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4075,6 +4110,23 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.44.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz", @@ -4672,6 +4724,127 @@ "integrity": "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==", "license": "MIT" }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.23", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", @@ -4951,6 +5124,17 @@ "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -5207,6 +5391,16 @@ "devOptional": true, "license": "Python-2.0" }, + "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": "2.1.2", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.1.2.tgz", @@ -5660,6 +5854,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5958,6 +6162,24 @@ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -6609,6 +6831,51 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7301,6 +7568,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -7829,6 +8106,37 @@ "uncrypto": "^0.1.3" } }, + "node_modules/happy-dom": { + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.5.3.tgz", + "integrity": "sha512-xqAxGnkRU0KNhheHpxb3uScqg/aehqUiVto/a9ApWMyNvnH9CAqHYq9dEPAovM6bOGbLstmTfGIln5ZIezEU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^6.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8442,6 +8750,64 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9208,9 +9574,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -9978,6 +10344,17 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/ofetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", @@ -11256,6 +11633,13 @@ "node": ">= 6" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -11943,6 +12327,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -12089,6 +12480,13 @@ "node": ">=12.0.0" } }, + "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/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -12105,9 +12503,9 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, "node_modules/streamx": { @@ -12536,12 +12934,22 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12558,6 +12966,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13617,6 +14035,84 @@ "@types/estree": "^1.0.0" } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -13653,6 +14149,13 @@ "ufo": "^1.6.1" } }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-devtools-stub": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", @@ -13710,6 +14213,16 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -13735,6 +14248,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/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 e3cea3a..83c1a59 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "postinstall": "nuxt prepare", "start": "nuxt start", "lint": "eslint . --ext .js,.ts,.vue", - "lint:fix": "npm run lint -- --fix" + "lint:fix": "npm run lint -- --fix", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", @@ -28,10 +30,13 @@ "@types/node": "^25.2.1", "@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/parser": "^8.44.1", + "@vue/test-utils": "^2.4.6", "eslint": "^9.36.0", "eslint-plugin-vue": "^10.5.0", + "happy-dom": "^20.5.3", "typescript": "^5.7.3", "unplugin-icons": "^0.19.3", + "vitest": "^4.0.18", "vue-eslint-parser": "^10.2.0" } } diff --git a/tests/__mocks__/imports.ts b/tests/__mocks__/imports.ts new file mode 100644 index 0000000..452f33b --- /dev/null +++ b/tests/__mocks__/imports.ts @@ -0,0 +1,35 @@ +/** + * Minimal mock for Nuxt's #imports auto-import. + * Add stubs here as tests require them. + */ + +import { ref } from 'vue' + +export const useRuntimeConfig = () => ({ + public: { + apiBaseUrl: 'http://localhost:8081/api', + appVersion: '0.0.0-test', + }, +}) + +export const useRoute = () => ({ + path: '/', + params: {}, + query: {}, +}) + +export const useRouter = () => ({ + push: () => Promise.resolve(), + replace: () => Promise.resolve(), +}) + +export const navigateTo = () => Promise.resolve() + +export const useRequestFetch = () => fetch + +export const useFetch = () => ({ + data: ref(null), + error: ref(null), + pending: ref(false), + refresh: () => Promise.resolve(), +}) diff --git a/tests/composables/useConfirm.test.ts b/tests/composables/useConfirm.test.ts new file mode 100644 index 0000000..62104c5 --- /dev/null +++ b/tests/composables/useConfirm.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest' +import { useConfirm } from '~/composables/useConfirm' + +describe('useConfirm', () => { + it('returns confirm function and state', () => { + const { confirm, confirmState, handleConfirm, handleCancel } = useConfirm() + expect(typeof confirm).toBe('function') + expect(typeof handleConfirm).toBe('function') + expect(typeof handleCancel).toBe('function') + expect(confirmState.open).toBe(false) + }) + + it('opens modal with correct options', () => { + const { confirm, confirmState } = useConfirm() + // Don't await — we'll manually resolve + confirm({ message: 'Delete this item?' }) + expect(confirmState.open).toBe(true) + expect(confirmState.message).toBe('Delete this item?') + expect(confirmState.title).toBe('Confirmation') + expect(confirmState.confirmText).toBe('Supprimer') + expect(confirmState.cancelText).toBe('Annuler') + expect(confirmState.dangerous).toBe(true) + // Clean up by canceling + const { handleCancel } = useConfirm() + handleCancel() + }) + + it('resolves true on confirm', async () => { + const { confirm, handleConfirm } = useConfirm() + const promise = confirm({ message: 'Confirm?' }) + handleConfirm() + const result = await promise + expect(result).toBe(true) + }) + + it('resolves false on cancel', async () => { + const { confirm, handleCancel } = useConfirm() + const promise = confirm({ message: 'Cancel?' }) + handleCancel() + const result = await promise + expect(result).toBe(false) + }) + + it('closes modal after confirm', async () => { + const { confirm, confirmState, handleConfirm } = useConfirm() + confirm({ message: 'Test' }) + expect(confirmState.open).toBe(true) + handleConfirm() + expect(confirmState.open).toBe(false) + }) + + it('closes modal after cancel', async () => { + const { confirm, confirmState, handleCancel } = useConfirm() + confirm({ message: 'Test' }) + expect(confirmState.open).toBe(true) + handleCancel() + expect(confirmState.open).toBe(false) + }) + + it('supports custom options', () => { + const { confirm, confirmState, handleCancel } = useConfirm() + confirm({ + title: 'Custom Title', + message: 'Custom message', + confirmText: 'Yes', + cancelText: 'No', + dangerous: false, + }) + expect(confirmState.title).toBe('Custom Title') + expect(confirmState.confirmText).toBe('Yes') + expect(confirmState.cancelText).toBe('No') + expect(confirmState.dangerous).toBe(false) + handleCancel() + }) + + it('shares state across calls (singleton)', () => { + const a = useConfirm() + const b = useConfirm() + expect(a.confirmState).toBe(b.confirmState) + }) +}) diff --git a/tests/composables/useToast.test.ts b/tests/composables/useToast.test.ts new file mode 100644 index 0000000..5fc5ce2 --- /dev/null +++ b/tests/composables/useToast.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { useToast } from '~/composables/useToast' + +describe('useToast', () => { + beforeEach(() => { + vi.useFakeTimers() + const { clearAll } = useToast() + clearAll() + }) + + it('returns all expected functions', () => { + const toast = useToast() + expect(typeof toast.showToast).toBe('function') + expect(typeof toast.showSuccess).toBe('function') + expect(typeof toast.showError).toBe('function') + expect(typeof toast.showWarning).toBe('function') + expect(typeof toast.showInfo).toBe('function') + expect(typeof toast.removeToast).toBe('function') + expect(typeof toast.clearAll).toBe('function') + }) + + it('adds a toast with correct properties', () => { + const { showToast, toasts } = useToast() + const id = showToast('Hello', 'info') + expect(toasts.value).toHaveLength(1) + expect(toasts.value[0].message).toBe('Hello') + expect(toasts.value[0].type).toBe('info') + expect(toasts.value[0].visible).toBe(true) + expect(toasts.value[0].id).toBe(id) + }) + + it('showSuccess creates a success toast', () => { + const { showSuccess, toasts } = useToast() + showSuccess('Saved!') + expect(toasts.value[0].type).toBe('success') + expect(toasts.value[0].message).toBe('Saved!') + }) + + it('showError creates an error toast', () => { + const { showError, toasts } = useToast() + showError('Failed!') + expect(toasts.value[0].type).toBe('error') + }) + + it('showWarning creates a warning toast', () => { + const { showWarning, toasts } = useToast() + showWarning('Caution!') + expect(toasts.value[0].type).toBe('warning') + }) + + it('limits to MAX_TOASTS (3)', () => { + const { showToast, toasts } = useToast() + showToast('A', 'info') + showToast('B', 'info') + showToast('C', 'info') + showToast('D', 'info') + expect(toasts.value).toHaveLength(3) + expect(toasts.value[0].message).toBe('B') + expect(toasts.value[2].message).toBe('D') + }) + + it('clearAll removes all toasts', () => { + const { showToast, toasts, clearAll } = useToast() + showToast('A', 'info') + showToast('B', 'info') + expect(toasts.value.length).toBeGreaterThan(0) + clearAll() + expect(toasts.value).toHaveLength(0) + }) + + it('shares state across calls (singleton)', () => { + const a = useToast() + const b = useToast() + expect(a.toasts).toBe(b.toasts) + }) + + it('removeToast sets visible to false', () => { + const { showToast, toasts, removeToast } = useToast() + const id = showToast('Test', 'info') + removeToast(id) + expect(toasts.value[0].visible).toBe(false) + }) +}) diff --git a/tests/shared/apiHelpers.test.ts b/tests/shared/apiHelpers.test.ts new file mode 100644 index 0000000..8baa5bb --- /dev/null +++ b/tests/shared/apiHelpers.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { extractCollection } from '~/shared/utils/apiHelpers' + +describe('extractCollection', () => { + it('returns the input if it is already an array', () => { + const items = [{ id: 1 }, { id: 2 }] + expect(extractCollection(items)).toEqual(items) + }) + + it('extracts from hydra:member', () => { + const payload = { 'hydra:member': [{ id: 1 }], 'hydra:totalItems': 1 } + expect(extractCollection(payload)).toEqual([{ id: 1 }]) + }) + + it('extracts from member', () => { + const payload = { member: [{ id: 1 }, { id: 2 }] } + expect(extractCollection(payload)).toEqual([{ id: 1 }, { id: 2 }]) + }) + + it('extracts from items', () => { + const payload = { items: [{ id: 1 }] } + expect(extractCollection(payload)).toEqual([{ id: 1 }]) + }) + + it('extracts from data', () => { + const payload = { data: [{ id: 1 }] } + expect(extractCollection(payload)).toEqual([{ id: 1 }]) + }) + + it('prefers member over hydra:member', () => { + const payload = { member: [{ id: 'member' }], 'hydra:member': [{ id: 'hydra' }] } + expect(extractCollection(payload)).toEqual([{ id: 'member' }]) + }) + + it('returns empty array for null', () => { + expect(extractCollection(null)).toEqual([]) + }) + + it('returns empty array for undefined', () => { + expect(extractCollection(undefined)).toEqual([]) + }) + + it('returns empty array for empty object', () => { + expect(extractCollection({})).toEqual([]) + }) + + it('returns empty array for string', () => { + expect(extractCollection('not an object')).toEqual([]) + }) +}) diff --git a/tests/shared/inventory-types.test.ts b/tests/shared/inventory-types.test.ts new file mode 100644 index 0000000..7206e9d --- /dev/null +++ b/tests/shared/inventory-types.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from 'vitest' +import { + componentModelStructureValidator, + createEmptyComponentModelStructure, + createEmptyPieceModelStructure, + createEmptyProductModelStructure, +} from '~/shared/types/inventory' + +describe('createEmptyComponentModelStructure', () => { + it('returns a valid empty structure', () => { + const result = createEmptyComponentModelStructure() + expect(result).toEqual({ + customFields: [], + pieces: [], + products: [], + subcomponents: [], + }) + }) +}) + +describe('createEmptyPieceModelStructure', () => { + it('returns a valid empty piece structure', () => { + const result = createEmptyPieceModelStructure() + expect(result).toEqual({ + customFields: [], + products: [], + }) + }) +}) + +describe('createEmptyProductModelStructure', () => { + it('returns a valid empty product structure', () => { + const result = createEmptyProductModelStructure() + expect(result).toEqual({ + customFields: [], + }) + }) +}) + +describe('componentModelStructureValidator', () => { + it('parses a minimal valid structure', () => { + const input = { + customFields: [], + pieces: [], + subcomponents: [], + } + const result = componentModelStructureValidator.safeParse(input) + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.customFields).toEqual([]) + expect(result.data.pieces).toEqual([]) + expect(result.data.subcomponents).toEqual([]) + } + }) + + it('rejects a non-object input', () => { + const result = componentModelStructureValidator.safeParse('not an object') + expect(result.success).toBe(false) + if (!result.success) { + expect(result.issues.length).toBeGreaterThan(0) + } + }) + + it('validates custom fields with name and type', () => { + const input = { + customFields: [ + { name: 'Color', type: 'text', required: true }, + { name: 'Size', type: 'select', required: false, options: ['S', 'M', 'L'] }, + ], + pieces: [], + subcomponents: [], + } + const result = componentModelStructureValidator.safeParse(input) + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.customFields).toHaveLength(2) + expect(result.data.customFields[0].name).toBe('Color') + expect(result.data.customFields[1].options).toEqual(['S', 'M', 'L']) + } + }) + + it('rejects custom fields without name', () => { + const input = { + customFields: [{ type: 'text', required: false }], + pieces: [], + subcomponents: [], + } + const result = componentModelStructureValidator.safeParse(input) + expect(result.success).toBe(false) + if (!result.success) { + expect(result.issues.some((i) => i.includes('name'))).toBe(true) + } + }) + + it('validates nested subcomponents', () => { + const input = { + customFields: [], + pieces: [], + subcomponents: [ + { + typeComposantId: 'abc-123', + alias: 'Motor', + subcomponents: [], + }, + ], + } + const result = componentModelStructureValidator.safeParse(input) + expect(result.success).toBe(true) + if (result.success) { + expect(result.data.subcomponents).toHaveLength(1) + expect(result.data.subcomponents[0].alias).toBe('Motor') + } + }) + + it('parse throws on invalid input', () => { + expect(() => componentModelStructureValidator.parse(null)).toThrow() + }) +}) diff --git a/tests/shared/modelUtils.test.ts b/tests/shared/modelUtils.test.ts new file mode 100644 index 0000000..f986aa4 --- /dev/null +++ b/tests/shared/modelUtils.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect } from 'vitest' +import { + isPlainObject, + defaultStructure, + cloneStructure, + computeStructureStats, + formatStructurePreview, +} from '~/shared/model/componentStructure' +import { + defaultPieceStructure, + defaultProductStructure, + clonePieceStructure, + cloneProductStructure, +} from '~/shared/model/pieceProductStructure' + +describe('isPlainObject', () => { + it('returns true for plain objects', () => { + expect(isPlainObject({})).toBe(true) + expect(isPlainObject({ key: 'value' })).toBe(true) + }) + + it('returns false for arrays', () => { + expect(isPlainObject([])).toBe(false) + }) + + it('returns false for null', () => { + expect(isPlainObject(null)).toBe(false) + }) + + it('returns false for primitives', () => { + expect(isPlainObject('string')).toBe(false) + expect(isPlainObject(42)).toBe(false) + expect(isPlainObject(undefined)).toBe(false) + }) +}) + +describe('defaultStructure', () => { + it('returns a fresh empty structure each time', () => { + const a = defaultStructure() + const b = defaultStructure() + expect(a).toEqual(b) + expect(a).not.toBe(b) + expect(a.customFields).toEqual([]) + expect(a.pieces).toEqual([]) + expect(a.products).toEqual([]) + expect(a.subcomponents).toEqual([]) + }) +}) + +describe('cloneStructure', () => { + it('deep clones a structure', () => { + const original = defaultStructure() + original.customFields.push({ name: 'test', type: 'text', required: false }) + const cloned = cloneStructure(original) + expect(cloned.customFields).toHaveLength(1) + expect(cloned.customFields[0].name).toBe('test') + // Ensure deep clone — mutating original doesn't affect clone + original.customFields[0].name = 'mutated' + expect(cloned.customFields[0].name).toBe('test') + }) + + it('returns default structure for null input', () => { + const result = cloneStructure(null) + expect(result).toEqual(defaultStructure()) + }) + + it('returns default structure for undefined input', () => { + const result = cloneStructure(undefined) + expect(result).toEqual(defaultStructure()) + }) + + it('preserves typeComposantId and alias', () => { + const input = { + ...defaultStructure(), + typeComposantId: 'abc-123', + alias: 'Motor', + } + const result = cloneStructure(input) + expect(result.typeComposantId).toBe('abc-123') + expect(result.alias).toBe('Motor') + }) +}) + +describe('computeStructureStats', () => { + it('counts elements in a structure', () => { + const structure = { + ...defaultStructure(), + customFields: [ + { name: 'A', type: 'text' as const, required: false }, + { name: 'B', type: 'number' as const, required: true }, + ], + pieces: [{ typePieceId: 'p1' }], + products: [{ typeProductId: 'pr1' }], + subcomponents: [{ subcomponents: [] }], + } + const stats = computeStructureStats(structure) + expect(stats.customFields).toBe(2) + expect(stats.pieces).toBe(1) + expect(stats.products).toBe(1) + expect(stats.subcomponents).toBe(1) + }) + + it('returns zeros for empty structure', () => { + const stats = computeStructureStats(defaultStructure()) + expect(stats).toEqual({ + customFields: 0, + pieces: 0, + products: 0, + subcomponents: 0, + }) + }) +}) + +describe('formatStructurePreview', () => { + it('returns "Structure vide" for empty structure', () => { + const result = formatStructurePreview(defaultStructure()) + expect(result).toBe('Structure vide') + }) + + it('formats a non-empty structure', () => { + const structure = { + ...defaultStructure(), + customFields: [{ name: 'A', type: 'text' as const, required: false }], + pieces: [{ typePieceId: 'p1' }, { typePieceId: 'p2' }], + } + const result = formatStructurePreview(structure) + expect(result).toContain('1 champ') + expect(result).toContain('2 pièce') + }) +}) + +describe('defaultPieceStructure', () => { + it('returns a valid empty piece structure', () => { + const result = defaultPieceStructure() + expect(result.customFields).toEqual([]) + expect(result.products).toEqual([]) + }) +}) + +describe('defaultProductStructure', () => { + it('returns a valid empty product structure', () => { + const result = defaultProductStructure() + expect(result.customFields).toEqual([]) + }) +}) + +describe('clonePieceStructure', () => { + it('deep clones a piece structure', () => { + const original = defaultPieceStructure() + original.customFields.push({ name: 'Weight', type: 'number', required: true }) + const cloned = clonePieceStructure(original) + expect(cloned.customFields).toHaveLength(1) + original.customFields[0].name = 'mutated' + expect(cloned.customFields[0].name).toBe('Weight') + }) + + it('returns default for null', () => { + expect(clonePieceStructure(null)).toEqual(defaultPieceStructure()) + }) +}) + +describe('cloneProductStructure', () => { + it('deep clones a product structure', () => { + const original = defaultProductStructure() + original.customFields.push({ name: 'Color', type: 'text', required: false }) + const cloned = cloneProductStructure(original) + expect(cloned.customFields).toHaveLength(1) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c8af1f8 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vitest/config' +import { resolve } from 'node:path' + +export default defineConfig({ + test: { + globals: true, + environment: 'happy-dom', + root: '.', + include: ['tests/**/*.test.ts'], + }, + resolve: { + alias: { + '~': resolve(__dirname, 'app'), + '#imports': resolve(__dirname, 'tests/__mocks__/imports.ts'), + }, + }, +})