diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml new file mode 100644 index 0000000..75f8a46 --- /dev/null +++ b/.github/workflows/vitest.yml @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Chen Asraf +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Vitest + +on: pull_request + +permissions: + contents: read + +concurrency: + group: vitest-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src }} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'src/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'tsconfig.json' + - 'tsconfig.*.json' + - 'vite.config.ts' + - 'vitest.config.ts' + + vitest: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + name: Vitest + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + + - name: Install pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test:run + + summary: + permissions: + contents: none + runs-on: ubuntu-latest + needs: [changes, vitest] + + if: always() + + name: vitest-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.vitest.result != 'success' }}; then exit 1; fi diff --git a/package.json b/package.json index e6a32ca..79a835c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "lint": "eslint src", "format": "eslint --fix src && prettier --write {vite.config.ts,src/,README.md}", "prepare": "husky", - "gen": "simple-scaffold -c . -k" + "gen": "simple-scaffold -c . -k", + "test": "vitest", + "test:run": "vitest run" }, "browserslist": [ "extends @nextcloud/browserslist-config" @@ -36,8 +38,11 @@ "@nextcloud/browserslist-config": "^3.1.2", "@nextcloud/eslint-config": "^8.4.2", "@nextcloud/stylelint-config": "^3.1.1", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.8.1", "eslint": "^9.39.2", + "happy-dom": "^20.0.11", "husky": "^9.1.7", "lint-staged": "^16.2.7", "prettier": "^2.8.8", @@ -49,6 +54,7 @@ "typescript-eslint": "^8.51.0", "vite": "^6.4.1", "vite-plugin-checker": "^0.12.0", + "vitest": "^4.0.16", "vue-router": "^4.6.4", "vue-tsc": "^2.2.12" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f2825c..15c9c74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,12 +54,21 @@ importers: '@nextcloud/stylelint-config': specifier: ^3.1.1 version: 3.1.1(stylelint-config-recommended-scss@13.1.0(postcss@8.5.6)(stylelint@16.11.0(typescript@5.9.2)))(stylelint-config-recommended-vue@1.5.0(postcss-html@1.7.0)(stylelint@16.11.0(typescript@5.9.2)))(stylelint@16.11.0(typescript@5.9.2)) + '@vitejs/plugin-vue': + specifier: ^6.0.3 + version: 6.0.3(vite@6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.2)) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 '@vue/tsconfig': specifier: ^0.8.1 version: 0.8.1(typescript@5.9.2)(vue@3.5.26(typescript@5.9.2)) eslint: specifier: ^9.39.2 version: 9.39.2 + happy-dom: + specifier: ^20.0.11 + version: 20.0.11 husky: specifier: ^9.1.7 version: 9.1.7 @@ -93,6 +102,9 @@ importers: vite-plugin-checker: specifier: ^0.12.0 version: 0.12.0(eslint@9.39.2)(meow@13.2.0)(optionator@0.9.4)(stylelint@16.11.0(typescript@5.9.2))(typescript@5.9.2)(vite@6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vue-tsc@2.2.12(typescript@5.9.2)) + vitest: + specifier: ^4.0.16 + version: 4.0.16(@types/node@20.17.10)(happy-dom@20.0.11)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2) vue-router: specifier: ^4.6.4 version: 4.6.4(vue@3.5.26(typescript@5.9.2)) @@ -463,6 +475,10 @@ packages: resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -618,6 +634,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -700,6 +719,13 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rollup/plugin-inject@5.0.5': resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} @@ -870,15 +896,24 @@ packages: '@rushstack/ts-command-line@5.1.5': resolution: {integrity: sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/escape-html@1.0.4': resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==} @@ -930,6 +965,9 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@typescript-eslint/eslint-plugin@7.18.0': resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1057,6 +1095,42 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 + '@vitejs/plugin-vue@6.0.3': + resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@vitest/expect@4.0.16': + resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} + + '@vitest/mocker@4.0.16': + resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.16': + resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} + + '@vitest/runner@4.0.16': + resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} + + '@vitest/snapshot@4.0.16': + resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} + + '@vitest/spy@4.0.16': + resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} + + '@vitest/utils@4.0.16': + resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@volar/language-core@2.4.15': resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} @@ -1137,6 +1211,9 @@ packages: '@vue/shared@3.5.26': resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@vue/tsconfig@0.8.1': resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} peerDependencies: @@ -1172,6 +1249,10 @@ packages: peerDependencies: vue: ^3.5.0 + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1284,6 +1365,10 @@ packages: assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1428,6 +1513,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1494,6 +1583,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -1517,6 +1610,9 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + console-browserify@1.2.0: resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} @@ -1702,6 +1798,14 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -1719,6 +1823,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.18.4: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} @@ -1758,6 +1865,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1959,6 +2069,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1973,6 +2086,10 @@ packages: evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -2071,6 +2188,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -2137,6 +2258,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -2174,6 +2299,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + happy-dom@20.0.11: + resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} + engines: {node: '>=20.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2475,9 +2604,21 @@ packages: resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} engines: {node: '>=10'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2589,6 +2730,9 @@ packages: lowlight@3.3.0: resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2755,6 +2899,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2762,6 +2910,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -2808,6 +2960,11 @@ packages: resolution: {integrity: sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==} engines: {node: '>=10'} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2847,6 +3004,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -2882,6 +3042,9 @@ packages: resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} engines: {node: '>=20'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-name-regex@2.0.6: resolution: {integrity: sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==} engines: {node: '>=12'} @@ -2925,6 +3088,10 @@ packages: path-posix@1.0.0: resolution: {integrity: sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3031,6 +3198,9 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -3398,6 +3568,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3461,6 +3634,12 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3479,6 +3658,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -3636,10 +3819,21 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + to-buffer@1.2.2: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} @@ -3902,12 +4096,49 @@ packages: yaml: optional: true + vitest@4.0.16: + resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + 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.16 + '@vitest/browser-preview': 4.0.16 + '@vitest/browser-webdriverio': 4.0.16 + '@vitest/ui': 4.0.16 + 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 + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -3954,6 +4185,10 @@ packages: resolution: {integrity: sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==} engines: {node: '>=14'} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3979,6 +4214,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3987,6 +4227,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -4348,6 +4592,15 @@ snapshots: dependencies: '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4625,6 +4878,8 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@one-ini/wasm@0.1.1': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -4686,6 +4941,11 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rollup/plugin-inject@5.0.5(rollup@4.54.0)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.54.0) @@ -4816,14 +5076,23 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@standard-schema/spec@1.1.0': {} + '@tokenizer/token@0.3.0': {} '@types/argparse@1.0.38': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/escape-html@1.0.4': {} '@types/estree-jsx@1.0.5': @@ -4853,7 +5122,6 @@ snapshots: '@types/node@20.17.10': dependencies: undici-types: 6.19.8 - optional: true '@types/semver@7.7.1': {} @@ -4870,6 +5138,8 @@ snapshots: '@types/web-bluetooth@0.0.21': {} + '@types/whatwg-mimetype@3.0.2': {} + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.2)(typescript@5.9.2))(eslint@9.39.2)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5049,6 +5319,51 @@ snapshots: vite: 6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2) vue: 3.5.26(typescript@5.9.2) + '@vitejs/plugin-vue@6.0.3(vite@6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2) + vue: 3.5.26(typescript@5.9.2) + + '@vitest/expect@4.0.16': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.16(vite@6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.16 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2) + + '@vitest/pretty-format@4.0.16': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.16': + dependencies: + '@vitest/utils': 4.0.16 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.16': + dependencies: + '@vitest/pretty-format': 4.0.16 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.16': {} + + '@vitest/utils@4.0.16': + dependencies: + '@vitest/pretty-format': 4.0.16 + tinyrainbow: 3.0.3 + '@volar/language-core@2.4.15': dependencies: '@volar/source-map': 2.4.15 @@ -5172,6 +5487,11 @@ snapshots: '@vue/shared@3.5.26': {} + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + '@vue/tsconfig@0.8.1(typescript@5.9.2)(vue@3.5.26(typescript@5.9.2))': optionalDependencies: typescript: 5.9.2 @@ -5201,6 +5521,8 @@ snapshots: dependencies: vue: 3.5.26(typescript@5.9.2) + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -5337,6 +5659,8 @@ snapshots: object.assign: 4.1.7 util: 0.12.5 + assertion-error@2.0.1: {} + astral-regex@2.0.0: {} async-function@1.0.0: {} @@ -5495,6 +5819,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -5555,6 +5881,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@14.0.2: {} comment-parser@1.4.1: {} @@ -5569,6 +5897,11 @@ snapshots: confbox@0.2.2: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + console-browserify@1.2.0: {} constants-browserify@1.0.0: {} @@ -5766,6 +6099,15 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.3 + electron-to-chromium@1.5.267: {} elliptic@6.6.1: @@ -5788,6 +6130,8 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 @@ -5868,6 +6212,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6159,6 +6505,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} eventemitter3@5.0.1: {} @@ -6170,6 +6520,8 @@ snapshots: md5.js: 1.3.5 safe-buffer: 5.2.1 + expect-type@1.3.0: {} + exsolve@1.0.8: {} extend@3.0.2: {} @@ -6254,6 +6606,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -6332,6 +6689,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -6370,6 +6736,12 @@ snapshots: graphemer@1.4.0: {} + happy-dom@20.0.11: + dependencies: + '@types/node': 20.17.10 + '@types/whatwg-mimetype': 3.0.2 + whatwg-mimetype: 3.0.0 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -6671,8 +7043,24 @@ snapshots: isomorphic-timers-promises@1.0.1: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jju@1.4.0: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -6781,6 +7169,8 @@ snapshots: devlop: 1.1.0 highlight.js: 11.11.1 + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -7086,12 +7476,18 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 minimist@1.2.8: {} + minipass@7.1.2: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -7156,6 +7552,10 @@ snapshots: util: 0.12.5 vm-browserify: 1.1.2 + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-path@3.0.0: {} npm-run-path@6.0.0: @@ -7205,6 +7605,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -7247,6 +7649,8 @@ snapshots: p-timeout@7.0.1: {} + package-json-from-dist@1.0.1: {} + package-name-regex@2.0.6: {} pako@1.0.11: {} @@ -7292,6 +7696,11 @@ snapshots: path-posix@1.0.0: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-type@4.0.0: {} pathe@2.0.3: {} @@ -7386,6 +7795,8 @@ snapshots: property-information@7.1.0: {} + proto-list@1.2.4: {} + proxy-from-env@1.1.0: {} public-encrypt@4.0.3: @@ -7820,6 +8231,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} slash@3.0.0: {} @@ -7881,6 +8294,10 @@ snapshots: sprintf-js@1.0.3: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -7906,6 +8323,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -8109,11 +8532,17 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + to-buffer@1.2.2: dependencies: isarray: 2.0.5 @@ -8218,8 +8647,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.19.8: - optional: true + undici-types@6.19.8: {} unicorn-magic@0.3.0: {} @@ -8376,10 +8804,50 @@ snapshots: sass-embedded: 1.97.1 yaml: 2.8.2 + vitest@4.0.16(@types/node@20.17.10)(happy-dom@20.0.11)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.16 + '@vitest/mocker': 4.0.16(vite@6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.16 + '@vitest/runner': 4.0.16 + '@vitest/snapshot': 4.0.16 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.4.1(@types/node@20.17.10)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.10 + happy-dom: 20.0.11 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} vscode-uri@3.1.0: {} + vue-component-type-helpers@2.2.12: {} + vue-eslint-parser@9.4.3(eslint@9.39.2): dependencies: debug: 4.4.3 @@ -8443,6 +8911,8 @@ snapshots: url-join: 5.0.0 url-parse: 1.5.10 + whatwg-mimetype@3.0.0: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -8492,6 +8962,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -8500,6 +8975,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 diff --git a/src/components/Pagination.test.ts b/src/components/Pagination.test.ts new file mode 100644 index 0000000..780061c --- /dev/null +++ b/src/components/Pagination.test.ts @@ -0,0 +1,178 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import Pagination from './Pagination.vue' + +// Mock @nextcloud/l10n +vi.mock('@nextcloud/l10n', () => ({ + t: (app: string, text: string, vars?: Record) => { + if (vars) { + return Object.entries(vars).reduce( + (acc, [key, value]) => acc.replace(`{${key}}`, String(value)), + text, + ) + } + return text + }, +})) + +// Mock @nextcloud/vue/components/NcButton +vi.mock('@nextcloud/vue/components/NcButton', () => ({ + default: { + name: 'NcButton', + template: + '', + props: ['variant', 'disabled', 'ariaLabel', 'title'], + }, +})) + +// Mock icon components +vi.mock('@icons/PageFirst.vue', () => ({ + default: { name: 'PageFirstIcon', template: '«', props: ['size'] }, +})) +vi.mock('@icons/PageLast.vue', () => ({ + default: { name: 'PageLastIcon', template: '»', props: ['size'] }, +})) +vi.mock('@icons/ChevronLeft.vue', () => ({ + default: { name: 'ChevronLeftIcon', template: '', props: ['size'] }, +})) +vi.mock('@icons/ChevronRight.vue', () => ({ + default: { name: 'ChevronRightIcon', template: '', props: ['size'] }, +})) + +describe('Pagination', () => { + describe('visibility', () => { + it('should not render when maxPages is 1', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 1, maxPages: 1 }, + }) + expect(wrapper.find('nav').exists()).toBe(false) + }) + + it('should render when maxPages is greater than 1', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 1, maxPages: 2 }, + }) + expect(wrapper.find('nav').exists()).toBe(true) + }) + }) + + describe('pageItems calculation', () => { + it('should show all pages when maxPages <= 10', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 1, maxPages: 5 }, + }) + // Access the computed property via vm + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + expect(pageItems).toEqual([1, 2, 3, 4, 5]) + }) + + it('should show all pages when maxPages is exactly 10', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 5, maxPages: 10 }, + }) + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + expect(pageItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + }) + + it('should add ellipsis for pages > 10 when on first page', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 1, maxPages: 20 }, + }) + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + // Should show: 1, 2, 3 (first 3) + ellipsis + 18, 19, 20 (last 3) + expect(pageItems).toEqual([1, 2, 3, 'ellipsis', 18, 19, 20]) + }) + + it('should add ellipsis for pages > 10 when on last page', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 20, maxPages: 20 }, + }) + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + // Should show: 1, 2, 3 (first 3) + ellipsis + 18, 19, 20 (last 3) + expect(pageItems).toEqual([1, 2, 3, 'ellipsis', 18, 19, 20]) + }) + + it('should show pages around current page in the middle', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 10, maxPages: 20 }, + }) + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + // Should show: 1, 2, 3 + ellipsis + 8, 9, 10, 11, 12 + ellipsis + 18, 19, 20 + expect(pageItems).toEqual([1, 2, 3, 'ellipsis', 8, 9, 10, 11, 12, 'ellipsis', 18, 19, 20]) + }) + + it('should handle edge case where current page is near the start', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 4, maxPages: 20 }, + }) + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + // Current=4, so around: 2,3,4,5,6, combined with first 3 (1,2,3): 1,2,3,4,5,6 + expect(pageItems).toContain(1) + expect(pageItems).toContain(4) + expect(pageItems).toContain(6) + }) + + it('should handle edge case where current page is near the end', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 17, maxPages: 20 }, + }) + const pageItems = (wrapper.vm as unknown as { pageItems: (number | 'ellipsis')[] }).pageItems + // Current=17, so around: 15,16,17,18,19, combined with last 3 (18,19,20): 15,16,17,18,19,20 + expect(pageItems).toContain(15) + expect(pageItems).toContain(17) + expect(pageItems).toContain(20) + }) + }) + + describe('navigation', () => { + it('should emit update:currentPage when going to a page', async () => { + const wrapper = mount(Pagination, { + props: { currentPage: 5, maxPages: 10 }, + }) + + // Find all buttons and click the one for page 3 + const buttons = wrapper.findAll('button') + const page3Button = buttons.find((btn) => btn.text() === '3') + expect(page3Button).toBeDefined() + + await page3Button!.trigger('click') + expect(wrapper.emitted('update:currentPage')).toBeTruthy() + expect(wrapper.emitted('update:currentPage')![0]).toEqual([3]) + }) + + it('should not emit when clicking current page', async () => { + const wrapper = mount(Pagination, { + props: { currentPage: 5, maxPages: 10 }, + }) + + const buttons = wrapper.findAll('button') + const page5Button = buttons.find((btn) => btn.text() === '5') + + await page5Button!.trigger('click') + expect(wrapper.emitted('update:currentPage')).toBeFalsy() + }) + + it('should disable first/previous buttons on first page', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 1, maxPages: 10 }, + }) + + const buttons = wrapper.findAll('button') + // First two buttons are first page and previous page + expect(buttons[0].attributes('disabled')).toBeDefined() + expect(buttons[1].attributes('disabled')).toBeDefined() + }) + + it('should disable next/last buttons on last page', () => { + const wrapper = mount(Pagination, { + props: { currentPage: 10, maxPages: 10 }, + }) + + const buttons = wrapper.findAll('button') + // Last two buttons are next page and last page + const lastIdx = buttons.length - 1 + expect(buttons[lastIdx].attributes('disabled')).toBeDefined() + expect(buttons[lastIdx - 1].attributes('disabled')).toBeDefined() + }) + }) +}) diff --git a/src/components/RoleBadge.test.ts b/src/components/RoleBadge.test.ts new file mode 100644 index 0000000..303f99e --- /dev/null +++ b/src/components/RoleBadge.test.ts @@ -0,0 +1,188 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import RoleBadge from './RoleBadge.vue' +import type { Role } from '@/types/models' + +// Mock isDarkTheme - default to light theme +vi.mock('@nextcloud/vue/functions/isDarkTheme', () => ({ + isDarkTheme: false, +})) + +// Helper to create a mock role +function createMockRole(overrides: Partial = {}): Role { + return { + id: 100, + name: 'Test Role', + description: null, + colorLight: null, + colorDark: null, + canAccessAdminTools: false, + canEditRoles: false, + canEditCategories: false, + isSystemRole: false, + roleType: 'custom', + createdAt: Date.now(), + ...overrides, + } +} + +describe('RoleBadge', () => { + describe('rendering', () => { + it('should display the role name', () => { + const role = createMockRole({ name: 'Super Admin' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + expect(wrapper.text()).toBe('Super Admin') + }) + + it('should apply normal density class by default', () => { + const role = createMockRole() + const wrapper = mount(RoleBadge, { + props: { role }, + }) + expect(wrapper.find('.role-badge').classes()).toContain('density-normal') + }) + + it('should apply compact density class when specified', () => { + const role = createMockRole() + const wrapper = mount(RoleBadge, { + props: { role, density: 'compact' }, + }) + expect(wrapper.find('.role-badge').classes()).toContain('density-compact') + }) + }) + + describe('color calculation', () => { + it('should use colorLight when provided (light theme)', () => { + const role = createMockRole({ colorLight: '#ff5500' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + expect(style).toContain('background-color: #ff5500') + }) + + it('should use fallback color for Admin role (id=1)', () => { + const role = createMockRole({ id: 1, name: 'Admin', roleType: 'admin' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + // Fallback light color for Admin is #dc2626 + expect(style).toContain('background-color: #dc2626') + }) + + it('should use fallback color for Moderator role (id=2)', () => { + const role = createMockRole({ id: 2, name: 'Moderator', roleType: 'moderator' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + // Fallback light color for Moderator is #2563eb + expect(style).toContain('background-color: #2563eb') + }) + + it('should use fallback color for User role (id=3)', () => { + const role = createMockRole({ id: 3, name: 'User', roleType: 'default' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + // Fallback light color for User is #059669 + expect(style).toContain('background-color: #059669') + }) + + it('should use default fallback for custom roles without colors', () => { + const role = createMockRole({ id: 999, name: 'Custom' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + // Default light fallback is #000000 + expect(style).toContain('background-color: #000000') + }) + }) + + describe('text color calculation (contrast)', () => { + it('should use dark text on light backgrounds', () => { + // White background should have black text + const role = createMockRole({ colorLight: '#ffffff' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + expect(style).toContain('color: #000000') + }) + + it('should use light text on dark backgrounds', () => { + // Black background should have white text + const role = createMockRole({ colorLight: '#000000' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + expect(style).toContain('color: #ffffff') + }) + + it('should use light text on moderately dark backgrounds', () => { + // Dark blue should have white text + const role = createMockRole({ colorLight: '#1e3a5f' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + expect(style).toContain('color: #ffffff') + }) + + it('should use dark text on moderately light backgrounds', () => { + // Light yellow should have black text + const role = createMockRole({ colorLight: '#ffeb3b' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const style = wrapper.find('.role-badge').attributes('style') + expect(style).toContain('color: #000000') + }) + }) + + describe('hexToRgb method', () => { + it('should correctly parse 6-digit hex colors', () => { + const role = createMockRole({ colorLight: '#ff5500' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + // Access the method via vm + const vm = wrapper.vm as unknown as { + hexToRgb: (hex: string) => { r: number; g: number; b: number } | null + } + const result = vm.hexToRgb('#ff5500') + expect(result).toEqual({ r: 255, g: 85, b: 0 }) + }) + + it('should correctly parse 3-digit shorthand hex colors', () => { + const role = createMockRole({ colorLight: '#f00' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const vm = wrapper.vm as unknown as { + hexToRgb: (hex: string) => { r: number; g: number; b: number } | null + } + // #f00 expands to #ff0000 + const result = vm.hexToRgb('#f00') + expect(result).toEqual({ r: 255, g: 0, b: 0 }) + }) + + it('should handle hex without # prefix', () => { + const role = createMockRole({ colorLight: '#00ff00' }) + const wrapper = mount(RoleBadge, { + props: { role }, + }) + const vm = wrapper.vm as unknown as { + hexToRgb: (hex: string) => { r: number; g: number; b: number } | null + } + const result = vm.hexToRgb('00ff00') + expect(result).toEqual({ r: 0, g: 255, b: 0 }) + }) + }) +}) diff --git a/src/constants.test.ts b/src/constants.test.ts new file mode 100644 index 0000000..b9d6d08 --- /dev/null +++ b/src/constants.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest' +import { + RoleType, + isSystemRole, + isAdminRole, + isModeratorRole, + isDefaultRole, + isGuestRole, + isCustomRole, +} from './constants' +import type { Role } from './types/models' + +// Helper to create a mock role +function createMockRole(overrides: Partial = {}): Role { + return { + id: 1, + name: 'Test Role', + description: null, + colorLight: null, + colorDark: null, + canAccessAdminTools: false, + canEditRoles: false, + canEditCategories: false, + isSystemRole: false, + roleType: 'custom', + createdAt: Date.now(), + ...overrides, + } +} + +describe('RoleType constants', () => { + it('should have correct values', () => { + expect(RoleType.ADMIN).toBe('admin') + expect(RoleType.MODERATOR).toBe('moderator') + expect(RoleType.DEFAULT).toBe('default') + expect(RoleType.GUEST).toBe('guest') + expect(RoleType.CUSTOM).toBe('custom') + }) +}) + +describe('isSystemRole', () => { + it('should return true for system roles', () => { + const role = createMockRole({ isSystemRole: true }) + expect(isSystemRole(role)).toBe(true) + }) + + it('should return false for non-system roles', () => { + const role = createMockRole({ isSystemRole: false }) + expect(isSystemRole(role)).toBe(false) + }) +}) + +describe('isAdminRole', () => { + it('should return true for admin roles', () => { + const role = createMockRole({ roleType: 'admin' }) + expect(isAdminRole(role)).toBe(true) + }) + + it('should return false for non-admin roles', () => { + const role = createMockRole({ roleType: 'moderator' }) + expect(isAdminRole(role)).toBe(false) + }) + + it('should return false for null/undefined', () => { + expect(isAdminRole(null)).toBe(false) + expect(isAdminRole(undefined)).toBe(false) + }) +}) + +describe('isModeratorRole', () => { + it('should return true for moderator roles', () => { + const role = createMockRole({ roleType: 'moderator' }) + expect(isModeratorRole(role)).toBe(true) + }) + + it('should return false for non-moderator roles', () => { + const role = createMockRole({ roleType: 'admin' }) + expect(isModeratorRole(role)).toBe(false) + }) + + it('should return false for null/undefined', () => { + expect(isModeratorRole(null)).toBe(false) + expect(isModeratorRole(undefined)).toBe(false) + }) +}) + +describe('isDefaultRole', () => { + it('should return true for default roles', () => { + const role = createMockRole({ roleType: 'default' }) + expect(isDefaultRole(role)).toBe(true) + }) + + it('should return false for non-default roles', () => { + const role = createMockRole({ roleType: 'admin' }) + expect(isDefaultRole(role)).toBe(false) + }) + + it('should return false for null/undefined', () => { + expect(isDefaultRole(null)).toBe(false) + expect(isDefaultRole(undefined)).toBe(false) + }) +}) + +describe('isGuestRole', () => { + it('should return true for guest roles', () => { + const role = createMockRole({ roleType: 'guest' }) + expect(isGuestRole(role)).toBe(true) + }) + + it('should return false for non-guest roles', () => { + const role = createMockRole({ roleType: 'admin' }) + expect(isGuestRole(role)).toBe(false) + }) + + it('should return false for null/undefined', () => { + expect(isGuestRole(null)).toBe(false) + expect(isGuestRole(undefined)).toBe(false) + }) +}) + +describe('isCustomRole', () => { + it('should return true for custom roles', () => { + const role = createMockRole({ roleType: 'custom' }) + expect(isCustomRole(role)).toBe(true) + }) + + it('should return false for non-custom roles', () => { + const role = createMockRole({ roleType: 'admin' }) + expect(isCustomRole(role)).toBe(false) + }) + + it('should return false for null/undefined', () => { + expect(isCustomRole(null)).toBe(false) + expect(isCustomRole(undefined)).toBe(false) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..f43dae9 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + '@icons': path.resolve(__dirname, 'node_modules/vue-material-design-icons'), + }, + }, + test: { + environment: 'happy-dom', + include: ['src/**/*.{test,spec}.{js,ts}'], + globals: true, + }, +})