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,
+ },
+ },
},
});