From b41623134156eb1cb602562536b2d888ac1ddbf9 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 19 Aug 2026 22:30:54 +0530 Subject: [PATCH] test: sonar and test coverage Signed-off-by: Arnab Nandy --- .github/workflows/sonarcloud.yml | 32 +- package-lock.json | 814 ++++++++++++++++++ package.json | 8 +- sonar-project.properties | 8 + tests/app/api/search/route.test.ts | 48 +- tests/components/theme-toggle.test.tsx | 37 + .../issues/components/issue-card.test.tsx | 87 ++ .../issues/components/issue-finder.test.tsx | 222 +++++ .../issues/lib/saved-searches.test.ts | 87 ++ .../issues/server/github-search.test.ts | 129 +++ vitest.config.ts | 21 +- 11 files changed, 1472 insertions(+), 21 deletions(-) create mode 100644 sonar-project.properties create mode 100644 tests/components/theme-toggle.test.tsx create mode 100644 tests/features/issues/components/issue-card.test.tsx create mode 100644 tests/features/issues/components/issue-finder.test.tsx create mode 100644 tests/features/issues/lib/saved-searches.test.ts diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 74ae7fb..8a7bc34 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -36,6 +36,7 @@ on: workflow_dispatch: permissions: + contents: read # allows checkout to read the repository pull-requests: read # allows SonarCloud to decorate PRs with analysis results jobs: @@ -48,25 +49,22 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests with coverage + run: npm run test:coverage + - name: Analyze with SonarCloud # You can pin the exact commit or the version. - # uses: SonarSource/sonarcloud-github-action@v2.2.0 - uses: SonarSource/sonarcloud-github-action@4006f663ecaf1f8093e8e4abb9227f6041f52216 + # uses: SonarSource/sonarqube-scan-action@v8.2.1 + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN - with: - # Additional arguments for the SonarScanner CLI - args: > - # Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu) - # mandatory - -Dsonar.projectKey=arnabnandy7_openissue.dev - -Dsonar.organization=arnabnandy7 - # Comma-separated paths to directories containing main source files. - -Dsonar.sources=src - # Comma-separated paths to directories containing test source files. - #-Dsonar.tests= # optional. For more info about Code Coverage, please refer to https://docs.sonarcloud.io/enriching/test-coverage/overview/ - # Adds more detail to both client and server-side analysis logs, activating DEBUG mode for the scanner, and adding client-side environment variables and system properties to the server-side log of analysis report processing. - #-Dsonar.verbose= # optional, default is false - # When you need the analysis to take place in a directory other than the one from which it was launched, default is . - projectBaseDir: . diff --git a/package-lock.json b/package-lock.json index 802aebf..ed1397f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,16 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/coverage-v8": "^4.1.10", "eslint": "^10", "eslint-config-next": "16.3.1", + "jsdom": "^26.1.0", "tailwindcss": "^4", "typescript": "^7", "vitest": "^4.1.10" @@ -45,6 +50,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -405,6 +431,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -450,6 +486,131 @@ "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.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.75.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.75.1.tgz", @@ -3976,6 +4137,78 @@ "tailwindcss": "4.3.3" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -4026,6 +4259,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -4832,6 +5072,37 @@ "win32" ] }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", @@ -4981,6 +5252,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -5055,6 +5336,19 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5272,6 +5566,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -5825,6 +6138,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -5839,6 +6166,20 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5925,6 +6266,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -6040,6 +6388,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6078,6 +6436,13 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -6174,6 +6539,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -7749,6 +8127,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -7844,6 +8232,26 @@ "node": ">=16.9.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "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-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -7864,6 +8272,34 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -8330,6 +8766,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -8545,6 +8988,45 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "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-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -8610,6 +9092,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9114,6 +9636,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -9124,6 +9656,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "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/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9427,6 +10000,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9817,6 +10397,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -10048,6 +10641,28 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -10555,6 +11170,13 @@ "node": ">= 18" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -10651,6 +11273,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -11392,6 +12027,19 @@ } } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -11405,6 +12053,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/systeminformation": { "version": "5.31.10", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.10.tgz", @@ -11543,6 +12198,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11564,6 +12239,32 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -12462,6 +13163,80 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12599,6 +13374,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", @@ -12615,6 +13412,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 96c8d3d..a2da3df 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start", "lint": "eslint", - "test": "vitest run" + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { "class-variance-authority": "^0.7.1", @@ -28,11 +29,16 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/coverage-v8": "^4.1.10", "eslint": "^10", "eslint-config-next": "16.3.1", + "jsdom": "^26.1.0", "tailwindcss": "^4", "typescript": "^7", "vitest": "^4.1.10" diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..b77fe23 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,8 @@ +sonar.projectKey=arnabnandy7_openissue.dev +sonar.organization=arnabnandy7 +sonar.sources=src +sonar.tests=tests +sonar.test.inclusions=tests/**/*.test.ts +sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.coverage.exclusions=src/components/ui/**,src/app/layout.tsx,src/app/page.tsx,src/components/theme-provider.tsx,src/features/issues/types/**,src/features/issues/data/** +sonar.sourceEncoding=UTF-8 diff --git a/tests/app/api/search/route.test.ts b/tests/app/api/search/route.test.ts index 799fcc8..095ad0d 100644 --- a/tests/app/api/search/route.test.ts +++ b/tests/app/api/search/route.test.ts @@ -1,9 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const searchGitHubIssues = vi.fn(); +const { searchGitHubIssues, headersMock } = vi.hoisted(() => ({ + searchGitHubIssues: vi.fn(), + headersMock: vi.fn(async () => new Headers({ "x-forwarded-for": "203.0.113.10" })), +})); vi.mock("next/headers", () => ({ - headers: vi.fn(async () => new Headers({ "x-forwarded-for": "203.0.113.10" })), + headers: headersMock, })); vi.mock("@/features/issues/server/github-search", () => ({ @@ -22,6 +25,7 @@ describe("GET /api/search", () => { issues: [], page: 1, }); + headersMock.mockResolvedValue(new Headers({ "x-forwarded-for": "203.0.113.10" })); }); it("requires a technology query", async () => { @@ -65,4 +69,44 @@ describe("GET /api/search", () => { expect(response.status).toBe(502); expect(body.error).toBe("GitHub is unavailable"); }); + + it("rate limits repeated requests from one IP", async () => { + const { GET } = await import("@/app/api/search/route"); + let response = new Response(); + + for (let attempt = 0; attempt < 7; attempt += 1) { + response = await GET(new Request("http://localhost/api/search?tech=React")); + if (response.status === 429) break; + } + + expect(response.status).toBe(429); + expect(await response.json()).toMatchObject({ error: expect.stringContaining("Too many") }); + }); + + it("normalizes page numbers and forwarded IP lists", async () => { + headersMock.mockResolvedValueOnce( + new Headers({ "x-forwarded-for": "198.51.100.2, 10.0.0.1" }), + ); + const { GET } = await import("@/app/api/search/route"); + await GET(new Request("http://localhost/api/search?tech=Go&page=-4")); + expect(searchGitHubIssues).toHaveBeenLastCalledWith( + expect.objectContaining({ page: 1 }), + ); + + headersMock.mockResolvedValueOnce(new Headers()); + await GET(new Request("http://localhost/api/search?tech=Go&page=invalid")); + expect(searchGitHubIssues).toHaveBeenLastCalledWith( + expect.objectContaining({ page: 1 }), + ); + }); + + it("uses a generic message for non-Error failures", async () => { + headersMock.mockResolvedValueOnce( + new Headers({ "x-forwarded-for": "192.0.2.55" }), + ); + searchGitHubIssues.mockRejectedValueOnce("offline"); + const { GET } = await import("@/app/api/search/route"); + const response = await GET(new Request("http://localhost/api/search?tech=Go")); + expect(await response.json()).toEqual({ error: "Unable to search GitHub issues." }); + }); }); diff --git a/tests/components/theme-toggle.test.tsx b/tests/components/theme-toggle.test.tsx new file mode 100644 index 0000000..ebb3109 --- /dev/null +++ b/tests/components/theme-toggle.test.tsx @@ -0,0 +1,37 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { renderToString } from "react-dom/server"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const setTheme = vi.fn(); + +vi.mock("next-themes", () => ({ + useTheme: () => ({ theme: "dark", setTheme }), +})); + +vi.mock("@/components/ui/tooltip", () => ({ + Tooltip: ({ children }: any) => <>{children}, + TooltipTrigger: ({ children }: any) => <>{children}, + TooltipContent: ({ children }: any) => {children}, +})); + +import { ThemeToggle } from "@/components/theme-toggle"; + +afterEach(cleanup); + +describe("ThemeToggle", () => { + it("shows all themes, marks the active one, and switches themes", () => { + render(); + + expect(screen.getByRole("button", { name: "Dark theme" }).getAttribute("aria-pressed")).toBe("true"); + for (const theme of ["Light", "Dark", "System"]) { + fireEvent.click(screen.getByRole("button", { name: `${theme} theme` })); + } + expect(setTheme.mock.calls.map(([theme]) => theme)).toEqual(["light", "dark", "system"]); + }); + + it("renders a stable placeholder on the server", () => { + expect(renderToString()).toContain("h-9 w-[116px]"); + }); +}); diff --git a/tests/features/issues/components/issue-card.test.tsx b/tests/features/issues/components/issue-card.test.tsx new file mode 100644 index 0000000..bb063d9 --- /dev/null +++ b/tests/features/issues/components/issue-card.test.tsx @@ -0,0 +1,87 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { IssueCard } from "@/features/issues/components/issue-card"; +import { LoadingResults } from "@/features/issues/components/loading-results"; +import { Metric } from "@/features/issues/components/metric"; +import type { Issue } from "@/features/issues/types/search"; + +function issue(overrides: Partial = {}): Issue { + return { + id: "issue-1", + title: "Improve accessibility", + url: "https://github.com/acme/repo/issues/1", + repo: "acme/repo", + repoUrl: "https://github.com/acme/repo", + stars: 12500, + comments: 3, + labels: ["help wanted", "a", "b", "c", "d", "e", "hidden"], + updatedAt: "2026-08-18T00:00:00.000Z", + createdAt: "2026-08-01T00:00:00.000Z", + assigned: false, + linkedPrCount: 2, + hacktoberfest: true, + hacktoberfestSource: "repo-topic", + qualityScore: 80, + helpStatus: "open", + ...overrides, + }; +} + +afterEach(cleanup); + +describe("issue presentation", () => { + it("renders a high-quality open Hacktoberfest issue", () => { + vi.setSystemTime(new Date("2026-08-19T00:00:00.000Z")); + render(); + + expect(screen.getByText("12.5K")).toBeTruthy(); + expect(screen.getByText("Hacktoberfest repo")).toBeTruthy(); + expect(screen.getByText("Needs Help")).toBeTruthy(); + expect(screen.queryByText("hidden")).toBeNull(); + vi.useRealTimers(); + }); + + it("renders medium, claimed, nullable metadata", () => { + render( + , + ); + expect(screen.getByText("Hacktoberfest label")).toBeTruthy(); + expect(screen.getByText("Possibly Claimed")).toBeTruthy(); + expect(screen.getByText("Assigned")).toBeTruthy(); + }); + + it("renders low-quality resolved non-Hacktoberfest issues", () => { + render( + , + ); + expect(screen.getByText("20 quality")).toBeTruthy(); + expect(screen.getByText("Likely Resolved")).toBeTruthy(); + }); + + it("renders loading placeholders and metrics", () => { + const { container } = render(); + expect(container.querySelectorAll("[data-slot=card]")).toHaveLength(4); + cleanup(); + render(); + expect(screen.getByText("Ranked")).toBeTruthy(); + expect(screen.getByText("24")).toBeTruthy(); + }); +}); diff --git a/tests/features/issues/components/issue-finder.test.tsx b/tests/features/issues/components/issue-finder.test.tsx new file mode 100644 index 0000000..67f9846 --- /dev/null +++ b/tests/features/issues/components/issue-finder.test.tsx @@ -0,0 +1,222 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Issue, SearchResponse } from "@/features/issues/types/search"; + +const { addSavedSearch, deleteSavedSearch, getSavedSearches } = vi.hoisted(() => ({ + addSavedSearch: vi.fn(), + deleteSavedSearch: vi.fn(), + getSavedSearches: vi.fn(), +})); + +vi.mock("@/features/issues/lib/saved-searches", () => ({ + addSavedSearch, + deleteSavedSearch, + getSavedSearches, +})); + +vi.mock("@/components/theme-toggle", () => ({ + ThemeToggle: () => , +})); + +vi.mock("@/components/ui/select", () => ({ + Select: ({ value, onValueChange, children }: any) => ( +
{children}
+ ), + SelectTrigger: ({ children, ...props }: any) => , + SelectValue: ({ children }: any) => {children}, + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) => {children}, +})); + +import { IssueFinder } from "@/features/issues/components/issue-finder"; + +function issue(id: number, qualityScore = 50): Issue { + return { + id: `issue-${id}`, + title: `Issue ${id}`, + url: `https://github.com/acme/repo/issues/${id}`, + repo: "acme/repo", + repoUrl: "https://github.com/acme/repo", + stars: 100, + comments: 0, + labels: ["help wanted"], + updatedAt: `2026-08-${String((id % 20) + 1).padStart(2, "0")}T00:00:00.000Z`, + createdAt: "2026-08-01T00:00:00.000Z", + assigned: false, + linkedPrCount: 0, + hacktoberfest: false, + hacktoberfestSource: null, + qualityScore, + helpStatus: "open", + }; +} + +function response(overrides: Partial = {}): SearchResponse { + return { + query: "is:issue language:Java", + totalCount: 1000, + candidateCount: 30, + rateLimitRemaining: "4999", + tokenConfigured: true, + issues: Array.from({ length: 24 }, (_, index) => issue(index + 1, index)), + page: 1, + ...overrides, + }; +} + +function jsonResponse(payload: SearchResponse, ok = true) { + return { ok, json: vi.fn().mockResolvedValue(payload) }; +} + +beforeEach(() => { + getSavedSearches.mockReset().mockReturnValue([]); + addSavedSearch.mockReset(); + deleteSavedSearch.mockReset(); + vi.stubGlobal("fetch", vi.fn()); +}); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe("IssueFinder", () => { + it("validates and manages saved searches", async () => { + const saved = { + id: "saved-1", + name: "React bugs", + tech: "React", + label: "bug", + sort: "created", + linkedPr: "no", + hacktoberfest: "only", + createdAt: "2026-08-19T00:00:00.000Z", + }; + getSavedSearches.mockReturnValueOnce([]).mockReturnValueOnce([]); + addSavedSearch.mockReturnValue(saved); + + render(); + fireEvent.click(screen.getByRole("button", { name: /save current search/i })); + expect(screen.getByText("Enter a name for the saved search.")).toBeTruthy(); + + fireEvent.change(screen.getByLabelText("Saved search name"), { target: { value: "React bugs" } }); + fireEvent.change(screen.getByLabelText("Technology"), { target: { value: " " } }); + fireEvent.click(screen.getByRole("button", { name: /save current search/i })); + expect(screen.getByText("Enter a technology before saving the search.")).toBeTruthy(); + + fireEvent.change(screen.getByLabelText("Technology"), { target: { value: "React" } }); + fireEvent.click(screen.getByRole("button", { name: /save current search/i })); + expect(addSavedSearch).toHaveBeenCalled(); + expect(screen.getByText("React bugs")).toBeTruthy(); + + fireEvent.click(screen.getByRole("button", { name: "Delete React bugs" })); + expect(deleteSavedSearch).toHaveBeenCalledWith("saved-1"); + }); + + it("shows save failures", () => { + addSavedSearch.mockImplementation(() => { + throw new Error("Storage unavailable"); + }); + render(); + fireEvent.change(screen.getByLabelText("Saved search name"), { target: { value: "Java" } }); + fireEvent.click(screen.getByRole("button", { name: /save current search/i })); + expect(screen.getByText("Storage unavailable")).toBeTruthy(); + }); + + it("searches, ranks, and loads another page", async () => { + const fetchMock = vi.mocked(fetch); + fetchMock + .mockResolvedValueOnce(jsonResponse(response()) as any) + .mockResolvedValueOnce( + jsonResponse(response({ issues: [issue(25, 99)], page: 2, candidateCount: 25 })) as any, + ); + + render(); + fireEvent.submit(screen.getByRole("button", { name: "Search" }).closest("form")!); + + expect(await screen.findByText("Ranked issues")).toBeTruthy(); + expect(screen.getByText("Issue 24")).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Load More" })); + expect(await screen.findByText("Issue 25")).toBeTruthy(); + expect(fetchMock.mock.calls[1][0]).toContain("page=2"); + }); + + it("handles empty searches and API failures", async () => { + const fetchMock = vi.mocked(fetch); + render(); + fireEvent.change(screen.getByLabelText("Technology"), { target: { value: " " } }); + fireEvent.submit(screen.getByRole("button", { name: "Search" }).closest("form")!); + expect(screen.getByText("Enter a technology to search.")).toBeTruthy(); + + fireEvent.change(screen.getByLabelText("Technology"), { target: { value: "Rust" } }); + fetchMock.mockResolvedValueOnce( + jsonResponse(response({ issues: [], candidateCount: 0, error: "No access" }), false) as any, + ); + fireEvent.submit(screen.getByRole("button", { name: "Search" }).closest("form")!); + expect(await screen.findByText("No access")).toBeTruthy(); + + await waitFor(() => + expect(screen.getByRole("button", { name: "Cooldown..." })).toBeTruthy(), + ); + }); + + it("updates every quick technology and supported label", () => { + render(); + for (const technology of ["Spring Boot", "React", "Python", "Kubernetes", "Java"]) { + fireEvent.click(screen.getByRole("button", { name: technology })); + expect((screen.getByLabelText("Technology") as HTMLInputElement).value).toBe(technology); + } + for (const label of [ + "help wanted", + "good first issue", + "up-for-grabs", + "first-timers-only", + "hacktoberfest", + "bug", + "documentation", + ]) { + fireEvent.click(screen.getByRole("button", { name: label })); + } + }); + + it("runs a saved search and reports pagination failures", async () => { + const saved = { + id: "saved-2", + name: "Saved Rust", + tech: "Rust", + label: "bug", + sort: "comments", + linkedPr: "yes", + hacktoberfest: "any", + createdAt: "2026-08-19T00:00:00.000Z", + }; + getSavedSearches.mockReturnValue([saved]); + const fetchMock = vi.mocked(fetch); + fetchMock + .mockResolvedValueOnce(jsonResponse(response()) as any) + .mockResolvedValueOnce( + jsonResponse(response({ error: "Pagination failed" }), false) as any, + ); + + render(); + fireEvent.click(screen.getByRole("button", { name: "Run" })); + expect(await screen.findByText("Ranked issues")).toBeTruthy(); + expect(fetchMock.mock.calls[0][0]).toContain("tech=Rust"); + fireEvent.click(screen.getByRole("button", { name: "Load More" })); + expect(await screen.findByText("Pagination failed")).toBeTruthy(); + }); + + it("uses the fallback message for non-Error pagination failures", async () => { + const fetchMock = vi.mocked(fetch); + fetchMock + .mockResolvedValueOnce(jsonResponse(response()) as any) + .mockRejectedValueOnce("offline"); + render(); + fireEvent.submit(screen.getByRole("button", { name: "Search" }).closest("form")!); + await screen.findByText("Ranked issues"); + fireEvent.click(screen.getByRole("button", { name: "Load More" })); + expect(await screen.findByText("Failed to load more issues.")).toBeTruthy(); + }); +}); diff --git a/tests/features/issues/lib/saved-searches.test.ts b/tests/features/issues/lib/saved-searches.test.ts new file mode 100644 index 0000000..fe630e4 --- /dev/null +++ b/tests/features/issues/lib/saved-searches.test.ts @@ -0,0 +1,87 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + addSavedSearch, + deleteSavedSearch, + getSavedSearches, +} from "@/features/issues/lib/saved-searches"; + +const validSearch = { + id: "saved-1", + name: "React help", + tech: "React", + label: "help-wanted", + sort: "updated", + linkedPr: "any", + hacktoberfest: "any", + createdAt: "2026-08-19T00:00:00.000Z", +}; + +describe("saved searches", () => { + beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("returns only structurally valid saved searches", () => { + localStorage.setItem( + "openissue:saved-searches", + JSON.stringify([ + validSearch, + null, + "invalid", + { ...validSearch, label: "unknown" }, + { ...validSearch, sort: "unknown" }, + { ...validSearch, linkedPr: "unknown" }, + { ...validSearch, hacktoberfest: "unknown" }, + { ...validSearch, id: 1 }, + ]), + ); + + expect(getSavedSearches()).toEqual([validSearch]); + }); + + it("handles empty, malformed, and non-array storage", () => { + expect(getSavedSearches()).toEqual([]); + localStorage.setItem("openissue:saved-searches", "not json"); + expect(getSavedSearches()).toEqual([]); + localStorage.setItem("openissue:saved-searches", JSON.stringify({})); + expect(getSavedSearches()).toEqual([]); + }); + + it("adds and deletes searches", () => { + vi.spyOn(crypto, "randomUUID").mockReturnValue("00000000-0000-4000-8000-000000000001"); + + const added = addSavedSearch({ + name: "TypeScript", + tech: "TypeScript", + label: "good-first-issue", + sort: "created", + linkedPr: "no", + hacktoberfest: "only", + }); + + expect(added.id).toBe("00000000-0000-4000-8000-000000000001"); + expect(getSavedSearches()).toEqual([added]); + deleteSavedSearch(added.id); + expect(getSavedSearches()).toEqual([]); + }); + + it("reports storage write failures", () => { + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("quota exceeded"); + }); + + expect(() => + addSavedSearch({ + name: "Broken", + tech: "Go", + label: "bug", + sort: "comments", + linkedPr: "yes", + hacktoberfest: "any", + }), + ).toThrow("Unable to save search."); + }); +}); diff --git a/tests/features/issues/server/github-search.test.ts b/tests/features/issues/server/github-search.test.ts index 3273443..eebb7ac 100644 --- a/tests/features/issues/server/github-search.test.ts +++ b/tests/features/issues/server/github-search.test.ts @@ -409,4 +409,133 @@ describe("searchGitHubIssues", () => { helpStatus: "claimed", }); }); + + it("supports custom technology terms and surfaces GitHub API errors", async () => { + const fetchMock = vi.fn(); + searchPageResponses([]).forEach((response) => fetchMock.mockResolvedValueOnce(response)); + vi.stubGlobal("fetch", fetchMock); + + await searchGitHubIssues({ + tech: "Web Components", + label: null, + sort: null, + linkedPr: null, + }); + expect(new URL(fetchMock.mock.calls[0][0]).searchParams.get("q")).toBe( + "topic:web-components archived:false", + ); + + fetchMock + .mockReset() + .mockImplementation(() => + Promise.resolve(new Response("forbidden", { status: 403 })), + ); + await expect( + searchGitHubIssues({ + tech: "Java", + label: null, + sort: null, + linkedPr: null, + }), + ).rejects.toThrow("GitHub API error 403: forbidden"); + }); + + it("returns an empty result when a topic has no matching repositories", async () => { + const fetchMock = vi.fn().mockResolvedValueOnce( + jsonResponse({ total_count: 0, items: [] }), + ); + vi.stubGlobal("fetch", fetchMock); + + const result = await searchGitHubIssues({ + tech: "Kubernetes", + label: "help-wanted", + sort: "updated", + linkedPr: "any", + page: 2, + }); + + expect(result).toMatchObject({ candidateCount: 0, issues: [], page: 2 }); + }); + + it("tolerates enrichment failures and treats assigned issues as claimed", async () => { + process.env.GITHUB_TOKEN = "test-token"; + const assignedIssue = githubIssue({ + comments: 1, + assignee: { login: "maintainer" }, + assignees: [{ login: "maintainer" }], + repository_url: "https://example.test/repos/acme/widgets", + }); + const failure = new Response("unavailable", { status: 503 }); + const fetchMock = vi.fn(); + searchPageResponses([assignedIssue]).forEach((response) => + fetchMock.mockResolvedValueOnce(response), + ); + fetchMock + .mockResolvedValueOnce(failure) + .mockResolvedValueOnce(new Response("unavailable", { status: 503 })) + .mockResolvedValueOnce(new Response("unavailable", { status: 503 })); + vi.stubGlobal("fetch", fetchMock); + + const result = await searchGitHubIssues({ + tech: "Java", + label: "help-wanted", + sort: "updated", + linkedPr: "any", + }); + + expect(result.issues[0]).toMatchObject({ + assigned: true, + stars: null, + linkedPrCount: null, + helpStatus: "claimed", + }); + }); + + it("distinguishes resolved and still-open discussion threads", async () => { + process.env.GITHUB_TOKEN = "test-token"; + const resolvedIssue = githubIssue({ + html_url: "https://github.com/acme/widgets/issues/1", + number: 1, + comments: 1, + }); + const openIssue = githubIssue({ + html_url: "https://github.com/acme/widgets/issues/2", + number: 2, + comments: 1, + }); + const fetchMock = vi + .fn() + .mockResolvedValueOnce(jsonResponse({ total_count: 2, items: [resolvedIssue, openIssue] })) + .mockResolvedValueOnce(jsonResponse({ total_count: 2, items: [] })) + .mockResolvedValueOnce(jsonResponse({ total_count: 2, items: [] })) + .mockResolvedValueOnce(jsonResponse({ total_count: 2, items: [] })) + .mockResolvedValueOnce(jsonResponse({ total_count: 2, items: [] })) + .mockResolvedValueOnce( + jsonResponse({ + full_name: "acme/widgets", + html_url: "https://github.com/acme/widgets", + stargazers_count: 500, + archived: false, + }), + ) + .mockResolvedValueOnce(jsonResponse([{ body: "This was fixed in #99" }])) + .mockResolvedValueOnce(jsonResponse([{ body: "Thanks for reporting this" }])) + .mockResolvedValueOnce(jsonResponse([])) + .mockResolvedValueOnce(jsonResponse([])); + vi.stubGlobal("fetch", fetchMock); + + const result = await searchGitHubIssues({ + tech: "Java", + label: "help-wanted", + sort: "updated", + linkedPr: "any", + }); + + expect(result.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: resolvedIssue.html_url, helpStatus: "resolved" }), + expect.objectContaining({ id: openIssue.html_url, helpStatus: "open" }), + ]), + ); + }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 019e58b..7a8c552 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,25 @@ export default defineConfig({ test: { environment: "node", globals: true, - include: ["tests/**/*.test.ts"], + include: ["tests/**/*.test.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "lcov"], + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "src/components/ui/**", + "src/app/layout.tsx", + "src/app/page.tsx", + "src/components/theme-provider.tsx", + "src/features/issues/types/**", + "src/features/issues/data/**", + ], + thresholds: { + statements: 95, + branches: 92, + functions: 95, + lines: 95, + }, + }, }, });