diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a214752..08fceb2 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -25,8 +25,21 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install Python dependencies run: uv sync --dev - - name: Run tests with coverage + - name: Install Node.js dependencies + run: cd frontend && npm ci + + - name: Run backend tests with coverage run: uv run pytest --cov=backend --cov-report=term + + - name: Run frontend tests + run: cd frontend && npm test diff --git a/README.md b/README.md index c3483f7..23ce833 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,28 @@ The container is now running in the background. Open your web browser and naviga ### 4. Inspect your agents - Try inputting a sample agent URL such as `https://sample-a2a-agent-908687846511.us-central1.run.app` + +## Testing + +### Run all tests + +```sh +# Make the script executable (first time only) +chmod +x scripts/test.sh + +# Run all tests +bash scripts/test.sh +``` + +### Run tests separately + +**Backend tests:** +```sh +uv run pytest backend/tests/ +``` + +**Frontend tests:** +```sh +cd frontend +npm test +``` diff --git a/tests/__init__.py b/backend/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to backend/tests/__init__.py diff --git a/tests/test_validators.py b/backend/tests/test_validators.py similarity index 100% rename from tests/test_validators.py rename to backend/tests/test_validators.py diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index f95bb33..d57b6bd 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,3 +1,11 @@ { - "extends": "./node_modules/gts/" + "extends": "./node_modules/gts/", + "overrides": [ + { + "files": ["tests/**/*.ts", "vitest.config.ts"], + "rules": { + "n/no-unpublished-import": "off" + } + } + ] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 09b9db3..8b9b175 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,12 +14,78 @@ "socket.io-client": "^4.8.1" }, "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.3.1", "esbuild": "^0.25.9", "gts": "^6.0.2", - "typescript": "^5.9.2" + "jsdom": "^27.2.0", + "typescript": "^5.9.2", + "vitest": "^4.0.10" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -45,6 +111,151 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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-syntax-patches-for-csstree": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.16.tgz", + "integrity": "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==", + "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-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/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -588,6 +799,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -639,99 +857,480 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/node": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", - "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "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/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/@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", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", @@ -893,6 +1492,117 @@ "dev": true, "license": "ISC" }, + "node_modules/@vitest/expect": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.10.tgz", + "integrity": "sha512-3QkTX/lK39FBNwARCQRSQr0TP9+ywSdxSX+LgbJ2M1WmveXP72anTbnp2yl5fH+dU6SUmBzNMrDHs80G8G2DZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.10", + "@vitest/utils": "4.0.10", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.10.tgz", + "integrity": "sha512-e2OfdexYkjkg8Hh3L9NVEfbwGXq5IZbDovkf30qW2tOh7Rh9sVtmSr2ztEXOFbymNxS4qjzLXUQIvATvN4B+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.10.tgz", + "integrity": "sha512-99EQbpa/zuDnvVjthwz5bH9o8iPefoQZ63WV8+bsRJZNw3qQSvSltfut8yu1Jc9mqOYi7pEbsKxYTi/rjaq6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.10.tgz", + "integrity": "sha512-EXU2iSkKvNwtlL8L8doCpkyclw0mc/t4t9SeOnfOFPyqLmQwuceMPA4zJBa6jw0MKsZYbw7kAn+gl7HxrlB8UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.10.tgz", + "integrity": "sha512-2N4X2ZZl7kZw0qeGdQ41H0KND96L3qX1RgwuCfy6oUsF2ISGD/HpSbmms+CkIOsQmg2kulwfhJ4CI0asnZlvkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.10.tgz", + "integrity": "sha512-AsY6sVS8OLb96GV5RoG8B6I35GAbNrC49AO+jNRF9YVGb/g9t+hzNm1H6kD0NDp8tt7VJLs6hb7YMkDXqu03iw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.10.tgz", + "integrity": "sha512-kOuqWnEwZNtQxMKg3WmPK1vmhZu9WcoX69iwWjVz+jvKTsF1emzsv3eoPcDr6ykA3qP2bsCQE7CwqfNtAVzsmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.10", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -916,6 +1626,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.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -995,6 +1715,16 @@ "dev": true, "license": "Python-2.0" }, + "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/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1015,6 +1745,16 @@ "node": ">=0.10.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1022,6 +1762,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1094,6 +1844,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1183,10 +1943,53 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1238,6 +2041,13 @@ "node": ">=0.10.0" } }, + "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/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1245,6 +2055,16 @@ "dev": true, "license": "MIT" }, + "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/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1271,6 +2091,13 @@ "node": ">=6.0.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/dompurify": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", @@ -1326,6 +2153,19 @@ "node": ">=10.0.0" } }, + "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/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1336,6 +2176,13 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -1721,6 +2568,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1755,6 +2612,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -1936,6 +2803,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2117,6 +2999,47 @@ "node": ">=10" } }, + "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/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": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2317,6 +3240,13 @@ "node": ">=0.10.0" } }, + "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-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2357,6 +3287,68 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.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", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2482,6 +3474,26 @@ "node": ">=10" } }, + "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", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -2507,6 +3519,13 @@ "node": ">= 20" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -2639,6 +3658,25 @@ "dev": true, "license": "ISC" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2820,6 +3858,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2867,6 +3918,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2887,6 +3945,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2926,6 +4013,34 @@ "node": ">=6.0.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/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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2967,6 +4082,13 @@ "node": ">=8" } }, + "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/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -3134,6 +4256,16 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -3207,6 +4339,48 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -3261,6 +4435,19 @@ "dev": true, "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/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -3297,6 +4484,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3376,6 +4570,16 @@ } } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -3412,6 +4616,20 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3502,6 +4720,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/synckit": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz", @@ -3540,6 +4765,98 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.18", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz", + "integrity": "sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.18" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.18", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.18.tgz", + "integrity": "sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -3566,6 +4883,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -3667,6 +5010,276 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.10.tgz", + "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.10", + "@vitest/mocker": "4.0.10", + "@vitest/pretty-format": "4.0.10", + "@vitest/runner": "4.0.10", + "@vitest/snapshot": "4.0.10", + "@vitest/spy": "4.0.10", + "@vitest/utils": "4.0.10", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.10", + "@vitest/browser-preview": "4.0.10", + "@vitest/browser-webdriverio": "4.0.10", + "@vitest/ui": "4.0.10", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "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": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "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==", + "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": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3683,6 +5296,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3735,6 +5365,23 @@ } } }, + "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/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 984e9a1..bc2acd7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,11 @@ "compile": "tsc", "fix": "gts fix", "prepare": "npm run compile", - "pretest": "npm run compile", - "posttest": "npm run lint" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:lint": "npm test && npm run lint", + "pretest": "npm run compile" }, "keywords": [], "author": "", @@ -22,9 +25,13 @@ "socket.io-client": "^4.8.1" }, "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.3.1", "esbuild": "^0.25.9", "gts": "^6.0.2", - "typescript": "^5.9.2" + "jsdom": "^27.2.0", + "typescript": "^5.9.2", + "vitest": "^4.0.10" } } diff --git a/frontend/public/index.html b/frontend/public/index.html index ea2a632..9120b2f 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -25,13 +25,31 @@

