diff --git a/.github/workflows/vitest.yaml b/.github/workflows/vitest.yaml new file mode 100644 index 0000000..017fdac --- /dev/null +++ b/.github/workflows/vitest.yaml @@ -0,0 +1,39 @@ +name: 'Vitest Continuous Integration (CI) Tests' +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + permissions: + # Required to checkout the code + contents: read + # Required to put a comment into the pull-request + pull-requests: write + + steps: + - name: Checkout Feature Branch + uses: actions/checkout@v4 + + - name: 'Install Node' + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: 'Install Dependencies' + run: npm install + + - name: 'Vitest Tests' + run: npm run test:coverage + + - name: 'Report Coverage' + # Set if: always() to also generate the report if tests are failing + # Only works if you set `reportOnFailure: true` in your vite config as specified above + if: always() + uses: davelosert/vitest-coverage-report-action@v2 diff --git a/package-lock.json b/package-lock.json index 858aff6..d3ca942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "devDependencies": { "@eslint/js": "^9.21.0", "@vitejs/plugin-vue": "^5.2.1", + "@vitest/coverage-v8": "^3.1.1", "@vitest/eslint-plugin": "^1.1.36", "@vue/eslint-config-prettier": "^10.2.0", "@vue/test-utils": "^2.4.6", @@ -560,6 +561,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1370,6 +1381,16 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1971,6 +1992,39 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.1.tgz", + "integrity": "sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.1", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.1.1", + "vitest": "3.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/eslint-plugin": { "version": "1.1.38", "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.38.tgz", @@ -1993,14 +2047,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", + "integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -2009,13 +2063,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", + "integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", + "@vitest/spy": "3.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2046,9 +2100,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", + "integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", "dev": true, "license": "MIT", "dependencies": { @@ -2059,13 +2113,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", + "integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.9", + "@vitest/utils": "3.1.1", "pathe": "^2.0.3" }, "funding": { @@ -2073,13 +2127,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", + "integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2088,9 +2142,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", + "integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2101,13 +2155,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", + "@vitest/pretty-format": "3.1.1", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -3864,6 +3918,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -4120,6 +4181,60 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -4384,6 +4499,34 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5340,6 +5483,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5659,9 +5817,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", + "integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", "dev": true, "license": "MIT", "dependencies": { @@ -5757,31 +5915,31 @@ } }, "node_modules/vitest": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", + "integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.9", - "@vitest/mocker": "3.0.9", - "@vitest/pretty-format": "^3.0.9", - "@vitest/runner": "3.0.9", - "@vitest/snapshot": "3.0.9", - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@vitest/expect": "3.1.1", + "@vitest/mocker": "3.1.1", + "@vitest/pretty-format": "^3.1.1", + "@vitest/runner": "3.1.1", + "@vitest/snapshot": "3.1.1", + "@vitest/spy": "3.1.1", + "@vitest/utils": "3.1.1", "chai": "^5.2.0", "debug": "^4.4.0", - "expect-type": "^1.1.0", + "expect-type": "^1.2.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "std-env": "^3.8.0", + "std-env": "^3.8.1", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.9", + "vite-node": "3.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -5797,8 +5955,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.9", - "@vitest/ui": "3.0.9", + "@vitest/browser": "3.1.1", + "@vitest/ui": "3.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index 2681d45..0cbb255 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "test:unit": "vitest", + "test": "vitest", + "test:coverage": "vitest run --coverage", "lint": "eslint . --fix", "format": "prettier --write src/" }, @@ -17,6 +18,7 @@ "devDependencies": { "@eslint/js": "^9.21.0", "@vitejs/plugin-vue": "^5.2.1", + "@vitest/coverage-v8": "^3.1.1", "@vitest/eslint-plugin": "^1.1.36", "@vue/eslint-config-prettier": "^10.2.0", "@vue/test-utils": "^2.4.6", diff --git a/src/App.vue b/src/App.vue index 012f694..a20712c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,12 @@ diff --git a/tests/algorithms/bubble-sort.spec.ts b/tests/algorithms/bubble-sort.spec.ts new file mode 100644 index 0000000..be1ca58 --- /dev/null +++ b/tests/algorithms/bubble-sort.spec.ts @@ -0,0 +1,145 @@ +import { describe, it, expect } from 'vitest' +import { bubbleSortSteps } from '@/algorithms/bubble-sort' +import { SortStep } from '@/algorithms/index' + +describe('Bubble Sort Algorithm', () => { + it('returns initial step with unsorted array', () => { + const arr = [5, 3, 8, 4, 2] + const steps = bubbleSortSteps(arr) + + // First step should be the initial array with no comparisons + expect(steps[0]).toEqual({ + array: [5, 3, 8, 4, 2], + comparedIndices: [-1, -1], + swapped: false, + }) + }) + + it('sorts a simple array correctly', () => { + const arr = [5, 3, 8, 4, 2] + const steps = bubbleSortSteps(arr) + + // Check that the original array wasn't modified + expect(arr).toEqual([5, 3, 8, 4, 2]) + + // Check that final array in the last step is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([2, 3, 4, 5, 8]) + }) + + it('generates correct comparison steps', () => { + const arr = [5, 3] + const steps = bubbleSortSteps(arr) + + // Should have 3 steps for [5, 3]: + // 1. Initial state + // 2. Comparison of 5 and 3 + // 3. Swap of 5 and 3 + expect(steps.length).toBe(3) + + // Check initial step + expect(steps[0]).toEqual({ + array: [5, 3], + comparedIndices: [-1, -1], + swapped: false, + }) + + // Check comparison step + expect(steps[1]).toEqual({ + array: [5, 3], + comparedIndices: [0, 1], + swapped: false, + }) + + // Check swap step + expect(steps[2]).toEqual({ + array: [3, 5], + comparedIndices: [0, 1], + swapped: true, + }) + }) + + it('handles already sorted arrays efficiently', () => { + const arr = [1, 2, 3, 4, 5] + const steps = bubbleSortSteps(arr) + + // For an already sorted array, we should have: + // 1. Initial step + // + n-1 comparison steps (no swaps) + // Total: n steps (where n is array length) + + // Verify we don't have any swaps + const swapSteps = steps.filter((step) => step.swapped) + expect(swapSteps.length).toBe(0) + + // Verify final state is still sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + }) + + it('handles reverse-sorted arrays', () => { + const arr = [5, 4, 3, 2, 1] + const steps = bubbleSortSteps(arr) + + // Verify final array is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + + // Reverse-sorted is worst case - should have many swaps + const swapSteps = steps.filter((step) => step.swapped) + expect(swapSteps.length).toBeGreaterThan(0) + }) + + it('handles arrays with duplicate elements', () => { + const arr = [4, 2, 4, 1, 3] + const steps = bubbleSortSteps(arr) + + // Verify final array is sorted with duplicates preserved + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 4]) + }) + + it('handles empty arrays', () => { + const arr: number[] = [] + const steps = bubbleSortSteps(arr) + + // Should just have the initial step + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([]) + }) + + it('handles single-element arrays', () => { + const arr = [42] + const steps = bubbleSortSteps(arr) + + // Should just have the initial step (nothing to sort) + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([42]) + }) + + it('performs the expected number of comparisons', () => { + const arr = [5, 4, 3, 2, 1] + const steps = bubbleSortSteps(arr) + + // Count actual comparison steps (exclude initial and swap steps) + const comparisonSteps = steps.filter( + (step) => step.comparedIndices[0] !== -1 && !step.swapped, + ) + + // For bubble sort, we expect n-1 + n-2 + ... + 1 comparison steps + // For n=5, that's 4+3+2+1 = 10 comparisons + // We're optimising with the counter though, so it might be fewer + + // Just verify we're not doing excessive comparisons + expect(comparisonSteps.length).toBeLessThanOrEqual(10) + }) + + it('preserves the original input array', () => { + const arr = [5, 3, 8, 4, 2] + const originalArr = [...arr] + bubbleSortSteps(arr) + + // Verify the original array is untouched + expect(arr).toEqual(originalArr) + }) +}) diff --git a/tests/algorithms/insertion-sort.spec.ts b/tests/algorithms/insertion-sort.spec.ts new file mode 100644 index 0000000..8b6984d --- /dev/null +++ b/tests/algorithms/insertion-sort.spec.ts @@ -0,0 +1,171 @@ +import { describe, it, expect } from 'vitest' +import { insertionSortSteps } from '@/algorithms/insertion-sort' +import { SortStep } from '@/algorithms/index' + +describe('Insertion Sort Algorithm', () => { + it('returns initial step with unsorted array', () => { + const arr = [5, 3, 8, 4, 2] + const steps = insertionSortSteps(arr) + + // First step should be the initial array with no comparisons + expect(steps[0]).toEqual({ + array: [5, 3, 8, 4, 2], + comparedIndices: [-1, -1], + swapped: false, + }) + }) + + it('sorts a simple array correctly', () => { + const arr = [5, 3, 8, 4, 2] + const steps = insertionSortSteps(arr) + + // Check that the original array wasn't modified + expect(arr).toEqual([5, 3, 8, 4, 2]) + + // Check that final array in the last step is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([2, 3, 4, 5, 8]) + }) + + it('generates correct comparison and swap steps', () => { + const arr = [5, 3] + const steps = insertionSortSteps(arr) + + // Should have 3 steps for [5, 3]: + // 1. Initial state + // 2. Comparison of 3 and 5 + // 3. Swap of 3 and 5 + expect(steps.length).toBe(3) + + // Check initial step + expect(steps[0]).toEqual({ + array: [5, 3], + comparedIndices: [-1, -1], + swapped: false, + }) + + // Check comparison step + expect(steps[1]).toEqual({ + array: [5, 3], + comparedIndices: [1, 0], + swapped: false, + }) + + // Check swap step + expect(steps[2]).toEqual({ + array: [3, 5], + comparedIndices: [1, 0], + swapped: true, + }) + }) + + it('handles already sorted arrays efficiently', () => { + const arr = [1, 2, 3, 4, 5] + const steps = insertionSortSteps(arr) + + // For an already sorted array, insertion sort is very efficient + // We should only have the initial step and no swaps + + // Verify we don't have any swaps + const swapSteps = steps.filter((step) => step.swapped) + expect(swapSteps.length).toBe(0) + + // Verify final state is still sorted + const lastStep = steps[steps.length - 1] || steps[0] // Could be just the initial step + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + }) + + it('handles reverse-sorted arrays', () => { + const arr = [5, 4, 3, 2, 1] + const steps = insertionSortSteps(arr) + + // Verify final array is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + + // Reverse-sorted is worst case for insertion sort - should have many swaps + const swapSteps = steps.filter((step) => step.swapped) + expect(swapSteps.length).toBeGreaterThan(0) + }) + + it('handles arrays with duplicate elements', () => { + const arr = [4, 2, 4, 1, 3] + const steps = insertionSortSteps(arr) + + // Verify final array is sorted with duplicates preserved + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 4]) + }) + + it('handles empty arrays', () => { + const arr: number[] = [] + const steps = insertionSortSteps(arr) + + // Should just have the initial step + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([]) + }) + + it('handles single-element arrays', () => { + const arr = [42] + const steps = insertionSortSteps(arr) + + // Should just have the initial step (nothing to sort) + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([42]) + }) + + it('correctly processes each element in sequence', () => { + // This test verifies the insertion sort processes elements left to right + const arr = [5, 2, 4, 1, 3] + const steps = insertionSortSteps(arr) + + // Find all steps where we're processing an actual comparison (non-initial steps) + const processingSteps = steps.filter( + (step) => step.comparedIndices[0] !== -1, + ) + + // First set of comparisons should involve element at index 1 (value 2) + const firstComparisonGroup = processingSteps + .slice(0, 2) // First comparison and its swap + .every((step) => step.comparedIndices.includes(1)) + + expect(firstComparisonGroup).toBe(true) + + // Fix: Check intermediate state in the steps array, not the original array + // Find the state after the first element (2) has been processed (after the swap) + const afterFirstSwap = + steps.find((step) => step.swapped && step.comparedIndices.includes(1)) + ?.array || [] + + // Verify that in this intermediate state, 2 comes before 5 + const twoBeforeFive = afterFirstSwap.indexOf(2) < afterFirstSwap.indexOf(5) + expect(twoBeforeFive).toBe(true) + }) + + it('preserves the original input array', () => { + const arr = [5, 3, 8, 4, 2] + const originalArr = [...arr] + insertionSortSteps(arr) + + // Verify the original array is untouched + expect(arr).toEqual(originalArr) + }) + + it('makes the expected number of swaps for a reversed array', () => { + const arr = [5, 4, 3, 2, 1] + const steps = insertionSortSteps(arr) + + // Count actual swap steps + const swapSteps = steps.filter((step) => step.swapped) + + // For insertion sort on a reversed array of n elements: + // Element 1 requires 0 swaps + // Element 2 requires 1 swap + // Element 3 requires 2 swaps + // Element 4 requires 3 swaps + // Element 5 requires 4 swaps + // Total: 0+1+2+3+4 = 10 swaps + expect(swapSteps.length).toBe(10) + }) +}) diff --git a/tests/algorithms/merge-sort.spec.ts b/tests/algorithms/merge-sort.spec.ts new file mode 100644 index 0000000..1ce3ef3 --- /dev/null +++ b/tests/algorithms/merge-sort.spec.ts @@ -0,0 +1,180 @@ +import { describe, it, expect } from 'vitest' +import { mergeSortSteps, mergeSort } from '@/algorithms/merge-sort' +import { SortStep } from '@/algorithms/index' + +describe('Merge Sort Algorithm', () => { + describe('mergeSort (direct sorting)', () => { + it('sorts an empty array', () => { + expect(mergeSort([])).toEqual([]) + }) + + it('sorts a single element array', () => { + expect(mergeSort([42])).toEqual([42]) + }) + + it('sorts a small array correctly', () => { + expect(mergeSort([5, 3, 8, 4, 2])).toEqual([2, 3, 4, 5, 8]) + }) + + it('sorts an already sorted array', () => { + expect(mergeSort([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]) + }) + + it('sorts a reverse-sorted array', () => { + expect(mergeSort([5, 4, 3, 2, 1])).toEqual([1, 2, 3, 4, 5]) + }) + + it('handles duplicate values', () => { + expect(mergeSort([3, 1, 4, 1, 5, 9, 2, 6, 5])).toEqual([ + 1, 1, 2, 3, 4, 5, 5, 6, 9, + ]) + }) + }) + + describe('mergeSortSteps (visualisation steps)', () => { + it('returns initial step with unsorted array', () => { + const arr = [5, 3, 8, 4, 2] + const steps = mergeSortSteps(arr) + + // First step should be the initial array with no comparisons + expect(steps[0]).toEqual({ + array: [5, 3, 8, 4, 2], + comparedIndices: [-1, -1], + swapped: false, + }) + }) + + it('sorts a simple array correctly', () => { + const arr = [5, 3, 8, 4, 2] + const steps = mergeSortSteps(arr) + + // Check that the original array wasn't modified + expect(arr).toEqual([5, 3, 8, 4, 2]) + + // Check that final array in the last step is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([2, 3, 4, 5, 8]) + }) + + it('handles empty arrays', () => { + const arr: number[] = [] + const steps = mergeSortSteps(arr) + + // Should just have the initial step + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([]) + }) + + it('handles single-element arrays', () => { + const arr = [42] + const steps = mergeSortSteps(arr) + + // Should just have the initial step (nothing to sort) + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([42]) + }) + + it('preserves the original input array', () => { + const arr = [5, 3, 8, 4, 2] + const originalArr = [...arr] + mergeSortSteps(arr) + + // Verify the original array is untouched + expect(arr).toEqual(originalArr) + }) + + it('creates appropriate comparison steps', () => { + const arr = [3, 1, 4, 2] + const steps = mergeSortSteps(arr) + + // Check that we have comparison steps (non-initial steps) + const comparisonSteps = steps.filter( + (step) => + step.comparedIndices[0] !== -1 && step.comparedIndices[1] !== -1, + ) + expect(comparisonSteps.length).toBeGreaterThan(0) + + // The steps should include comparison indices + const hasValidComparisons = comparisonSteps.every((step) => { + const [i, j] = step.comparedIndices + return i >= 0 && j >= 0 && i < arr.length && j < arr.length + }) + expect(hasValidComparisons).toBe(true) + }) + + it('includes merge operation steps', () => { + const arr = [3, 1, 4, 2] + const steps = mergeSortSteps(arr) + + // Check for steps marked as swapped (merges) + const mergeSteps = steps.filter((step) => step.swapped) + expect(mergeSteps.length).toBeGreaterThan(0) + }) + + it('handles already sorted arrays', () => { + const arr = [1, 2, 3, 4, 5] + const steps = mergeSortSteps(arr) + + // Even for sorted arrays, merge sort does the same work + // We should have more than just the initial step + expect(steps.length).toBeGreaterThan(1) + + // Final array should still be sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + }) + + it('handles reverse-sorted arrays', () => { + const arr = [5, 4, 3, 2, 1] + const steps = mergeSortSteps(arr) + + // Check the final array is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + }) + + it('creates expected number of steps for small arrays', () => { + // For merge sort, we can calculate the minimum number of steps needed + // based on the number of merges required + + // Test with a 2-element array (should have at least 3 steps: initial + comparison + merge) + const stepsFor2 = mergeSortSteps([2, 1]) + expect(stepsFor2.length).toBeGreaterThanOrEqual(3) + + // Test with a 4-element array + const stepsFor4 = mergeSortSteps([4, 3, 2, 1]) + expect(stepsFor4.length).toBeGreaterThan(stepsFor2.length) + }) + + it('has intermediate steps with partially sorted subarrays', () => { + const arr = [5, 2, 4, 1, 3] + const steps = mergeSortSteps(arr) + + // Find steps where parts of the array are sorted + // We can't easily predict the exact step, so we'll check if there exists a step + // where some subarray is sorted while the whole array isn't yet sorted + + const partialSortSteps = steps.filter((step) => { + const array = step.array + // Avoid the initial and final steps + if ( + array.join(',') === arr.join(',') || + array.join(',') === [1, 2, 3, 4, 5].join(',') + ) { + return false + } + + // Look for sorted subarrays of length at least 2 + for (let i = 0; i < array.length - 1; i++) { + if (array[i] < array[i + 1]) { + return true + } + } + + return false + }) + + expect(partialSortSteps.length).toBeGreaterThan(0) + }) + }) +}) diff --git a/tests/algorithms/quick-sort.spec.ts b/tests/algorithms/quick-sort.spec.ts new file mode 100644 index 0000000..5b8a4da --- /dev/null +++ b/tests/algorithms/quick-sort.spec.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from 'vitest' +import { quickSortSteps } from '@/algorithms/quick-sort' +import { SortStep } from '@/algorithms/index' + +describe('Quick Sort Algorithm', () => { + it('returns initial step with unsorted array', () => { + const arr = [5, 3, 8, 4, 2] + const steps = quickSortSteps(arr) + + // First step should be the initial array with no comparisons + expect(steps[0]).toEqual({ + array: [5, 3, 8, 4, 2], + comparedIndices: [-1, -1], + swapped: false, + }) + }) + + it('sorts a simple array correctly', () => { + const arr = [5, 3, 8, 4, 2] + const steps = quickSortSteps(arr) + + // Check that the original array wasn't modified + expect(arr).toEqual([5, 3, 8, 4, 2]) + + // Check that final array in the last step is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([2, 3, 4, 5, 8]) + }) + + it('handles already sorted arrays', () => { + const arr = [1, 2, 3, 4, 5] + const steps = quickSortSteps(arr) + + // Even for sorted arrays, quick sort does the same work + // We should have more than just the initial step + expect(steps.length).toBeGreaterThan(1) + + // Final array should still be sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + }) + + it('handles reverse-sorted arrays', () => { + const arr = [5, 4, 3, 2, 1] + const steps = quickSortSteps(arr) + + // Verify final array is sorted + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 5]) + + // Reverse-sorted should have several partition and swap steps + const swapSteps = steps.filter((step) => step.swapped) + expect(swapSteps.length).toBeGreaterThan(0) + }) + + it('handles arrays with duplicate elements', () => { + const arr = [4, 2, 4, 1, 3] + const steps = quickSortSteps(arr) + + // Verify final array is sorted with duplicates preserved + const lastStep = steps[steps.length - 1] + expect(lastStep.array).toEqual([1, 2, 3, 4, 4]) + }) + + it('handles empty arrays', () => { + const arr: number[] = [] + const steps = quickSortSteps(arr) + + // Should just have the initial step + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([]) + }) + + it('handles single-element arrays', () => { + const arr = [42] + const steps = quickSortSteps(arr) + + // Should just have the initial step (nothing to sort) + expect(steps.length).toBe(1) + expect(steps[0].array).toEqual([42]) + }) + + it('creates appropriate partition and comparison steps', () => { + const arr = [3, 1, 4, 2] + const steps = quickSortSteps(arr) + + // Check that we have comparison steps (non-initial steps) + const comparisonSteps = steps.filter( + (step) => step.comparedIndices[0] !== -1 && !step.swapped, + ) + expect(comparisonSteps.length).toBeGreaterThan(0) + + // Check that we have swap steps (partitioning) + const swapSteps = steps.filter((step) => step.swapped) + expect(swapSteps.length).toBeGreaterThan(0) + + // Check that comparedIndices always refers to valid indices + const hasValidComparisons = steps.every((step) => { + const [i, j] = step.comparedIndices + // -1 is valid for initial step + if (i === -1 && j === -1) return true + return i >= 0 && j >= 0 && i < arr.length && j < arr.length + }) + expect(hasValidComparisons).toBe(true) + }) + + it('preserves the original input array', () => { + const arr = [5, 3, 8, 4, 2] + const originalArr = [...arr] + quickSortSteps(arr) + + // Verify the original array is untouched + expect(arr).toEqual(originalArr) + }) + + it('uses the last element as pivot in partitioning', () => { + // This is specific to this quick sort implementation + const arr = [5, 3, 8, 4, 2] + const steps = quickSortSteps(arr) + + // Find the first comparison step + const firstComparisonStep = steps.find( + (step) => step.comparedIndices[0] !== -1 && !step.swapped, + ) + + // In this implementation, one of the compared indices should be + // the last element of the array (the pivot) + expect(firstComparisonStep?.comparedIndices).toContain(arr.length - 1) + }) + + it('correctly partitions around the pivot', () => { + // Test a simple case where we can track the pivot + const arr = [5, 3, 8, 4, 2] + const steps = quickSortSteps(arr) + + // Find the first swap that involves the pivot (when it's moved to its final position) + // This is usually the swap where one index is the last element + const pivotSwapStep = steps.find( + (step) => step.swapped && step.comparedIndices.includes(arr.length - 1), + ) + + if (pivotSwapStep) { + const pivotValue = arr[arr.length - 1] // Original pivot value (2) + const pivotIndex = pivotSwapStep.comparedIndices.find( + (i) => i !== arr.length - 1, + ) + + if (pivotIndex !== undefined) { + // In the array after this swap, check that: + // 1. Elements before pivotIndex are <= pivotValue + // 2. Elements after pivotIndex are >= pivotValue + const arrayAfterSwap = pivotSwapStep.array + + const allSmallerOnLeft = arrayAfterSwap + .slice(0, pivotIndex) + .every((value) => value <= pivotValue) + + const allLargerOnRight = arrayAfterSwap + .slice(pivotIndex + 1) + .every((value) => value >= pivotValue) + + expect(allSmallerOnLeft).toBe(true) + expect(allLargerOnRight).toBe(true) + } + } + }) + + it('has the expected number of steps for a simple case', () => { + // For [3,1,2] with last-element pivot: + // 1. Initial step + // 2. Compare 3 with pivot 2 + // 3. Compare 1 with pivot 2 + // 4. Swap 3 and 1 + // 5. Swap 1 and 2 (pivot) + // 6. Recursive call on left subarray (just 1) + // 7. Recursive call on right subarray (just 3) + const arr = [3, 1, 2] + const steps = quickSortSteps(arr) + + // The exact number of steps depends on implementation details, + // but we can make some general assertions + expect(steps.length).toBeGreaterThanOrEqual(5) + + // And the final array should be sorted + expect(steps[steps.length - 1].array).toEqual([1, 2, 3]) + }) +}) diff --git a/tests/hooks/useTimer.spec.ts b/tests/hooks/useTimer.spec.ts new file mode 100644 index 0000000..5c183f7 --- /dev/null +++ b/tests/hooks/useTimer.spec.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { nextTick } from 'vue' +import { useTimer } from '@/hooks/useTimer' + +describe('useTimer', () => { + // Store original implementations using spyOn + let dateNowSpy: any + let setIntervalSpy: any + let clearIntervalSpy: any + + // Mock values + let currentTime = 1000 + let intervalCallback: Function | null = null + let intervalId = 123 + + beforeEach(() => { + // Mock Date.now to return controlled time + dateNowSpy = vi.spyOn(Date, 'now').mockImplementation(() => currentTime) + + // Mock setInterval to capture callback + setIntervalSpy = vi + .spyOn(window, 'setInterval') + .mockImplementation((callback: any, ms?: number) => { + intervalCallback = callback as Function + return intervalId as any + }) + + // Mock clearInterval + clearIntervalSpy = vi + .spyOn(window, 'clearInterval') + .mockImplementation(() => {}) + + // Reset time for each test + currentTime = 1000 + }) + + afterEach(() => { + // Restore original functions + dateNowSpy.mockRestore() + setIntervalSpy.mockRestore() + clearIntervalSpy.mockRestore() + + // Reset interval callback + intervalCallback = null + }) + + it('initializes with correct values', () => { + const timer = useTimer() + + expect(timer.seconds.value).toBe(0) + expect(timer.formattedTime.value).toBe('00:00.000') + expect(timer.isRunning.value).toBe(false) + }) + + it('starts the timer correctly', () => { + const timer = useTimer() + + timer.start() + + expect(timer.isRunning.value).toBe(true) + expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 50) + }) + + it('updates milliseconds correctly when running', async () => { + const timer = useTimer() + + timer.start() + + // Simulate time passing (500ms) + currentTime += 500 + if (intervalCallback) intervalCallback() + await nextTick() + + expect(timer.seconds.value).toBe(0) + expect(timer.formattedTime.value).toBe('00:00.500') + + // Simulate more time passing (600ms more, total 1100ms) + currentTime += 600 + if (intervalCallback) intervalCallback() + await nextTick() + + expect(timer.seconds.value).toBe(1) + expect(timer.formattedTime.value).toBe('00:01.100') + }) + + it('pauses the timer correctly', () => { + const timer = useTimer() + + // Start and advance timer + timer.start() + currentTime += 1500 + if (intervalCallback) intervalCallback() + + // Pause timer + timer.pause() + + expect(timer.isRunning.value).toBe(false) + expect(clearIntervalSpy).toHaveBeenCalledWith(intervalId) + expect(timer.seconds.value).toBe(1) + expect(timer.formattedTime.value).toBe('00:01.500') + + // Verify time is frozen + currentTime += 1000 + expect(timer.seconds.value).toBe(1) + expect(timer.formattedTime.value).toBe('00:01.500') + }) + + it('resumes from the correct position after pause', async () => { + const timer = useTimer() + + // Start and advance timer + timer.start() + currentTime += 1500 + if (intervalCallback) intervalCallback() + + // Pause timer + timer.pause() + expect(timer.formattedTime.value).toBe('00:01.500') + + // Resume timer (simulate a new interval) + currentTime += 500 // Simulate 500ms delay before resuming + timer.start() + + // Verify it doesn't jump ahead due to the delay + expect(timer.formattedTime.value).toBe('00:01.500') + + // Advance time after resuming + currentTime += 800 + if (intervalCallback) intervalCallback() + await nextTick() + + // Should be 1500ms + 800ms = 2300ms + expect(timer.seconds.value).toBe(2) + expect(timer.formattedTime.value).toBe('00:02.300') + }) + + it('resets the timer correctly', () => { + const timer = useTimer() + + // Start and advance timer + timer.start() + currentTime += 5500 + if (intervalCallback) intervalCallback() + expect(timer.seconds.value).toBe(5) + + // Reset timer + timer.reset() + + expect(timer.isRunning.value).toBe(false) + expect(timer.seconds.value).toBe(0) + expect(timer.formattedTime.value).toBe('00:00.000') + expect(clearIntervalSpy).toHaveBeenCalled() + }) + + it('restarts the timer correctly', () => { + const timer = useTimer() + + // Start and advance timer + timer.start() + currentTime += 5500 + if (intervalCallback) intervalCallback() + expect(timer.seconds.value).toBe(5) + + // Reset calls to track restart properly + vi.clearAllMocks() + + // Restart timer + timer.restart() + + expect(timer.isRunning.value).toBe(true) + expect(timer.seconds.value).toBe(0) + expect(timer.formattedTime.value).toBe('00:00.000') + expect(clearIntervalSpy).toHaveBeenCalled() + expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 50) + }) + + it('formats time with millisecond precision', async () => { + const timer = useTimer() + + timer.start() + + // Test various times + const testTimes = [ + { advance: 123, expected: '00:00.123' }, + { advance: 877, expected: '00:01.000' }, // 1000ms total + { advance: 59000, expected: '01:00.000' }, // 60000ms total + { advance: 60000, expected: '02:00.000' }, // 120000ms total + { advance: 506, expected: '02:00.506' }, // 120506ms total + ] + + for (const test of testTimes) { + currentTime += test.advance + if (intervalCallback) intervalCallback() + await nextTick() + + expect(timer.formattedTime.value).toBe(test.expected) + } + }) + + it('does nothing when starting an already running timer', () => { + const timer = useTimer() + + timer.start() + const firstCall = setIntervalSpy.mock.calls.length + + // Try to start again + timer.start() + + // setInterval should not be called again + expect(setIntervalSpy.mock.calls.length).toBe(firstCall) + }) + + it('does nothing when pausing an already stopped timer', () => { + const timer = useTimer() + + // Try to pause without starting + timer.pause() + + // clearInterval should not be called + expect(clearIntervalSpy).not.toHaveBeenCalled() + }) +}) diff --git a/vitest.config.js b/vitest.config.js index c328717..0c8ad21 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -9,6 +9,16 @@ export default mergeConfig( environment: 'jsdom', exclude: [...configDefaults.exclude, 'e2e/**'], root: fileURLToPath(new URL('./', import.meta.url)), + include: [ + '**/*.spec.ts', + '**/*.spec.vue', + 'tests/**/*.{ts,vue}', + 'src/**/*.{spec,test}.{ts,vue}', + ], + fileNameMatcher: '*.{spec,test}.{ts,vue}', + coverage: { + reporter: ['text', 'json', 'json-summary', 'lcov'], + }, }, }), )