A2A Inspector

- HTTP Headers + Authentication & Headers
-
- +
+ + + +
+ +
+
+ +
+
+ +
+ +
+
-
diff --git a/frontend/public/styles.css b/frontend/public/styles.css index 0a1cb0a..8f0f076 100644 --- a/frontend/public/styles.css +++ b/frontend/public/styles.css @@ -163,9 +163,16 @@ body.dark-mode .chat-info { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -h1, +h1 { + color: #1c1e21; + font-size: 2em; + margin: 0; +} + h2 { color: #1c1e21; + font-size: 1.25em; + margin: 0; } .header-container { @@ -257,6 +264,8 @@ body.dark-mode input:checked + .slider { .collapsible-header { cursor: pointer; user-select: none; + font-size: 1.25em; + margin: 0 0 10px 0; } .toggle-icon { @@ -307,8 +316,10 @@ body.dark-mode input:checked + .slider { .message-metadata-header { cursor: pointer; user-select: none; - padding: 8px 0; + padding: 0; + margin: 0 0 10px 0; font-weight: bold; + font-size: 1.25em; color: #1c1e21; } @@ -323,8 +334,122 @@ body.dark-mode input:checked + .slider { .http-headers-content.expanded, .message-metadata-content.expanded { - max-height: 500px; + max-height: 800px; padding: 10px 0; + overflow-y: auto; +} + +/* Authentication Section Styles */ +.auth-section { + margin-bottom: 15px; + padding: 15px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--bg-tertiary); +} + +body.dark-mode .auth-section { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +.auth-label { + display: block; + font-weight: 500; + margin-bottom: 8px; + color: var(--text-secondary); + font-size: 0.9em; +} + +.auth-type-select { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--input-bg); + color: var(--text-primary); + font-size: 0.85em; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; +} + +.auth-type-select:hover { + background-color: var(--bg-secondary); +} + +.auth-type-select:focus { + outline: none; + border-color: #1877f2; +} + +body.dark-mode .auth-type-select { + background-color: var(--input-bg); + color: var(--text-primary); + border-color: var(--border-color); +} + +body.dark-mode .auth-type-select:hover { + background-color: var(--bg-tertiary); +} + +.auth-inputs { + margin-top: 15px; +} + +.auth-input-group { + margin-bottom: 12px; +} + +.auth-input-group label { + display: block; + font-size: 0.9em; + margin-bottom: 4px; + color: var(--text-secondary); + font-weight: 500; +} + +.auth-input-group input, +.auth-input-group select { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--input-bg); + color: var(--text-primary); + font-size: 0.85em; + box-sizing: border-box; + font-family: monospace; +} + +.auth-input-group input:focus, +.auth-input-group select:focus { + outline: none; + border-color: #1877f2; +} + +.auth-input-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 10px; +} + +.section-divider { + height: 1px; + background-color: var(--border-color); + margin: 20px 0; +} + +.section-label { + display: block; + font-weight: 500; + margin-bottom: 12px; + color: var(--text-primary); + font-size: 0.9em; +} + +.custom-headers-section { + margin-top: 15px; } #headers-list, @@ -933,6 +1058,7 @@ body.dark-mode .loading-spinner { align-items: center; gap: 12px; margin: 0; + font-size: 1.25em; } .session-badge { diff --git a/frontend/src/script.ts b/frontend/src/script.ts index 78e4ec8..e814baa 100644 --- a/frontend/src/script.ts +++ b/frontend/src/script.ts @@ -107,6 +107,12 @@ document.addEventListener('DOMContentLoaded', () => { const httpHeadersContent = document.getElementById( 'http-headers-content', ) as HTMLElement; + const authTypeSelect = document.getElementById( + 'auth-type', + ) as HTMLSelectElement; + const authInputsContainer = document.getElementById( + 'auth-inputs', + ) as HTMLElement; const headersList = document.getElementById('headers-list') as HTMLElement; const addHeaderBtn = document.getElementById( 'add-header-btn', @@ -235,6 +241,104 @@ document.addEventListener('DOMContentLoaded', () => { setupToggle(httpHeadersToggle, httpHeadersContent); setupToggle(messageMetadataToggle, messageMetadataContent); + const createAuthInput = ( + id: string, + label: string, + type: string, + placeholder: string, + defaultValue = '', + ): HTMLElement => { + const group = document.createElement('div'); + group.className = 'auth-input-group'; + + const labelEl = document.createElement('label'); + labelEl.htmlFor = id; + labelEl.textContent = label; + + const inputEl = document.createElement('input'); + inputEl.type = type; + inputEl.id = id; + inputEl.placeholder = placeholder; + inputEl.value = defaultValue; + + group.appendChild(labelEl); + group.appendChild(inputEl); + return group; + }; + + // Auth type change handler + const renderAuthInputs = (authType: string) => { + authInputsContainer.replaceChildren(); + + switch (authType) { + case 'bearer': + authInputsContainer.appendChild( + createAuthInput( + 'bearer-token', + 'Token', + 'password', + 'Enter your bearer token', + ), + ); + break; + + case 'api-key': { + const grid = document.createElement('div'); + grid.className = 'auth-input-grid'; + grid.appendChild( + createAuthInput( + 'api-key-header', + 'Header Name', + 'text', + 'e.g., X-API-Key', + 'X-API-Key', + ), + ); + grid.appendChild( + createAuthInput( + 'api-key-value', + 'API Key', + 'password', + 'Enter your API key', + ), + ); + authInputsContainer.appendChild(grid); + break; + } + + case 'basic': + authInputsContainer.appendChild( + createAuthInput( + 'basic-username', + 'Username', + 'text', + 'Enter username', + ), + ); + authInputsContainer.appendChild( + createAuthInput( + 'basic-password', + 'Password', + 'password', + 'Enter password', + ), + ); + break; + + case 'none': + default: + // No auth inputs needed + break; + } + }; + + authTypeSelect.addEventListener('change', () => { + renderAuthInputs(authTypeSelect.value); + }); + + // Initialize with default auth type + renderAuthInputs(authTypeSelect.value); + const sessionDetailsToggle = document.getElementById( 'session-details-toggle', ) as HTMLElement; @@ -472,13 +576,59 @@ document.addEventListener('DOMContentLoaded', () => { ); } + const getInputValue = (id: string): string => { + const input = document.getElementById(id) as HTMLInputElement; + return input?.value.trim() || ''; + }; + function getCustomHeaders(): Record { - return getKeyValuePairs( + const headers: Record = {}; + const authType = authTypeSelect.value; + + // Add auth headers based on selected type + switch (authType) { + case 'bearer': { + const token = getInputValue('bearer-token'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + break; + } + + case 'api-key': { + const headerName = getInputValue('api-key-header'); + const value = getInputValue('api-key-value'); + if (headerName && value) { + headers[headerName] = value; + } + break; + } + + case 'basic': { + const username = getInputValue('basic-username'); + const password = getInputValue('basic-password'); + if (username && password) { + const credentials = btoa(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + break; + } + + case 'none': + default: + break; + } + + // Always add custom headers from the header list + const customHeaders = getKeyValuePairs( headersList, '.header-item', '.header-name', '.header-value', ); + Object.assign(headers, customHeaders); + + return headers; } function getMessageMetadata(): Record { @@ -885,8 +1035,8 @@ document.addEventListener('DOMContentLoaded', () => { // Collect all artifact content const allContent: string[] = []; - event.artifacts.forEach((artifact) => { - artifact.parts?.forEach((p) => { + event.artifacts.forEach(artifact => { + artifact.parts?.forEach(p => { const content = processPart(p); if (content) allContent.push(content); }); diff --git a/frontend/tests/auth.test.ts b/frontend/tests/auth.test.ts new file mode 100644 index 0000000..eac0b66 --- /dev/null +++ b/frontend/tests/auth.test.ts @@ -0,0 +1,645 @@ +/** + * Tests for authentication UI functionality + */ + +import {describe, it, expect, beforeEach} from 'vitest'; +import {fireEvent} from '@testing-library/dom'; + +describe('Authentication UI', () => { + let authTypeSelect: HTMLSelectElement; + let authInputsContainer: HTMLElement; + + beforeEach(() => { + // Set up the DOM structure that matches the actual HTML + document.body.innerHTML = ` +
+ +
+
+
+ `; + + authTypeSelect = document.getElementById('auth-type') as HTMLSelectElement; + authInputsContainer = document.getElementById('auth-inputs') as HTMLElement; + }); + + describe('Auth Type Selection', () => { + it('should have "none" selected by default', () => { + expect(authTypeSelect.value).toBe('none'); + }); + + it('should contain all four auth type options', () => { + const options = Array.from(authTypeSelect.options).map(opt => opt.value); + expect(options).toEqual(['none', 'basic', 'bearer', 'api-key']); + }); + + it('should change value when a different option is selected', () => { + fireEvent.change(authTypeSelect, {target: {value: 'bearer'}}); + expect(authTypeSelect.value).toBe('bearer'); + }); + }); + + describe('Auth Input Rendering - No Auth', () => { + it('should not render any input fields for "none" auth type', () => { + authTypeSelect.value = 'none'; + renderAuthInputs(authTypeSelect.value); + + expect(authInputsContainer.children.length).toBe(0); + }); + }); + + describe('Auth Input Rendering - Bearer Token', () => { + beforeEach(() => { + authTypeSelect.value = 'bearer'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should render a token input field for bearer auth', () => { + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + expect(tokenInput).toBeTruthy(); + expect(tokenInput.type).toBe('password'); + }); + + it('should have correct label for bearer token input', () => { + const label = authInputsContainer.querySelector('label'); + expect(label?.textContent).toBe('Token'); + }); + + it('should have correct placeholder for bearer token input', () => { + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + expect(tokenInput.placeholder).toBe('Enter your bearer token'); + }); + }); + + describe('Auth Input Rendering - API Key', () => { + beforeEach(() => { + authTypeSelect.value = 'api-key'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should render header name and API key input fields', () => { + const headerInput = document.getElementById( + 'api-key-header', + ) as HTMLInputElement; + const keyInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + + expect(headerInput).toBeTruthy(); + expect(keyInput).toBeTruthy(); + }); + + it('should default header name to "X-API-Key"', () => { + const headerInput = document.getElementById( + 'api-key-header', + ) as HTMLInputElement; + expect(headerInput.value).toBe('X-API-Key'); + }); + + it('should use password type for API key input', () => { + const keyInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + expect(keyInput.type).toBe('password'); + }); + + it('should use grid layout for API key inputs', () => { + const grid = authInputsContainer.querySelector('.auth-input-grid'); + expect(grid).toBeTruthy(); + }); + }); + + describe('Auth Input Rendering - Basic Auth', () => { + beforeEach(() => { + authTypeSelect.value = 'basic'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should render username and password input fields', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + + expect(usernameInput).toBeTruthy(); + expect(passwordInput).toBeTruthy(); + }); + + it('should use text type for username input', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + expect(usernameInput.type).toBe('text'); + }); + + it('should use password type for password input', () => { + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + expect(passwordInput.type).toBe('password'); + }); + + it('should have correct placeholders for basic auth inputs', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + + expect(usernameInput.placeholder).toBe('Enter username'); + expect(passwordInput.placeholder).toBe('Enter password'); + }); + }); + + describe('Auth Input Re-rendering', () => { + it('should clear inputs when switching between auth types', () => { + // Start with bearer + authTypeSelect.value = 'bearer'; + renderAuthInputs(authTypeSelect.value); + expect(authInputsContainer.children.length).toBeGreaterThan(0); + + // Switch to none + authTypeSelect.value = 'none'; + renderAuthInputs(authTypeSelect.value); + expect(authInputsContainer.children.length).toBe(0); + + // Switch to basic + authTypeSelect.value = 'basic'; + renderAuthInputs(authTypeSelect.value); + expect(authInputsContainer.children.length).toBe(2); // username + password groups + }); + + it('should replace inputs completely when changing types', () => { + authTypeSelect.value = 'bearer'; + renderAuthInputs(authTypeSelect.value); + + const bearerInput = document.getElementById('bearer-token'); + expect(bearerInput).toBeTruthy(); + + authTypeSelect.value = 'basic'; + renderAuthInputs(authTypeSelect.value); + + const bearerInputAfter = document.getElementById('bearer-token'); + expect(bearerInputAfter).toBeNull(); + + const usernameInput = document.getElementById('basic-username'); + expect(usernameInput).toBeTruthy(); + }); + }); +}); + +describe('Custom Header Generation', () => { + let authTypeSelect: HTMLSelectElement; + + beforeEach(() => { + document.body.innerHTML = ` +
+ +
+
+
+ `; + authTypeSelect = document.getElementById('auth-type') as HTMLSelectElement; + }); + + describe('No Auth Headers', () => { + it('should return empty headers object when no auth is selected', () => { + authTypeSelect.value = 'none'; + const headers = getCustomHeaders(); + expect(headers).toEqual({}); + }); + }); + + describe('Bearer Token Headers', () => { + beforeEach(() => { + authTypeSelect.value = 'bearer'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should generate Authorization header with Bearer prefix', () => { + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + tokenInput.value = 'test-token-123'; + + const headers = getCustomHeaders(); + expect(headers['Authorization']).toBe('Bearer test-token-123'); + }); + + it('should not generate Authorization header if token is empty', () => { + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + tokenInput.value = ''; + + const headers = getCustomHeaders(); + expect(headers['Authorization']).toBeUndefined(); + }); + + it('should trim whitespace from bearer token', () => { + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + tokenInput.value = ' token-with-spaces '; + + const headers = getCustomHeaders(); + expect(headers['Authorization']).toBe('Bearer token-with-spaces'); + }); + }); + + describe('API Key Headers', () => { + beforeEach(() => { + authTypeSelect.value = 'api-key'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should generate custom header with specified name', () => { + const headerInput = document.getElementById( + 'api-key-header', + ) as HTMLInputElement; + const valueInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + + headerInput.value = 'X-Custom-Key'; + valueInput.value = 'secret-key-456'; + + const headers = getCustomHeaders(); + expect(headers['X-Custom-Key']).toBe('secret-key-456'); + }); + + it('should use default X-API-Key header name', () => { + const valueInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + valueInput.value = 'my-api-key'; + + const headers = getCustomHeaders(); + expect(headers['X-API-Key']).toBe('my-api-key'); + }); + + it('should not generate header if key value is empty', () => { + const headerInput = document.getElementById( + 'api-key-header', + ) as HTMLInputElement; + const valueInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + + headerInput.value = 'X-API-Key'; + valueInput.value = ''; + + const headers = getCustomHeaders(); + expect(headers['X-API-Key']).toBeUndefined(); + }); + + it('should not generate header if header name is empty', () => { + const headerInput = document.getElementById( + 'api-key-header', + ) as HTMLInputElement; + const valueInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + + headerInput.value = ''; + valueInput.value = 'my-key'; + + const headers = getCustomHeaders(); + expect(Object.keys(headers).length).toBe(0); + }); + }); + + describe('Basic Auth Headers', () => { + beforeEach(() => { + authTypeSelect.value = 'basic'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should generate Authorization header with Basic prefix and base64 encoding', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + + usernameInput.value = 'user123'; + passwordInput.value = 'pass456'; + + const headers = getCustomHeaders(); + const expectedCredentials = btoa('user123:pass456'); + expect(headers['Authorization']).toBe(`Basic ${expectedCredentials}`); + }); + + it('should handle special characters in username and password', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + + usernameInput.value = 'user@example.com'; + passwordInput.value = 'p@ss:w0rd!'; + + const headers = getCustomHeaders(); + const expectedCredentials = btoa('user@example.com:p@ss:w0rd!'); + expect(headers['Authorization']).toBe(`Basic ${expectedCredentials}`); + }); + + it('should not generate Authorization header if username is empty', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + + usernameInput.value = ''; + passwordInput.value = 'password'; + + const headers = getCustomHeaders(); + expect(headers['Authorization']).toBeUndefined(); + }); + + it('should not generate Authorization header if password is empty', () => { + const usernameInput = document.getElementById( + 'basic-username', + ) as HTMLInputElement; + const passwordInput = document.getElementById( + 'basic-password', + ) as HTMLInputElement; + + usernameInput.value = 'username'; + passwordInput.value = ''; + + const headers = getCustomHeaders(); + expect(headers['Authorization']).toBeUndefined(); + }); + }); + + describe('Custom Headers Integration', () => { + beforeEach(() => { + authTypeSelect.value = 'bearer'; + renderAuthInputs(authTypeSelect.value); + }); + + it('should merge auth headers with custom headers', () => { + // Set up bearer token + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + tokenInput.value = 'bearer-token'; + + // Add custom header + const headersList = document.getElementById('headers-list')!; + headersList.innerHTML = ` +
+ + +
+ `; + + const headers = getCustomHeaders(); + expect(headers['Authorization']).toBe('Bearer bearer-token'); + expect(headers['X-Custom-Header']).toBe('custom-value'); + }); + + it('should allow custom headers to override auth headers if specified', () => { + // Set up bearer token + const tokenInput = document.getElementById( + 'bearer-token', + ) as HTMLInputElement; + tokenInput.value = 'bearer-token'; + + // Add custom Authorization header (this should override) + const headersList = document.getElementById('headers-list')!; + headersList.innerHTML = ` +
+ + +
+ `; + + const headers = getCustomHeaders(); + // Custom headers are added after auth headers using Object.assign, + // so custom headers should override + expect(headers['Authorization']).toBe('Custom Auth Value'); + }); + + it('should handle multiple custom headers with auth headers', () => { + authTypeSelect.value = 'api-key'; + renderAuthInputs(authTypeSelect.value); + + const keyInput = document.getElementById( + 'api-key-value', + ) as HTMLInputElement; + keyInput.value = 'my-api-key'; + + const headersList = document.getElementById('headers-list')!; + headersList.innerHTML = ` +
+ + +
+
+ + +
+ `; + + const headers = getCustomHeaders(); + expect(headers['X-API-Key']).toBe('my-api-key'); + expect(headers['X-Request-ID']).toBe('req-123'); + expect(headers['X-Client-Version']).toBe('1.0.0'); + }); + + it('should skip empty custom headers', () => { + authTypeSelect.value = 'none'; + + const headersList = document.getElementById('headers-list')!; + headersList.innerHTML = ` +
+ + +
+
+ + +
+ `; + + const headers = getCustomHeaders(); + expect(headers['Valid-Header']).toBe('valid-value'); + expect(Object.keys(headers).length).toBe(1); + }); + }); +}); + +// Helper functions that mirror the actual implementation +function createAuthInput( + id: string, + label: string, + type: string, + placeholder: string, + defaultValue = '', +): HTMLElement { + const group = document.createElement('div'); + group.className = 'auth-input-group'; + + const labelEl = document.createElement('label'); + labelEl.htmlFor = id; + labelEl.textContent = label; + + const inputEl = document.createElement('input'); + inputEl.type = type; + inputEl.id = id; + inputEl.placeholder = placeholder; + inputEl.value = defaultValue; + + group.appendChild(labelEl); + group.appendChild(inputEl); + return group; +} + +function renderAuthInputs(authType: string) { + const authInputsContainer = document.getElementById('auth-inputs')!; + authInputsContainer.replaceChildren(); + + switch (authType) { + case 'bearer': + authInputsContainer.appendChild( + createAuthInput( + 'bearer-token', + 'Token', + 'password', + 'Enter your bearer token', + ), + ); + break; + + case 'api-key': { + const grid = document.createElement('div'); + grid.className = 'auth-input-grid'; + grid.appendChild( + createAuthInput( + 'api-key-header', + 'Header Name', + 'text', + 'e.g., X-API-Key', + 'X-API-Key', + ), + ); + grid.appendChild( + createAuthInput( + 'api-key-value', + 'API Key', + 'password', + 'Enter your API key', + ), + ); + authInputsContainer.appendChild(grid); + break; + } + + case 'basic': + authInputsContainer.appendChild( + createAuthInput('basic-username', 'Username', 'text', 'Enter username'), + ); + authInputsContainer.appendChild( + createAuthInput( + 'basic-password', + 'Password', + 'password', + 'Enter password', + ), + ); + break; + + case 'none': + default: + break; + } +} + +function getInputValue(id: string): string { + const input = document.getElementById(id) as HTMLInputElement; + return input?.value.trim() || ''; +} + +function getCustomHeaders(): Record { + const headers: Record = {}; + const authTypeSelect = document.getElementById( + 'auth-type', + ) as HTMLSelectElement; + const authType = authTypeSelect.value; + + // Add auth headers based on selected type + switch (authType) { + case 'bearer': { + const token = getInputValue('bearer-token'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + break; + } + + case 'api-key': { + const headerName = getInputValue('api-key-header'); + const value = getInputValue('api-key-value'); + if (headerName && value) { + headers[headerName] = value; + } + break; + } + + case 'basic': { + const username = getInputValue('basic-username'); + const password = getInputValue('basic-password'); + if (username && password) { + const credentials = btoa(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + break; + } + + case 'none': + default: + break; + } + + // Always add custom headers from the header list + const headersList = document.getElementById('headers-list')!; + const headerItems = headersList.querySelectorAll('.header-item'); + + headerItems.forEach(item => { + const nameInput = item.querySelector('.header-name') as HTMLInputElement; + const valueInput = item.querySelector('.header-value') as HTMLInputElement; + + const name = nameInput?.value.trim(); + const value = valueInput?.value.trim(); + + if (name && value) { + headers[name] = value; + } + }); + + return headers; +} diff --git a/frontend/tests/dark-mode.test.ts b/frontend/tests/dark-mode.test.ts new file mode 100644 index 0000000..0964a9c --- /dev/null +++ b/frontend/tests/dark-mode.test.ts @@ -0,0 +1,120 @@ +/** + * Tests for dark mode toggle functionality + */ + +import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest'; +import {fireEvent} from '@testing-library/dom'; + +describe('Dark Mode Toggle', () => { + let themeCheckbox: HTMLInputElement; + let highlightLight: HTMLLinkElement; + let highlightDark: HTMLLinkElement; + + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear(); + + // Set up the DOM structure + document.body.innerHTML = ` +
+ + + +
+ `; + + themeCheckbox = document.getElementById('theme-checkbox') as HTMLInputElement; + highlightLight = document.getElementById('highlight-light') as HTMLLinkElement; + highlightDark = document.getElementById('highlight-dark') as HTMLLinkElement; + + // Initialize the dark mode handler + initializeDarkMode(); + }); + + afterEach(() => { + // Clean up body class + document.body.classList.remove('dark-mode'); + }); + + it('starts with light theme', () => { + expect(document.body.classList.contains('dark-mode')).toBe(false); + expect(themeCheckbox.checked).toBe(false); + }); + + it('toggles to dark mode', () => { + fireEvent.click(themeCheckbox); + + expect(document.body.classList.contains('dark-mode')).toBe(true); + expect(themeCheckbox.checked).toBe(true); + }); + + it('toggles back to light mode', () => { + fireEvent.click(themeCheckbox); // Enable + fireEvent.click(themeCheckbox); // Disable + + expect(document.body.classList.contains('dark-mode')).toBe(false); + }); + + it('switches syntax highlighting theme', () => { + fireEvent.click(themeCheckbox); + + expect(highlightLight.disabled).toBe(true); + expect(highlightDark.disabled).toBe(false); + }); + + it('persists dark theme to localStorage', () => { + fireEvent.click(themeCheckbox); + + expect(localStorage.getItem('theme')).toBe('dark'); + }); + + it('persists light theme to localStorage', () => { + fireEvent.click(themeCheckbox); // Enable + fireEvent.click(themeCheckbox); // Disable + + expect(localStorage.getItem('theme')).toBe('light'); + }); + + it('restores theme from localStorage on load', () => { + localStorage.setItem('theme', 'dark'); + initializeDarkMode(); + + expect(document.body.classList.contains('dark-mode')).toBe(true); + expect(themeCheckbox.checked).toBe(true); + }); +}); + +// Helper function that mirrors the actual implementation +function initializeDarkMode() { + const themeCheckbox = document.getElementById('theme-checkbox') as HTMLInputElement; + const highlightLight = document.getElementById('highlight-light') as HTMLLinkElement; + const highlightDark = document.getElementById('highlight-dark') as HTMLLinkElement; + + const updateSyntaxHighlighting = (isDark: boolean) => { + if (isDark) { + highlightLight.disabled = true; + highlightDark.disabled = false; + } else { + highlightLight.disabled = false; + highlightDark.disabled = true; + } + }; + + // Restore theme from localStorage + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === 'dark') { + document.body.classList.add('dark-mode'); + themeCheckbox.checked = true; + updateSyntaxHighlighting(true); + } + + themeCheckbox.addEventListener('change', () => { + document.body.classList.toggle('dark-mode'); + const isDark = document.body.classList.contains('dark-mode'); + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + updateSyntaxHighlighting(isDark); + }); +} diff --git a/frontend/tests/file-attachments.test.ts b/frontend/tests/file-attachments.test.ts new file mode 100644 index 0000000..96d544c --- /dev/null +++ b/frontend/tests/file-attachments.test.ts @@ -0,0 +1,258 @@ +/** + * Tests for file attachment UI + */ + +import {describe, it, expect, beforeEach} from 'vitest'; +import {fireEvent} from '@testing-library/dom'; + +describe('File Attachments', () => { + let fileInput: HTMLInputElement; + let attachBtn: HTMLButtonElement; + let attachmentsPreview: HTMLElement; + + beforeEach(() => { + document.body.innerHTML = ` +
+ + +
+
+ `; + + fileInput = document.getElementById('file-input') as HTMLInputElement; + attachBtn = document.getElementById('attach-btn') as HTMLButtonElement; + attachmentsPreview = document.getElementById('attachments-preview') as HTMLElement; + }); + + it('starts with file input hidden', () => { + expect(fileInput.classList.contains('file-input-hidden')).toBe(true); + }); + + it('starts with attach button disabled', () => { + expect(attachBtn.disabled).toBe(true); + }); + + it('starts with empty preview', () => { + expect(attachmentsPreview.children.length).toBe(0); + }); + + it('accepts multiple files of any type', () => { + expect(fileInput.multiple).toBe(true); + expect(fileInput.accept).toBe('*/*'); + }); + + it('enables attach button when connected', () => { + attachBtn.disabled = false; + expect(attachBtn.disabled).toBe(false); + }); + + it('triggers file input when attach button clicked', () => { + let fileInputClicked = false; + attachBtn.disabled = false; + + fileInput.addEventListener('click', () => { + fileInputClicked = true; + }); + + attachBtn.addEventListener('click', () => { + fileInput.click(); + }); + + fireEvent.click(attachBtn); + + expect(fileInputClicked).toBe(true); + }); + + it('renders attachment chip with name', () => { + const attachment = { + name: 'report.pdf', + size: 2048, + mimeType: 'application/pdf', + data: 'base64data', + }; + + renderAttachmentPreview(attachment); + + const nameEl = attachmentsPreview.querySelector('.attachment-name'); + expect(nameEl?.textContent).toBe('report.pdf'); + }); + + it('formats file sizes correctly', () => { + const testCases = [ + {size: 500, expected: '500 B'}, + {size: 1536, expected: '1.5 KB'}, + {size: 5242880, expected: '5.0 MB'}, + ]; + + testCases.forEach(({size, expected}) => { + const attachment = { + name: 'file', + size, + mimeType: 'text/plain', + data: 'data', + }; + + attachmentsPreview.innerHTML = ''; // Clear + renderAttachmentPreview(attachment); + + const sizeEl = attachmentsPreview.querySelector('.attachment-size'); + expect(sizeEl?.textContent).toBe(expected); + }); + }); + + it('renders multiple attachments', () => { + const attachments = [ + {name: 'file1.pdf', size: 1024, mimeType: 'application/pdf', data: 'data1'}, + {name: 'file2.png', size: 2048, mimeType: 'image/png', data: 'data2'}, + {name: 'file3.txt', size: 512, mimeType: 'text/plain', data: 'data3'}, + ]; + + attachments.forEach(att => renderAttachmentPreview(att)); + + const chips = attachmentsPreview.querySelectorAll('.attachment-chip'); + expect(chips.length).toBe(3); + }); + + it('renders image thumbnails with correct src', () => { + const attachment = { + name: 'photo.jpg', + size: 2048, + mimeType: 'image/jpeg', + data: 'base64data', + thumbnail: 'data:image/jpeg;base64,thumbnaildata', + }; + + renderAttachmentPreview(attachment); + + const thumbnail = attachmentsPreview.querySelector( + '.attachment-thumbnail', + ) as HTMLImageElement; + expect(thumbnail).toBeTruthy(); + expect(thumbnail.src).toContain('data:image/jpeg;base64,thumbnaildata'); + }); + + it('removes attachment when remove button clicked', () => { + const attachment = { + name: 'file.pdf', + size: 1024, + mimeType: 'application/pdf', + data: 'base64data', + }; + + renderAttachmentPreview(attachment); + + const removeBtn = attachmentsPreview.querySelector( + '.attachment-remove', + ) as HTMLButtonElement; + fireEvent.click(removeBtn); + + const chips = attachmentsPreview.querySelectorAll('.attachment-chip'); + expect(chips.length).toBe(0); + }); + + it('removes specific attachment from list', () => { + const attachments = [ + {name: 'file1.pdf', size: 1024, mimeType: 'application/pdf', data: 'data1'}, + {name: 'file2.png', size: 2048, mimeType: 'image/png', data: 'data2'}, + {name: 'file3.txt', size: 512, mimeType: 'text/plain', data: 'data3'}, + ]; + + attachments.forEach(att => renderAttachmentPreview(att)); + + // Remove the second attachment + const chips = attachmentsPreview.querySelectorAll('.attachment-chip'); + const removeBtn = chips[1].querySelector( + '.attachment-remove', + ) as HTMLButtonElement; + fireEvent.click(removeBtn); + + const remainingChips = attachmentsPreview.querySelectorAll('.attachment-chip'); + expect(remainingChips.length).toBe(2); + }); + + + it('renders thumbnails for images', () => { + const attachment = { + name: 'photo.png', + size: 2048, + mimeType: 'image/png', + data: 'base64data', + thumbnail: 'data:image/png;base64,thumb', + }; + + renderAttachmentPreview(attachment); + + const thumbnail = attachmentsPreview.querySelector('.attachment-thumbnail'); + expect(thumbnail).toBeTruthy(); + }); + + it('renders chips for non-image files', () => { + const attachment = { + name: 'document.pdf', + size: 1024, + mimeType: 'application/pdf', + data: 'base64data', + }; + + renderAttachmentPreview(attachment); + + const chip = attachmentsPreview.querySelector('.attachment-chip'); + const thumbnail = attachmentsPreview.querySelector('.attachment-thumbnail'); + expect(chip).toBeTruthy(); + expect(thumbnail).toBeNull(); + }); +}); + +// Helper functions that mirror the actual implementation +interface Attachment { + name: string; + size: number; + mimeType: string; + data: string; + thumbnail?: string; +} + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function renderAttachmentPreview(attachment: Attachment) { + const attachmentsPreview = document.getElementById('attachments-preview')!; + + const chip = document.createElement('div'); + chip.className = 'attachment-chip'; + + if (attachment.thumbnail) { + const thumbnail = document.createElement('img'); + thumbnail.className = 'attachment-thumbnail'; + thumbnail.src = attachment.thumbnail; + chip.appendChild(thumbnail); + } + + const info = document.createElement('div'); + info.className = 'attachment-info'; + + const name = document.createElement('div'); + name.className = 'attachment-name'; + name.textContent = attachment.name; + info.appendChild(name); + + const size = document.createElement('div'); + size.className = 'attachment-size'; + size.textContent = formatFileSize(attachment.size); + info.appendChild(size); + + chip.appendChild(info); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'attachment-remove'; + removeBtn.textContent = '×'; + removeBtn.addEventListener('click', () => { + chip.remove(); + }); + chip.appendChild(removeBtn); + + attachmentsPreview.appendChild(chip); +} diff --git a/frontend/tests/session-management.test.ts b/frontend/tests/session-management.test.ts new file mode 100644 index 0000000..30d8895 --- /dev/null +++ b/frontend/tests/session-management.test.ts @@ -0,0 +1,205 @@ +/** + * Tests for session management UI + */ + +import {describe, it, expect, beforeEach} from 'vitest'; +import {fireEvent} from '@testing-library/dom'; + +describe('Session Management', () => { + beforeEach(() => { + document.body.innerHTML = ` +
+
+

Chat

+ +
+
+
+ Session Details +
+
+
+ Transport: + Not connected +
+
+ Input Modalities: +
+ Not connected +
+
+
+ Output Modalities: +
+ Not connected +
+
+
+ Context ID: + No active session +
+
+
+
+ `; + }); + + it('shows "Not connected" by default', () => { + const transportEl = document.getElementById('session-transport'); + expect(transportEl?.textContent).toBe('Not connected'); + }); + + it('shows "No active session" for context ID', () => { + const contextIdEl = document.getElementById('session-details'); + expect(contextIdEl?.textContent).toBe('No active session'); + }); + + it('disables new session button by default', () => { + const newSessionBtn = document.getElementById( + 'new-session-btn', + ) as HTMLButtonElement; + expect(newSessionBtn.disabled).toBe(true); + }); + + describe('Session Details Toggle', () => { + beforeEach(() => { + const toggle = document.getElementById('session-details-toggle')!; + const content = document.getElementById('session-details-content')!; + setupToggle(toggle, content); + }); + + it('expands when clicked', () => { + const toggle = document.getElementById('session-details-toggle')!; + const content = document.getElementById('session-details-content')!; + + fireEvent.click(toggle); + + expect(content.classList.contains('expanded')).toBe(true); + }); + + it('collapses when clicked again', () => { + const toggle = document.getElementById('session-details-toggle')!; + const content = document.getElementById('session-details-content')!; + + fireEvent.click(toggle); // Expand + fireEvent.click(toggle); // Collapse + + expect(content.classList.contains('expanded')).toBe(false); + }); + }); + + function setupToggle(toggleElement: HTMLElement, contentElement: HTMLElement) { + const icon = toggleElement.querySelector('.toggle-icon') as HTMLElement; + + toggleElement.addEventListener('click', () => { + const isExpanded = contentElement.classList.contains('expanded'); + contentElement.classList.toggle('expanded'); + + if (icon) { + icon.style.transform = isExpanded ? 'rotate(0deg)' : 'rotate(90deg)'; + } + }); + } + + it('displays transport type', () => { + updateTransport('jsonrpc'); + + const transportEl = document.getElementById('session-transport'); + expect(transportEl?.textContent).toBe('jsonrpc'); + }); + + it('displays input modalities', () => { + updateInputModalities(['text/plain', 'image/png', 'audio/mpeg']); + + const inputModesEl = document.getElementById('session-input-modes')!; + const tags = inputModesEl.querySelectorAll('.modality-tag'); + + expect(tags.length).toBe(3); + expect(tags[0].textContent).toBe('text/plain'); + }); + + it('displays output modalities', () => { + updateOutputModalities(['text/plain', 'image/jpeg']); + + const outputModesEl = document.getElementById('session-output-modes')!; + const tags = outputModesEl.querySelectorAll('.modality-tag'); + + expect(tags.length).toBe(2); + }); + + it('replaces modalities when updated', () => { + updateInputModalities(['text/plain', 'image/png']); + updateInputModalities(['audio/mpeg']); + + const inputModesEl = document.getElementById('session-input-modes')!; + const tags = inputModesEl.querySelectorAll('.modality-tag'); + + expect(tags.length).toBe(1); + expect(tags[0].textContent).toBe('audio/mpeg'); + }); + + it('displays and updates context ID', () => { + const contextId = 'ctx_12345abcde'; + updateContextId(contextId); + + const contextIdEl = document.getElementById('session-details'); + expect(contextIdEl?.textContent).toBe(contextId); + }); + + it('enables new session button when connected', () => { + const newSessionBtn = document.getElementById( + 'new-session-btn', + ) as HTMLButtonElement; + let clicked = false; + + newSessionBtn.disabled = false; + newSessionBtn.addEventListener('click', () => { + clicked = true; + }); + + fireEvent.click(newSessionBtn); + + expect(clicked).toBe(true); + }); +}); + +// Helper functions that mirror the actual implementation +function updateTransport(transport: string) { + const transportEl = document.getElementById('session-transport'); + if (transportEl) { + transportEl.textContent = transport; + } +} + +function updateInputModalities(modalities: string[]) { + const inputModesEl = document.getElementById('session-input-modes'); + if (inputModesEl) { + inputModesEl.innerHTML = ''; + modalities.forEach(modality => { + const tag = document.createElement('span'); + tag.className = 'modality-tag'; + tag.textContent = modality; + inputModesEl.appendChild(tag); + }); + } +} + +function updateOutputModalities(modalities: string[]) { + const outputModesEl = document.getElementById('session-output-modes'); + if (outputModesEl) { + outputModesEl.innerHTML = ''; + modalities.forEach(modality => { + const tag = document.createElement('span'); + tag.className = 'modality-tag'; + tag.textContent = modality; + outputModesEl.appendChild(tag); + }); + } +} + +function updateContextId(contextId: string) { + const contextIdEl = document.getElementById('session-details'); + if (contextIdEl) { + contextIdEl.textContent = contextId; + } +} diff --git a/frontend/tests/setup.ts b/frontend/tests/setup.ts new file mode 100644 index 0000000..be8809c --- /dev/null +++ b/frontend/tests/setup.ts @@ -0,0 +1,9 @@ +/** + * Test setup file for Vitest + * This file runs before all tests to set up the testing environment + */ + +// Mock btoa (base64 encoding) if not available in jsdom +if (typeof global.btoa === 'undefined') { + global.btoa = (str: string) => Buffer.from(str, 'binary').toString('base64'); +} diff --git a/frontend/tests/ui-components.test.ts b/frontend/tests/ui-components.test.ts new file mode 100644 index 0000000..a680344 --- /dev/null +++ b/frontend/tests/ui-components.test.ts @@ -0,0 +1,177 @@ +/** + * Tests for UI components and interactions + * Tests collapsible sections, buttons, and general UI functionality + */ + +import {describe, it, expect, beforeEach} from 'vitest'; +import {fireEvent} from '@testing-library/dom'; + +describe('Collapsible Sections', () => { + let toggleElement: HTMLElement; + let contentElement: HTMLElement; + + beforeEach(() => { + document.body.innerHTML = ` +
+
+ + Test Section +
+
+

Content goes here

+
+
+ `; + + toggleElement = document.getElementById('test-toggle')!; + contentElement = document.getElementById('test-content')!; + + setupToggle(toggleElement, contentElement); + }); + + it('starts collapsed', () => { + expect(contentElement.classList.contains('expanded')).toBe(false); + }); + + it('expands when clicked', () => { + fireEvent.click(toggleElement); + expect(contentElement.classList.contains('expanded')).toBe(true); + }); + + it('toggles back to collapsed', () => { + fireEvent.click(toggleElement); // Expand + fireEvent.click(toggleElement); // Collapse + expect(contentElement.classList.contains('expanded')).toBe(false); + }); + +}); + +describe('User Input', () => { + it('accepts agent URL in input', () => { + document.body.innerHTML = ` + + `; + + const input = document.getElementById('agent-card-url') as HTMLInputElement; + input.value = 'https://example.com/agent'; + + expect(input.value).toBe('https://example.com/agent'); + }); + + it('accepts chat messages in input', () => { + document.body.innerHTML = ` + + `; + + const input = document.getElementById('chat-input') as HTMLInputElement; + input.value = 'Hello, agent!'; + + expect(input.value).toBe('Hello, agent!'); + }); + +}); + +describe('Message Rendering', () => { + let chatMessages: HTMLElement; + + beforeEach(() => { + document.body.innerHTML = ` +
+

Messages will appear here.

+
+ `; + + chatMessages = document.getElementById('chat-messages')!; + }); + + it('renders user messages', () => { + addUserMessage('Hello, agent!'); + + const message = chatMessages.querySelector('.message.user'); + expect(message).toBeTruthy(); + expect(message?.textContent).toContain('Hello, agent!'); + }); + + it('renders agent messages with validation status', () => { + addAgentMessage('Response', true); + + const message = chatMessages.querySelector('.message.agent'); + const status = chatMessages.querySelector('.validation-status.valid'); + + expect(message).toBeTruthy(); + expect(status?.textContent).toBe('✅'); + }); + + it('shows warning for non-compliant messages', () => { + addAgentMessage('Response', false); + + const status = chatMessages.querySelector('.validation-status.invalid'); + expect(status?.textContent).toBe('⚠️'); + }); + + it('renders loading state', () => { + addLoadingMessage(); + + const loading = chatMessages.querySelector('.message.agent-loading'); + const spinner = chatMessages.querySelector('.loading-spinner'); + + expect(loading).toBeTruthy(); + expect(spinner).toBeTruthy(); + }); + + function addUserMessage(text: string) { + const message = document.createElement('div'); + message.className = 'message user'; + message.textContent = text; + chatMessages.appendChild(message); + } + + function addAgentMessage(text: string, isValid: boolean) { + const message = document.createElement('div'); + message.className = 'message agent'; + + const textEl = document.createElement('span'); + textEl.textContent = text; + message.appendChild(textEl); + + const status = document.createElement('span'); + status.className = `validation-status ${isValid ? 'valid' : 'invalid'}`; + status.textContent = isValid ? '✅' : '⚠️'; + message.appendChild(status); + + chatMessages.appendChild(message); + } + + function addLoadingMessage() { + const message = document.createElement('div'); + message.className = 'message agent-loading'; + + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner'; + message.appendChild(spinner); + + const text = document.createElement('span'); + text.className = 'loading-text'; + text.textContent = 'Agent is thinking...'; + message.appendChild(text); + + chatMessages.appendChild(message); + } +}); + +// Helper function to set up collapsible toggle +function setupToggle( + toggleElement: HTMLElement, + contentElement: HTMLElement, +) { + const icon = toggleElement.querySelector('.toggle-icon') as HTMLElement; + + toggleElement.addEventListener('click', () => { + const isExpanded = contentElement.classList.contains('expanded'); + contentElement.classList.toggle('expanded'); + + if (icon) { + icon.style.transform = isExpanded ? 'rotate(0deg)' : 'rotate(90deg)'; + } + }); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 7d4c9ae..02a1c01 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -11,5 +11,5 @@ "module": "esnext", "skipLibCheck": true }, - "include": ["src/**/*.ts", "test/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts", "vitest.config.ts"] } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..cb4c7b8 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,16 @@ +import {defineConfig} from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./tests/setup.ts'], + include: ['./tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['tests/**/*.ts'], + }, + }, +}); diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..2c65310 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}╔════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ Running A2A Inspector Test Suite ║${NC}" +echo -e "${BLUE}╚════════════════════════════════════════╝${NC}" +echo "" + +# Track failures +FAILED=0 + +# Run backend tests +echo -e "${BLUE}[1/2] Running Backend Tests (Python)...${NC}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +if uv run pytest backend/tests/ -v; then + echo -e "${GREEN}✓ Backend tests passed${NC}" +else + echo -e "${RED}✗ Backend tests failed${NC}" + FAILED=1 +fi +echo "" + +# Run frontend tests +echo -e "${BLUE}[2/2] Running Frontend Tests (TypeScript)...${NC}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +cd frontend +if npm test; then + echo -e "${GREEN}✓ Frontend tests passed${NC}" +else + echo -e "${RED}✗ Frontend tests failed${NC}" + FAILED=1 +fi +cd .. +echo "" + +# Summary +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +if [ $FAILED -eq 0 ]; then + echo -e "${GREEN}✓ All tests passed!${NC}" + exit 0 +else + echo -e "${RED}✗ Some tests failed${NC}" + exit 1 +fi