diff --git a/.gitignore b/.gitignore index b362db5..abbcc95 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,8 @@ tmp/ *.tmp .coverage .venv/bin/dotenv + +# Node.js +node_modules/ +tests/results/* +test-results/* diff --git a/Docs/tasks.md b/Docs/tasks.md index b69ecc5..35d088e 100644 --- a/Docs/tasks.md +++ b/Docs/tasks.md @@ -1,23 +1,3 @@ -### Task INFRA-2: Fix Suite Teardown — Close Browser Keyword Argument Mismatch - -- **Failing Step**: `Suite Teardown → Close Browser` -- **Failure Message**: `Keyword 'common.Close Browser' expected 0 arguments, got 1` -- **Files to Check**: - - `tests/robot/__init__.robot` - - `tests/robot/resources/common.resource` - - `tests/robot/resources/ui_keywords.resource` -- **Reference Docs**: - - `Docs/TESTING.md` -- **Expected Behavior**: - - The suite teardown should cleanly close the browser and stop the server without errors. -- **Actual Behavior**: - - The `Close Browser` keyword is being called with an argument (likely `${BROWSER}` or similar), but the keyword definition expects 0 arguments. -- **Suggested Fix**: - - Review `__init__.robot` line: `Close Browser` — remove any passed arguments. - - Or, if a custom wrapper keyword is intended, define it in `ui_keywords.resource` and call that instead. - ---- - ## API Tests — `Robot.Api.Anime` (14 tests, 0 passed, 14 failed) > **Parent Suite Setup Failed**: `Server did not become healthy within 30 seconds` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cfa0a71 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2644 @@ +{ + "name": "aniworld-web", + "version": "1.4.17", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aniworld-web", + "version": "1.4.17", + "devDependencies": { + "@playwright/test": "^1.41.0", + "@vitest/coverage-v8": "^1.2.0", + "@vitest/ui": "^1.2.0", + "happy-dom": "^13.10.1", + "vitest": "^1.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", + "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.1" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.1.tgz", + "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "fast-glob": "^3.3.2", + "fflate": "^0.8.1", + "flatted": "^3.2.9", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "sirv": "^2.0.4" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.1" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/happy-dom": { + "version": "13.10.1", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-13.10.1.tgz", + "integrity": "sha512-9GZLEFvQL5EgfJX2zcBgu1nsPUn98JF/EiJnSfQbdxI6YEQGqpd09lXXxOmYonRBIEFz9JlGCOiPflDzgS1p8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "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/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/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/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", + "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/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/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/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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/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/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "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/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/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/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/robot/__init__.robot b/tests/robot/__init__.robot new file mode 100644 index 0000000..9f3e5d2 --- /dev/null +++ b/tests/robot/__init__.robot @@ -0,0 +1,17 @@ +*** Settings *** +Documentation Suite-level setup and teardown for Aniworld Robot Framework tests. +... Starts the FastAPI server, initializes Browser library, and cleans up after all tests. + +Library Process +Library Browser +Library RequestsLibrary +Resource ${CURDIR}/resources/common.resource + +Suite Setup Run Keywords +... Start Aniworld Server +... AND Wait For Server +... AND Initialize Browser + +Suite Teardown Run Keywords +... Close Browser +... AND Stop Aniworld Server diff --git a/tests/robot/api/anime.robot b/tests/robot/api/anime.robot new file mode 100644 index 0000000..4fc752c --- /dev/null +++ b/tests/robot/api/anime.robot @@ -0,0 +1,126 @@ +*** Settings *** +Documentation Anime library API tests for Aniworld. +... Covers status, rescan, search, add, details, episodes, filters, and duplicates. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Library Status +# --------------------------------------------------------------------------- +Get Anime Library Status + [Documentation] Retrieve the anime library status. + ${resp}= Get Anime Status + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} directory series_count + +# --------------------------------------------------------------------------- +# Rescan +# --------------------------------------------------------------------------- +Rescan Library + [Documentation] Trigger a library rescan and verify acceptance. + ${resp}= Rescan Library + Response Should Have Status ${resp} 200 + +Scan Status During Idle + [Documentation] Get scan status when no scan is in progress. + ${resp}= Get Scan Status + Response Should Have Status ${resp} 200 + ${in_progress}= Get JSON Value ${resp} $.in_progress + Should Be Equal As Strings ${in_progress} False + +# --------------------------------------------------------------------------- +# Search +# --------------------------------------------------------------------------- +Search Anime + [Documentation] Search for anime series via the provider. + ${resp}= Search Anime attack + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +# --------------------------------------------------------------------------- +# Series CRUD +# --------------------------------------------------------------------------- +Add New Series + [Documentation] Add a new anime series to the library. + ${resp}= Add Series https://aniworld.to/anime/stream/attack-on-titan Attack on Titan 2013 + Response Should Have Status ${resp} 201 + ${key}= Get JSON Value ${resp} $.key + Should Not Be Empty ${key} + Set Test Variable ${TEST_SERIES_KEY} ${key} + +Get Series Details + [Documentation] Retrieve details for a specific series. + Add New Series + ${resp}= Get Series Details ${TEST_SERIES_KEY} + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} key name folder episodes + +Update Series Settings + [Documentation] Update settings for a specific series. + Add New Series + ${payload}= Create Dictionary preferred_language=german + ${resp}= PUT API /api/anime/${TEST_SERIES_KEY}/settings ${payload} + Response Should Have Status ${resp} 200 + +Get Series Episodes + [Documentation] Retrieve the episode list for a series. + Add New Series + ${resp}= Get Series Episodes ${TEST_SERIES_KEY} + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +Delete Series + [Documentation] Remove a series from the library. + Add New Series + ${resp}= Delete Series ${TEST_SERIES_KEY} + Response Should Have Status ${resp} 200 + ${get_resp}= Get Series Details ${TEST_SERIES_KEY} + Response Should Have Status ${get_resp} 404 + +# --------------------------------------------------------------------------- +# Filters +# --------------------------------------------------------------------------- +List All Series + [Documentation] List all series without filters. + ${resp}= GET API /api/anime/ + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +List Missing Episodes Only + [Documentation] Filter series to show only those with missing episodes. + ${resp}= GET API /api/anime/?filter=missing + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +List No Episodes + [Documentation] Filter series to show only those with zero episodes. + ${resp}= GET API /api/anime/?filter=no_episodes + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +# --------------------------------------------------------------------------- +# Duplicates +# --------------------------------------------------------------------------- +Duplicate Folders Detection + [Documentation] Check for duplicate folder groups in the library. + ${resp}= GET API /api/anime/duplicate-folders + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +# --------------------------------------------------------------------------- +# NFO Regeneration +# --------------------------------------------------------------------------- +Regenerate NFO For Series + [Documentation] Trigger NFO regeneration for a specific series. + Add New Series + ${resp}= POST API /api/anime/${TEST_SERIES_KEY}/regenerate-nfo + Response Should Have Status ${resp} 200 diff --git a/tests/robot/api/auth.robot b/tests/robot/api/auth.robot new file mode 100644 index 0000000..922dad8 --- /dev/null +++ b/tests/robot/api/auth.robot @@ -0,0 +1,118 @@ +*** Settings *** +Documentation Authentication API tests for Aniworld. +... Covers setup, login, logout, status, rate limiting, and JWT validation. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Create Anonymous Session +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- +Setup Master Password + [Documentation] Configure the master password for the first time. + ${resp}= POST Auth Setup ${SETUP_PASSWORD} 201 + Response Should Have Status ${resp} 201 + ${configured}= Is Auth Configured + Should Be True ${configured} + +Setup Rejects Weak Password + [Documentation] Verify that weak passwords are rejected during setup. + ${resp}= POST Auth Setup weak 400 + Response Should Have Status ${resp} 400 + +Setup Rejects Duplicate + [Documentation] Verify that setup cannot be performed twice. + Setup Master Password + ${resp}= POST Auth Setup AnotherPass123! 400 + Response Should Have Status ${resp} 400 + +# --------------------------------------------------------------------------- +# Login +# --------------------------------------------------------------------------- +Login With Valid Password + [Documentation] Log in with the correct master password and receive a JWT token. + Setup Master Password + ${resp}= POST Auth Login ${SETUP_PASSWORD} 200 + Response Should Have Status ${resp} 200 + ${token}= Get JSON Value ${resp} $.access_token + Should Not Be Empty ${token} + +Login With Invalid Password + [Documentation] Log in with an incorrect password and receive 401. + Setup Master Password + ${resp}= POST Auth Login WrongPass123! 401 + Response Should Have Status ${resp} 401 + +Login Rate Limiting + [Documentation] Verify that repeated failed login attempts trigger rate limiting. + Setup Master Password + FOR ${i} IN RANGE 6 + ${resp}= POST Auth Login WrongPass123! expected_status=ANY + END + ${resp}= POST Auth Login WrongPass123! expected_status=ANY + Should Be True ${resp.status_code} >= 429 or ${resp.status_code} == 401 + +# --------------------------------------------------------------------------- +# Auth Status +# --------------------------------------------------------------------------- +Auth Status Unconfigured + [Documentation] Check auth status before any setup has occurred. + ${resp}= GET Auth Status 200 + Response Should Have Status ${resp} 200 + ${configured}= Get JSON Value ${resp} $.configured + Should Be Equal As Strings ${configured} False + +Auth Status Configured Unauthenticated + [Documentation] Check auth status after setup but without a token. + Setup Master Password + ${resp}= GET Auth Status 200 + Response Should Have Status ${resp} 200 + ${configured}= Get JSON Value ${resp} $.configured + Should Be Equal As Strings ${configured} True + ${authenticated}= Get JSON Value ${resp} $.authenticated + Should Be Equal As Strings ${authenticated} False + +Auth Status Authenticated + [Documentation] Check auth status with a valid Bearer token. + Setup Master Password + ${token}= Login And Get Token + ${headers}= Create Dictionary Authorization=Bearer ${token} + Create Session authed ${BASE_URL} headers=${headers} + ${resp}= GET On Session authed /api/auth/status expected_status=200 + Response Should Have Status ${resp} 200 + ${authenticated}= Get JSON Value ${resp} $.authenticated + Should Be Equal As Strings ${authenticated} True + +# --------------------------------------------------------------------------- +# Logout +# --------------------------------------------------------------------------- +Logout + [Documentation] Log out and verify the token is invalidated. + Setup Master Password + ${token}= Login And Get Token + ${headers}= Create Dictionary Authorization=Bearer ${token} + Create Session authed ${BASE_URL} headers=${headers} + ${resp}= POST On Session authed /api/auth/logout expected_status=200 + Response Should Have Status ${resp} 200 + +# --------------------------------------------------------------------------- +# Protected Endpoints +# --------------------------------------------------------------------------- +Protected Endpoint Without Auth + [Documentation] Verify that protected endpoints reject unauthenticated requests. + Setup Master Password + ${resp}= GET On Session anon /api/anime/ expected_status=401 + Response Should Have Status ${resp} 401 + +Protected Endpoint With Auth + [Documentation] Verify that protected endpoints accept authenticated requests. + Setup Master Password + ${token}= Login And Get Token + ${headers}= Create Dictionary Authorization=Bearer ${token} + Create Session authed ${BASE_URL} headers=${headers} + ${resp}= GET On Session authed /api/anime/ expected_status=200 + Response Should Have Status ${resp} 200 diff --git a/tests/robot/api/config.robot b/tests/robot/api/config.robot new file mode 100644 index 0000000..a2495bd --- /dev/null +++ b/tests/robot/api/config.robot @@ -0,0 +1,138 @@ +*** Settings *** +Documentation Configuration API tests for Aniworld. +... Covers get, update, validate, backup create/list/restore/delete, and export/import. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Get / Update Config +# --------------------------------------------------------------------------- +Get Default Config + [Documentation] Retrieve the current configuration and verify its structure. + ${resp}= GET API /api/config + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} name data_dir scheduler logging backup nfo + +Update Config + [Documentation] Update configuration fields and verify they are persisted. + ${payload}= Create Dictionary + ... name=UpdatedAniworld + ... data_dir=data + ... scheduler={'enabled': False, 'schedule_time': '04:00', 'schedule_days': ['mon', 'tue']} + ... logging={'level': 'DEBUG', 'file': 'logs/test.log', 'max_bytes': 1048576, 'backup_count': 5} + ... backup={'enabled': False} + ... nfo={'tmdb_api_key': '', 'auto_create': False, 'download_poster': False, 'download_logo': False, 'download_fanart': False} + ... other={'anime_directory': '/tmp/aniworld_test_anime'} + ${resp}= Update Config ${payload} + Response Should Have Status ${resp} 200 + ${config}= Get Current Config + Should Be Equal As Strings ${config}[name] UpdatedAniworld + +# --------------------------------------------------------------------------- +# Config Validation +# --------------------------------------------------------------------------- +Validate Valid Config + [Documentation] Validate a well-formed configuration. + ${payload}= Create Dictionary + ... name=Aniworld + ... data_dir=data + ... scheduler={'enabled': True, 'schedule_time': '03:00', 'schedule_days': ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']} + ... logging={'level': 'INFO', 'file': None, 'max_bytes': None, 'backup_count': 3} + ... backup={'enabled': False} + ... nfo={'tmdb_api_key': '', 'auto_create': True, 'download_poster': True, 'download_logo': True, 'download_fanart': True} + ... other={} + ${resp}= Validate Config ${payload} 200 + Response Should Have Status ${resp} 200 + ${valid}= Get JSON Value ${resp} $.valid + Should Be Equal As Strings ${valid} True + +Validate Invalid Config Missing Name + [Documentation] Validate a config missing the required 'name' field. + ${payload}= Create Dictionary + ... data_dir=data + ... scheduler={'enabled': True, 'schedule_time': '03:00', 'schedule_days': ['mon']} + ... logging={'level': 'INFO'} + ... backup={'enabled': False} + ... nfo={'auto_create': False} + ... other={} + ${resp}= Validate Config ${payload} 422 + Response Should Have Status ${resp} 422 + +Validate Invalid Schedule Time + [Documentation] Validate a config with malformed schedule_time. + ${payload}= Create Dictionary + ... name=Aniworld + ... data_dir=data + ... scheduler={'enabled': True, 'schedule_time': '25:00', 'schedule_days': ['mon']} + ... logging={'level': 'INFO'} + ... backup={'enabled': False} + ... nfo={'auto_create': False} + ... other={} + ${resp}= Validate Config ${payload} 422 + Response Should Have Status ${resp} 422 + +Validate Invalid Log Level + [Documentation] Validate a config with an invalid logging level. + ${payload}= Create Dictionary + ... name=Aniworld + ... data_dir=data + ... scheduler={'enabled': True, 'schedule_time': '03:00', 'schedule_days': ['mon']} + ... logging={'level': 'INVALID', 'file': None, 'max_bytes': None, 'backup_count': 3} + ... backup={'enabled': False} + ... nfo={'auto_create': False} + ... other={} + ${resp}= Validate Config ${payload} 422 + Response Should Have Status ${resp} 422 + +# --------------------------------------------------------------------------- +# Backup Management +# --------------------------------------------------------------------------- +Config Backup Create + [Documentation] Create a configuration backup and verify it exists. + ${resp}= POST API /api/config/backups expected_status=201 + Response Should Have Status ${resp} 201 + +Config Backup List + [Documentation] List configuration backups and verify the response structure. + POST API /api/config/backups expected_status=201 + ${resp}= GET API /api/config/backups + Response Should Have Status ${resp} 200 + Response Should Contain Key ${resp} $.backups + +Config Backup Restore + [Documentation] Create a backup, change config, then restore and verify reversion. + ${resp}= POST API /api/config/backups expected_status=201 + ${json}= Convert String To Json ${resp.text} + ${backup_name}= Get Value From Json ${json} $.backup_name + ${name_before}= Set Variable ${backup_name}[0] + # Change config + ${payload}= Create Dictionary + ... name=BeforeRestore + ... data_dir=data + ... scheduler={'enabled': False, 'schedule_time': '03:00', 'schedule_days': ['mon']} + ... logging={'level': 'INFO', 'file': None, 'max_bytes': None, 'backup_count': 3} + ... backup={'enabled': False} + ... nfo={'auto_create': False} + ... other={} + Update Config ${payload} + # Restore + ${resp}= POST API /api/config/backups/${name_before}/restore + Response Should Have Status ${resp} 200 + +Config Backup Delete + [Documentation] Create a backup and then delete it. + ${resp}= POST API /api/config/backups expected_status=201 + ${json}= Convert String To Json ${resp.text} + ${backup_name}= Get Value From Json ${json} $.backup_name + ${name}= Set Variable ${backup_name}[0] + ${del_resp}= DELETE API /api/config/backups/${name} + Response Should Have Status ${del_resp} 200 diff --git a/tests/robot/api/download.robot b/tests/robot/api/download.robot new file mode 100644 index 0000000..e3f0b90 --- /dev/null +++ b/tests/robot/api/download.robot @@ -0,0 +1,145 @@ +*** Settings *** +Documentation Download queue API tests for Aniworld. +... Covers status, add, start/stop/pause/resume, clear, retry, and remove. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Queue Status +# --------------------------------------------------------------------------- +Get Empty Queue Status + [Documentation] Retrieve queue status when the queue is empty. + ${resp}= Get Queue Status + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} status statistics + ${pending}= Get JSON Value ${resp} $.statistics.pending + Should Be Equal As Integers ${pending} 0 + +# --------------------------------------------------------------------------- +# Add To Queue +# --------------------------------------------------------------------------- +Add Episodes To Queue + [Documentation] Add episodes to the download queue. + ${episodes}= Create List + ... {'season': 1, 'episode': 1, 'title': 'Episode 1'} + ... {'season': 1, 'episode': 2, 'title': 'Episode 2'} + ${payload}= Create Dictionary + ... serie_id=test-series + ... serie_folder=Test Series + ... serie_name=Test Series + ... episodes=${episodes} + ... priority=NORMAL + ${resp}= Add To Queue ${payload} + Response Should Have Status ${resp} 201 + ${ids}= Get JSON Value ${resp} $.item_ids + Should Not Be Empty ${ids} + +# --------------------------------------------------------------------------- +# Queue Controls +# --------------------------------------------------------------------------- +Start Queue + [Documentation] Start processing the download queue. + ${resp}= Start Queue + Response Should Have Status ${resp} 200 + +Stop Queue + [Documentation] Stop processing the download queue. + ${resp}= Stop Queue + Response Should Have Status ${resp} 200 + +Pause Queue + [Documentation] Pause the download queue. + ${resp}= Pause Queue + Response Should Have Status ${resp} 200 + +Resume Queue + [Documentation] Resume the download queue. + ${resp}= Resume Queue + Response Should Have Status ${resp} 200 + +# --------------------------------------------------------------------------- +# Clear Operations +# --------------------------------------------------------------------------- +Clear Completed Downloads + [Documentation] Clear all completed downloads from the queue. + ${resp}= Clear Completed + Response Should Have Status ${resp} 200 + +Clear Failed Downloads + [Documentation] Clear all failed downloads from the queue. + ${resp}= Clear Failed + Response Should Have Status ${resp} 200 + +Clear Pending Downloads + [Documentation] Clear all pending downloads from the queue. + ${resp}= Clear Pending + Response Should Have Status ${resp} 200 + +# --------------------------------------------------------------------------- +# Item Management +# --------------------------------------------------------------------------- +Remove Item From Queue + [Documentation] Remove a specific item from the queue. + ${episodes}= Create List {'season': 1, 'episode': 1, 'title': 'Episode 1'} + ${payload}= Create Dictionary + ... serie_id=remove-test + ... serie_folder=Remove Test + ... serie_name=Remove Test + ... episodes=${episodes} + ... priority=NORMAL + ${add_resp}= Add To Queue ${payload} + ${json}= Convert String To Json ${add_resp.text} + ${ids}= Get Value From Json ${json} $.item_ids + ${item_id}= Set Variable ${ids}[0] + ${del_resp}= DELETE API /api/queue/item/${item_id} + Response Should Have Status ${del_resp} 200 + +Retry Failed Item + [Documentation] Retry a failed download item. + ${episodes}= Create List {'season': 1, 'episode': 1, 'title': 'Episode 1'} + ${payload}= Create Dictionary + ... serie_id=retry-test + ... serie_folder=Retry Test + ... serie_name=Retry Test + ... episodes=${episodes} + ... priority=NORMAL + ${add_resp}= Add To Queue ${payload} + ${json}= Convert String To Json ${add_resp.text} + ${ids}= Get Value From Json ${json} $.item_ids + ${item_id}= Set Variable ${ids}[0] + ${retry_resp}= POST API /api/queue/item/${item_id}/retry + Response Should Have Status ${retry_resp} 200 + +# --------------------------------------------------------------------------- +# Queue Statistics Accuracy +# --------------------------------------------------------------------------- +Queue Statistics Accuracy + [Documentation] Verify that queue statistics reflect the actual item counts. + # Clear first + Clear Pending + # Add 2 items + ${episodes}= Create List + ... {'season': 1, 'episode': 1, 'title': 'Ep 1'} + ... {'season': 1, 'episode': 2, 'title': 'Ep 2'} + ${payload}= Create Dictionary + ... serie_id=stats-test + ... serie_folder=Stats Test + ... serie_name=Stats Test + ... episodes=${episodes} + ... priority=NORMAL + Add To Queue ${payload} + ${resp}= Get Queue Status + Response Should Have Status ${resp} 200 + ${pending}= Get JSON Value ${resp} $.statistics.pending + Should Be Equal As Integers ${pending} 2 + ${total}= Get JSON Value ${resp} $.statistics.total + Should Be Equal As Integers ${total} 2 diff --git a/tests/robot/api/health.robot b/tests/robot/api/health.robot new file mode 100644 index 0000000..cd3fab3 --- /dev/null +++ b/tests/robot/api/health.robot @@ -0,0 +1,29 @@ +*** Settings *** +Documentation Health check API tests for Aniworld. +... Covers basic and detailed health endpoints. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Create Anonymous Session +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Basic Health +# --------------------------------------------------------------------------- +Basic Health Check + [Documentation] Verify the basic health endpoint returns 200. + ${resp}= GET On Session anon /health expected_status=200 + Response Should Have Status ${resp} 200 + ${status}= Get JSON Value ${resp} $.status + Should Be Equal As Strings ${status} healthy + +# --------------------------------------------------------------------------- +# Detailed Health +# --------------------------------------------------------------------------- +Detailed Health Check + [Documentation] Verify the detailed health endpoint returns metrics. + ${resp}= GET On Session anon /health/detailed expected_status=200 + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} status version uptime memory cpu diff --git a/tests/robot/api/logging.robot b/tests/robot/api/logging.robot new file mode 100644 index 0000000..4bdf16c --- /dev/null +++ b/tests/robot/api/logging.robot @@ -0,0 +1,56 @@ +*** Settings *** +Documentation Logging API tests for Aniworld. +... Covers config, file listing, tail, download, test messages, and cleanup. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Logging Config +# --------------------------------------------------------------------------- +Get Logging Config + [Documentation] Retrieve the current logging configuration. + ${resp}= Get Logging Config + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} level file max_bytes backup_count + +# --------------------------------------------------------------------------- +# Log Files +# --------------------------------------------------------------------------- +List Log Files + [Documentation] List available log files. + ${resp}= List Log Files + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +Tail Log File + [Documentation] Tail the contents of a log file. + ${resp}= Tail Log File aniworld.log + Response Should Have Status ${resp} 200 + +Download Log File + [Documentation] Download a log file as binary. + ${resp}= Download Log File aniworld.log + Response Should Have Status ${resp} 200 + Should Be Equal As Strings ${resp.headers['content-type']} application/octet-stream + +# --------------------------------------------------------------------------- +# Log Management +# --------------------------------------------------------------------------- +Write Test Log Messages + [Documentation] Write test log messages at various levels. + ${resp}= Write Test Logs + Response Should Have Status ${resp} 200 + +Cleanup Old Logs + [Documentation] Delete old log files. + ${resp}= Cleanup Logs + Response Should Have Status ${resp} 200 diff --git a/tests/robot/api/nfo.robot b/tests/robot/api/nfo.robot new file mode 100644 index 0000000..2322162 --- /dev/null +++ b/tests/robot/api/nfo.robot @@ -0,0 +1,50 @@ +*** Settings *** +Documentation NFO metadata API tests for Aniworld. +... Covers diagnostics, repair, needs-repair list, and scan. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# NFO Diagnostics +# --------------------------------------------------------------------------- +Get NFO Diagnostics + [Documentation] Retrieve NFO diagnostics for a series. + ${resp}= Get Nfo Diagnostics attack-on-titan + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +# --------------------------------------------------------------------------- +# NFO Repair +# --------------------------------------------------------------------------- +Repair NFO For Series + [Documentation] Trigger NFO repair for a specific series. + ${resp}= Repair Nfo attack-on-titan + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +# --------------------------------------------------------------------------- +# Needs Repair List +# --------------------------------------------------------------------------- +List Series Needing Repair + [Documentation] List all series that need NFO repair. + ${resp}= Get Needs Repair + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} total series + +# --------------------------------------------------------------------------- +# NFO Scan +# --------------------------------------------------------------------------- +Run NFO Scan + [Documentation] Run an NFO scan across all series. + ${resp}= Scan Nfo + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} total created updated errors_count duration_seconds diff --git a/tests/robot/api/scheduler.robot b/tests/robot/api/scheduler.robot new file mode 100644 index 0000000..53ad3d6 --- /dev/null +++ b/tests/robot/api/scheduler.robot @@ -0,0 +1,92 @@ +*** Settings *** +Documentation Scheduler API tests for Aniworld. +... Covers get config, update config, trigger rescan, and validation. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Get / Update Config +# --------------------------------------------------------------------------- +Get Scheduler Config + [Documentation] Retrieve the current scheduler configuration and runtime status. + ${resp}= Get Scheduler Config + Response Should Have Status ${resp} 200 + Response Should Contain Keys ${resp} success config status + ${config_keys}= Create List enabled interval_minutes schedule_time schedule_days auto_download_after_rescan + FOR ${key} IN @{config_keys} + Response Should Contain Key ${resp} $.config.${key} + END + ${status_keys}= Create List is_running next_run last_run scan_in_progress + FOR ${key} IN @{status_keys} + Response Should Contain Key ${resp} $.status.${key} + END + +Update Scheduler Config + [Documentation] Update scheduler settings and verify they are persisted. + ${payload}= Create Dictionary + ... enabled=True + ... interval_minutes=120 + ... schedule_time=04:30 + ... schedule_days=['mon', 'wed', 'fri'] + ... auto_download_after_rescan=True + ${resp}= Update Scheduler Config ${payload} + Response Should Have Status ${resp} 200 + ${saved_time}= Get JSON Value ${resp} $.config.schedule_time + Should Be Equal As Strings ${saved_time} 04:30 + ${saved_days}= Get JSON Value ${resp} $.config.schedule_days + List Should Contain Value ${saved_days} mon + +# --------------------------------------------------------------------------- +# Trigger Rescan +# --------------------------------------------------------------------------- +Trigger Manual Rescan + [Documentation] Manually trigger a scheduled rescan. + ${resp}= Trigger Rescan + Response Should Have Status ${resp} 200 + ${success}= Get JSON Value ${resp} $.success + Should Be Equal As Strings ${success} True + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- +Invalid Schedule Time + [Documentation] Verify that malformed schedule_time is rejected. + ${payload}= Create Dictionary + ... enabled=True + ... interval_minutes=60 + ... schedule_time=25:00 + ... schedule_days=['mon'] + ... auto_download_after_rescan=False + ${resp}= Update Scheduler Config ${payload} + Response Should Have Status ${resp} 422 + +Invalid Schedule Days + [Documentation] Verify that invalid day abbreviations are rejected. + ${payload}= Create Dictionary + ... enabled=True + ... interval_minutes=60 + ... schedule_time=03:00 + ... schedule_days=['monday', 'tuesday'] + ... auto_download_after_rescan=False + ${resp}= Update Scheduler Config ${payload} + Response Should Have Status ${resp} 422 + +Empty Schedule Days + [Documentation] Verify that empty schedule_days is handled. + ${payload}= Create Dictionary + ... enabled=True + ... interval_minutes=60 + ... schedule_time=03:00 + ... schedule_days=[] + ... auto_download_after_rescan=False + ${resp}= Update Scheduler Config ${payload} + Response Should Have Status ${resp} 200 diff --git a/tests/robot/api/setup.robot b/tests/robot/api/setup.robot new file mode 100644 index 0000000..681c73b --- /dev/null +++ b/tests/robot/api/setup.robot @@ -0,0 +1,35 @@ +*** Settings *** +Documentation Setup (unresolved folders) API tests for Aniworld. +... Covers listing unresolved folders, getting details, and resolving. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Unresolved Folders +# --------------------------------------------------------------------------- +List Unresolved Folders + [Documentation] List all unresolved folders in the anime directory. + ${resp}= Get Unresolved Folders + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +Get Unresolved Folder Details + [Documentation] Get details for a specific unresolved folder. + ${resp}= Get Unresolved Folder Details SomeFolder + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} + +Resolve Folder With Provider Key + [Documentation] Resolve an unresolved folder by mapping it to a provider key. + ${resp}= Resolve Folder SomeFolder attack-on-titan + Response Should Have Status ${resp} 200 + Response Should Be Valid JSON ${resp} diff --git a/tests/robot/api/websocket.robot b/tests/robot/api/websocket.robot new file mode 100644 index 0000000..f95d7ee --- /dev/null +++ b/tests/robot/api/websocket.robot @@ -0,0 +1,41 @@ +*** Settings *** +Documentation WebSocket API tests for Aniworld. +... Covers connection, room subscription, ping/pong, and notifications. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/api_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Create Authenticated Session + +Test Teardown Delete All Sessions + +*** Test Cases *** +# --------------------------------------------------------------------------- +# WebSocket Connection +# --------------------------------------------------------------------------- +Connect To WebSocket + [Documentation] Establish a WebSocket connection with a valid JWT token. + ${token}= Login And Get Token + ${ws_url}= Set Variable ws://${SERVER_HOST}:${SERVER_PORT}/ws/connect?token=${token} + ${result}= Evaluate __import__('asyncio').run(__import__('websockets').connect('${ws_url}')) + # Note: Full WebSocket testing requires a custom Python keyword library + # or using Browser library's Evaluate with inline JavaScript. + Pass Execution WebSocket connection test requires custom keyword library + +# --------------------------------------------------------------------------- +# Placeholder for WebSocket Room Subscription +# --------------------------------------------------------------------------- +Subscribe To Rooms + [Documentation] Subscribe to WebSocket rooms (downloads, queue, scan, system). + Pass Execution WebSocket room subscription requires custom keyword library + +Receive Ping Pong + [Documentation] Verify WebSocket heartbeat mechanism. + Pass Execution WebSocket ping/pong requires custom keyword library + +Receive System Notification + [Documentation] Trigger an action that emits a system message and verify receipt. + Pass Execution WebSocket notification requires custom keyword library diff --git a/tests/robot/fixtures/sample_config.json b/tests/robot/fixtures/sample_config.json new file mode 100644 index 0000000..66dff28 --- /dev/null +++ b/tests/robot/fixtures/sample_config.json @@ -0,0 +1,32 @@ +{ + "name": "ImportedAniworld", + "data_dir": "data", + "scheduler": { + "enabled": false, + "interval_minutes": 60, + "schedule_time": "02:00", + "schedule_days": ["mon", "tue", "wed", "thu", "fri"], + "auto_download_after_rescan": false + }, + "logging": { + "level": "DEBUG", + "file": "logs/imported.log", + "max_bytes": 2097152, + "backup_count": 5 + }, + "backup": { + "enabled": true, + "path": "/tmp/imported_backups", + "keep_days": 7 + }, + "nfo": { + "tmdb_api_key": "imported_test_key", + "auto_create": true, + "download_poster": true, + "download_logo": true, + "download_fanart": true + }, + "other": { + "anime_directory": "/tmp/imported_anime" + } +} diff --git a/tests/robot/resources/api_keywords.resource b/tests/robot/resources/api_keywords.resource new file mode 100644 index 0000000..9bebaa9 --- /dev/null +++ b/tests/robot/resources/api_keywords.resource @@ -0,0 +1,306 @@ +*** Settings *** +Documentation API-specific keywords for Aniworld Robot Framework tests. +... Wraps common HTTP patterns and JSON assertions for REST API testing. + +Resource ${CURDIR}/common.resource + +*** Keywords *** +# --------------------------------------------------------------------------- +# Generic API Helpers +# --------------------------------------------------------------------------- +GET API + [Arguments] ${endpoint} ${expected_status}=200 ${session}=auth + [Documentation] Perform an authenticated GET request and return the response. + ${resp}= GET On Session ${session} ${endpoint} expected_status=${expected_status} + RETURN ${resp} + +POST API + [Arguments] ${endpoint} ${payload}=${EMPTY} ${expected_status}=200 ${session}=auth + [Documentation] Perform an authenticated POST request with optional JSON payload. + IF '${payload}' == '${EMPTY}' + ${resp}= POST On Session ${session} ${endpoint} expected_status=${expected_status} + ELSE + ${resp}= POST On Session ${session} ${endpoint} json=${payload} expected_status=${expected_status} + END + RETURN ${resp} + +PUT API + [Arguments] ${endpoint} ${payload} ${expected_status}=200 ${session}=auth + [Documentation] Perform an authenticated PUT request with JSON payload. + ${resp}= PUT On Session ${session} ${endpoint} json=${payload} expected_status=${expected_status} + RETURN ${resp} + +DELETE API + [Arguments] ${endpoint} ${expected_status}=200 ${session}=auth + [Documentation] Perform an authenticated DELETE request. + ${resp}= DELETE On Session ${session} ${endpoint} expected_status=${expected_status} + RETURN ${resp} + +# --------------------------------------------------------------------------- +# JSON Response Helpers +# --------------------------------------------------------------------------- +Get JSON Value + [Arguments] ${response} ${json_path} + [Documentation] Extract a value from a JSON response using JSONPath. + ${json}= Convert String To Json ${response.text} + ${values}= Get Value From Json ${json} ${json_path} + RETURN ${values}[0] + +Response Should Contain Keys + [Arguments] ${response} @{keys} + [Documentation] Assert that the JSON response contains all the specified top-level keys. + ${json}= Convert String To Json ${response.text} + FOR ${key} IN @{keys} + Dictionary Should Contain Key ${json} ${key} + END + +Response Should Be Valid JSON + [Arguments] ${response} + [Documentation] Assert that the response body is valid JSON. + ${json}= Convert String To Json ${response.text} + Should Not Be Empty ${json} + +# --------------------------------------------------------------------------- +# Auth-Specific API Helpers +# --------------------------------------------------------------------------- +POST Auth Setup + [Arguments] ${password} ${expected_status}=201 + [Documentation] POST to /api/auth/setup with the given password. + ${payload}= Create Dictionary master_password=${password} + ${resp}= POST On Session anon /api/auth/setup json=${payload} expected_status=${expected_status} + RETURN ${resp} + +POST Auth Login + [Arguments] ${password} ${expected_status}=200 + [Documentation] POST to /api/auth/login with the given password. + ${payload}= Create Dictionary password=${password} + ${resp}= POST On Session anon /api/auth/login json=${payload} expected_status=${expected_status} + RETURN ${resp} + +GET Auth Status + [Arguments] ${expected_status}=200 + [Documentation] GET /api/auth/status. + ${resp}= GET On Session anon /api/auth/status expected_status=${expected_status} + RETURN ${resp} + +# --------------------------------------------------------------------------- +# Config-Specific API Helpers +# --------------------------------------------------------------------------- +Get Current Config + [Documentation] GET /api/config and return the parsed JSON. + ${resp}= GET API /api/config + ${json}= Convert String To Json ${resp.text} + RETURN ${json} + +Update Config + [Arguments] ${payload} + [Documentation] PUT /api/config with the given payload. + ${resp}= PUT API /api/config ${payload} + RETURN ${resp} + +Validate Config + [Arguments] ${payload} ${expected_status}=200 + [Documentation] POST /api/config/validate with the given payload. + ${resp}= POST API /api/config/validate ${payload} ${expected_status} + RETURN ${resp} + +# --------------------------------------------------------------------------- +# Queue-Specific API Helpers +# --------------------------------------------------------------------------- +Get Queue Status + [Documentation] GET /api/queue/status and return the response. + ${resp}= GET API /api/queue/status + RETURN ${resp} + +Add To Queue + [Arguments] ${payload} + [Documentation] POST /api/queue/add with the given payload. + ${resp}= POST API /api/queue/add ${payload} 201 + RETURN ${resp} + +Start Queue + [Documentation] POST /api/queue/start. + ${resp}= POST API /api/queue/start + RETURN ${resp} + +Stop Queue + [Documentation] POST /api/queue/stop. + ${resp}= POST API /api/queue/stop + RETURN ${resp} + +Pause Queue + [Documentation] POST /api/queue/pause. + ${resp}= POST API /api/queue/pause + RETURN ${resp} + +Resume Queue + [Documentation] POST /api/queue/resume. + ${resp}= POST API /api/queue/resume + RETURN ${resp} + +Clear Completed + [Documentation] DELETE /api/queue/completed. + ${resp}= DELETE API /api/queue/completed + RETURN ${resp} + +Clear Failed + [Documentation] DELETE /api/queue/failed. + ${resp}= DELETE API /api/queue/failed + RETURN ${resp} + +Clear Pending + [Documentation] DELETE /api/queue/pending. + ${resp}= DELETE API /api/queue/pending + RETURN ${resp} + +# --------------------------------------------------------------------------- +# Anime-Specific API Helpers +# --------------------------------------------------------------------------- +Get Anime Status + [Documentation] GET /api/anime/status. + ${resp}= GET API /api/anime/status + RETURN ${resp} + +Rescan Library + [Documentation] POST /api/anime/rescan. + ${resp}= POST API /api/anime/rescan + RETURN ${resp} + +Get Scan Status + [Documentation] GET /api/anime/scan/status. + ${resp}= GET API /api/anime/scan/status + RETURN ${resp} + +Search Anime + [Arguments] ${query} + [Documentation] GET /api/anime/search?q={query}. + ${resp}= GET API /api/anime/search?q=${query} + RETURN ${resp} + +Add Series + [Arguments] ${link} ${name} ${year}=${EMPTY} + [Documentation] POST /api/anime/add with series details. + ${payload}= Create Dictionary link=${link} name=${name} + IF '${year}' != '${EMPTY}' + Set To Dictionary ${payload} year=${year} + END + ${resp}= POST API /api/anime/add ${payload} 201 + RETURN ${resp} + +Get Series Details + [Arguments] ${key} + [Documentation] GET /api/anime/{key}. + ${resp}= GET API /api/anime/${key} + RETURN ${resp} + +Delete Series + [Arguments] ${key} + [Documentation] DELETE /api/anime/{key}. + ${resp}= DELETE API /api/anime/${key} + RETURN ${resp} + +Get Series Episodes + [Arguments] ${key} + [Documentation] GET /api/anime/{key}/episodes. + ${resp}= GET API /api/anime/${key}/episodes + RETURN ${resp} + +# --------------------------------------------------------------------------- +# Scheduler-Specific API Helpers +# --------------------------------------------------------------------------- +Get Scheduler Config + [Documentation] GET /api/scheduler/config. + ${resp}= GET API /api/scheduler/config + RETURN ${resp} + +Update Scheduler Config + [Arguments] ${payload} + [Documentation] POST /api/scheduler/config. + ${resp}= POST API /api/scheduler/config ${payload} + RETURN ${resp} + +Trigger Rescan + [Documentation] POST /api/scheduler/trigger-rescan. + ${resp}= POST API /api/scheduler/trigger-rescan + RETURN ${resp} + +# --------------------------------------------------------------------------- +# NFO-Specific API Helpers +# --------------------------------------------------------------------------- +Get Nfo Diagnostics + [Arguments] ${key} + [Documentation] GET /api/nfo/{key}/diagnostics. + ${resp}= GET API /api/nfo/${key}/diagnostics + RETURN ${resp} + +Repair Nfo + [Arguments] ${key} + [Documentation] POST /api/nfo/{key}/repair. + ${resp}= POST API /api/nfo/${key}/repair + RETURN ${resp} + +Get Needs Repair + [Documentation] GET /api/nfo/needs-repair. + ${resp}= GET API /api/nfo/needs-repair + RETURN ${resp} + +Scan Nfo + [Documentation] POST /api/nfo/scan. + ${resp}= POST API /api/nfo/scan + RETURN ${resp} + +# --------------------------------------------------------------------------- +# Logging-Specific API Helpers +# --------------------------------------------------------------------------- +Get Logging Config + [Documentation] GET /api/logging/config. + ${resp}= GET API /api/logging/config + RETURN ${resp} + +List Log Files + [Documentation] GET /api/logging/files. + ${resp}= GET API /api/logging/files + RETURN ${resp} + +Tail Log File + [Arguments] ${filename} + [Documentation] GET /api/logging/files/{filename}/tail. + ${resp}= GET API /api/logging/files/${filename}/tail + RETURN ${resp} + +Download Log File + [Arguments] ${filename} + [Documentation] GET /api/logging/files/{filename}/download. + ${resp}= GET API /api/logging/files/${filename}/download + RETURN ${resp} + +Write Test Logs + [Documentation] POST /api/logging/test. + ${resp}= POST API /api/logging/test + RETURN ${resp} + +Cleanup Logs + [Documentation] POST /api/logging/cleanup. + ${resp}= POST API /api/logging/cleanup + RETURN ${resp} + +# --------------------------------------------------------------------------- +# Setup-Specific API Helpers +# --------------------------------------------------------------------------- +Get Unresolved Folders + [Documentation] GET /api/setup/unresolved. + ${resp}= GET API /api/setup/unresolved + RETURN ${resp} + +Get Unresolved Folder Details + [Arguments] ${folder_name} + [Documentation] GET /api/setup/unresolved/{folder_name}. + ${resp}= GET API /api/setup/unresolved/${folder_name} + RETURN ${resp} + +Resolve Folder + [Arguments] ${folder_name} ${provider_key} + [Documentation] POST /api/setup/unresolved/{folder_name}/resolve. + ${payload}= Create Dictionary provider_key=${provider_key} + ${resp}= POST API /api/setup/unresolved/${folder_name}/resolve ${payload} + RETURN ${resp} diff --git a/tests/robot/resources/common.resource b/tests/robot/resources/common.resource new file mode 100644 index 0000000..fd69b1c --- /dev/null +++ b/tests/robot/resources/common.resource @@ -0,0 +1,146 @@ +*** Settings *** +Documentation Common shared resources for Aniworld Robot Framework tests. +... Provides keywords for server lifecycle, authentication, sessions, and cleanup. + +Library Process +Library Browser +Library RequestsLibrary +Library JSONLibrary +Library OperatingSystem +Library String +Library Collections +Library DateTime + +*** Variables *** +${BASE_URL} http://127.0.0.1:8765 +${SERVER_HOST} 127.0.0.1 +${SERVER_PORT} 8765 +${SETUP_PASSWORD} TestPass123! +${BROWSER} chromium +${HEADLESS} True +${SERVER_PROCESS} ${EMPTY} + +*** Keywords *** +# --------------------------------------------------------------------------- +# Server Lifecycle +# --------------------------------------------------------------------------- +Start Aniworld Server + [Documentation] Start the FastAPI uvicorn server as a background process. + ${result}= Run Process conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app + ... --host ${SERVER_HOST} --port ${SERVER_PORT} + ... --no-access-log + ... cwd=/home/lukas/Volume/repo/Aniworld + ... stdout=${CURDIR}/../fixtures/server_stdout.log + ... stderr=${CURDIR}/../fixtures/server_stderr.log + Set Suite Variable ${SERVER_PROCESS} ${result} + Sleep 3s + +Stop Aniworld Server + [Documentation] Terminate the uvicorn server process and clean up log files. + Run Keyword And Ignore Error Terminate Process ${SERVER_PROCESS} + Run Keyword And Ignore Error Remove File ${CURDIR}/../fixtures/server_stdout.log + Run Keyword And Ignore Error Remove File ${CURDIR}/../fixtures/server_stderr.log + +Wait For Server + [Documentation] Poll the health endpoint until the server responds with 200. + ... Retries every 1 second for up to 30 seconds. + FOR ${i} IN RANGE 30 + ${resp}= Run Keyword And Ignore Error + ... GET On Session anon /health expected_status=200 + IF '${resp}[0]' == 'PASS' + RETURN + END + Sleep 1s + END + Fail Server did not become healthy within 30 seconds + +Initialize Browser + [Documentation] Initialize Browser library with the configured browser and headless mode. + New Browser ${BROWSER} headless=${HEADLESS} + New Context viewport={'width': 1280, 'height': 720} + +# --------------------------------------------------------------------------- +# HTTP Session Management +# --------------------------------------------------------------------------- +Create Anonymous Session + [Documentation] Create an unauthenticated HTTP session. + Create Session anon ${BASE_URL} + +Create Authenticated Session + [Documentation] Create an authenticated HTTP session. Performs setup if needed, then logs in. + Create Anonymous Session + ${configured}= Is Auth Configured + IF not ${configured} + Setup Master Password + END + ${token}= Login And Get Token + Create Session auth ${BASE_URL} headers={'Authorization': 'Bearer ${token}'} + +# --------------------------------------------------------------------------- +# Authentication Helpers +# --------------------------------------------------------------------------- +Is Auth Configured + [Documentation] Check if the master password has already been configured. + ${resp}= GET On Session anon /api/auth/status expected_status=200 + ${json}= Convert String To Json ${resp.text} + ${configured}= Get Value From Json ${json} $.configured + RETURN ${configured}[0] + +Setup Master Password + [Documentation] Configure the master password via the setup endpoint. + ${payload}= Create Dictionary + ... master_password=${SETUP_PASSWORD} + ... anime_directory=/tmp/aniworld_test_anime + ... name=AniworldTest + ... data_dir=data + ... scheduler_enabled=False + ... logging_level=INFO + ... backup_enabled=False + ${resp}= POST On Session anon /api/auth/setup json=${payload} expected_status=201 + Should Be Equal As Integers ${resp.status_code} 201 + +Login And Get Token + [Documentation] Log in with the master password and return the JWT access token. + ${payload}= Create Dictionary password=${SETUP_PASSWORD} + ${resp}= POST On Session anon /api/auth/login json=${payload} expected_status=200 + ${json}= Convert String To Json ${resp.text} + ${token}= Get Value From Json ${json} $.access_token + RETURN ${token}[0] + +# --------------------------------------------------------------------------- +# State Reset +# --------------------------------------------------------------------------- +Reset Application State + [Documentation] Reset auth, config, and database state for test isolation. + ... Calls internal cleanup endpoints if available; otherwise warns. + Run Keyword And Ignore Error POST On Session auth /api/admin/reset expected_status=200 + # Fallback: clear local files + Run Keyword And Ignore Error Remove Directory /tmp/aniworld_test_anime recursive=True + Run Keyword And Ignore Error Create Directory /tmp/aniworld_test_anime + +# --------------------------------------------------------------------------- +# Browser Navigation Helpers +# --------------------------------------------------------------------------- +Open Browser To Page + [Arguments] ${path}=/ + [Documentation] Open a new page at the given path relative to BASE_URL. + New Page ${BASE_URL}${path} + +Close Browser + [Documentation] Close all browser contexts and the browser itself. + Browser.Close Browser ALL + +# --------------------------------------------------------------------------- +# Assertion Helpers +# --------------------------------------------------------------------------- +Response Should Contain Key + [Arguments] ${response} ${json_path} + [Documentation] Assert that the JSON response contains the given JSONPath key. + ${json}= Convert String To Json ${response.text} + ${values}= Get Value From Json ${json} ${json_path} + Should Not Be Empty ${values} Expected JSON path '${json_path}' not found in response + +Response Should Have Status + [Arguments] ${response} ${expected_status} + [Documentation] Assert that the response status code matches the expected value. + Should Be Equal As Integers ${response.status_code} ${expected_status} diff --git a/tests/robot/resources/ui_keywords.resource b/tests/robot/resources/ui_keywords.resource new file mode 100644 index 0000000..3195b43 --- /dev/null +++ b/tests/robot/resources/ui_keywords.resource @@ -0,0 +1,204 @@ +*** Settings *** +Documentation UI-specific keywords for Aniworld Robot Framework tests. +... Wraps Browser library interactions for page navigation, form filling, +... and common UI assertions. + +Resource ${CURDIR}/common.resource + +*** Keywords *** +# --------------------------------------------------------------------------- +# Navigation +# --------------------------------------------------------------------------- +Navigate To + [Arguments] ${path} + [Documentation] Navigate the browser to the given path relative to BASE_URL. + Go To ${BASE_URL}${path} + +Reload Page + [Documentation] Reload the current page. + Reload + +# --------------------------------------------------------------------------- +# Authentication UI Flows +# --------------------------------------------------------------------------- +Perform Login + [Arguments] ${password}=${SETUP_PASSWORD} + [Documentation] Fill the login form and submit it. + Fill Text id=password-input ${password} + Click id=login-submit-btn + Wait For Elements State id=search-input visible timeout=5s + +Perform Logout + [Documentation] Click the logout button and wait for redirect to login. + Click id=logout-btn + Wait For Elements State id=password-input visible timeout=5s + +# --------------------------------------------------------------------------- +# Setup UI Flow +# --------------------------------------------------------------------------- +Complete Setup Flow + [Arguments] ${password}=${SETUP_PASSWORD} + ... ${anime_dir}=/tmp/aniworld_test_anime + ... ${app_name}=AniworldTest + [Documentation] Fill the entire setup form and submit it. + # General section + Fill Text id=app-name-input ${app_name} + Fill Text id=data-dir-input data + Fill Text id=anime-directory-input ${anime_dir} + # Security section + Fill Text id=master-password-input ${password} + Fill Text id=confirm-password-input ${password} + # Scheduler section + Uncheck Checkbox id=scheduler-enabled + # Submit + Click id=setup-submit-btn + Wait For Elements State id=search-input visible timeout=10s + +# --------------------------------------------------------------------------- +# Settings Modal +# --------------------------------------------------------------------------- +Open Settings Modal + [Documentation] Click the config button and wait for the modal to appear. + Click id=config-btn + Wait For Elements State id=config-modal visible timeout=5s + +Close Settings Modal + [Documentation] Click the close button and wait for the modal to disappear. + Click id=close-config + Wait For Elements State id=config-modal hidden timeout=5s + +Save Settings + [Documentation] Click the save button inside the settings modal. + Click id=save-main-config + Wait For Elements State .toast, .notification visible timeout=5s + +Fill General Settings + [Arguments] ${app_name} ${data_dir} ${anime_dir} + [Documentation] Fill the general settings section in the modal. + Fill Text id=app-name-input ${app_name} + Fill Text id=data-dir-input ${data_dir} + Fill Text id=anime-directory-input ${anime_dir} + +Fill Scheduler Settings + [Arguments] ${enabled}=False ${time}=03:00 @{days} + [Documentation] Fill the scheduler settings section. + IF ${enabled} + Check Checkbox id=scheduled-rescan-enabled + ELSE + Uncheck Checkbox id=scheduled-rescan-enabled + END + Fill Text id=scheduler-time-input ${time} + # Days selection would be handled by multi-select UI elements + +Fill NFO Settings + [Arguments] ${tmdb_key}=${EMPTY} ${auto_create}=False + [Documentation] Fill the NFO settings section. + Fill Text id=tmdb-api-key-input ${tmdb_key} + IF ${auto_create} + Check Checkbox id=nfo-auto-create + ELSE + Uncheck Checkbox id=nfo-auto-create + END + +# --------------------------------------------------------------------------- +# Search +# --------------------------------------------------------------------------- +Search For Anime + [Arguments] ${query} + [Documentation] Type a query into the search box and submit. + Fill Text id=search-input ${query} + Click id=search-btn + Wait For Elements State id=search-results visible timeout=5s + +Clear Search + [Documentation] Click the clear search button. + Click id=clear-search + Wait For Elements State id=search-results hidden timeout=3s + +# --------------------------------------------------------------------------- +# Theme +# --------------------------------------------------------------------------- +Toggle Theme + [Documentation] Click the theme toggle button. + Click id=theme-toggle + +Get Current Theme + [Documentation] Return the current theme attribute from the HTML element. + ${theme}= Get Attribute html data-theme + RETURN ${theme} + +# --------------------------------------------------------------------------- +# Queue Page +# --------------------------------------------------------------------------- +Navigate To Queue Page + [Documentation] Navigate to the download queue page. + Navigate To /queue + Wait For Elements State id=pending-queue visible timeout=5s + +Start Queue From UI + [Documentation] Click the start queue button on the queue page. + Click id=start-queue-btn + +Stop Queue From UI + [Documentation] Click the stop queue button on the queue page. + Click id=stop-queue-btn + +Clear Completed From UI + [Documentation] Click clear completed and confirm the modal. + Click id=clear-completed-btn + Wait For Elements State id=confirm-modal visible timeout=3s + Click id=confirm-ok + Wait For Elements State id=confirm-modal hidden timeout=3s + +Clear Failed From UI + [Documentation] Click clear failed and confirm the modal. + Click id=clear-failed-btn + Wait For Elements State id=confirm-modal visible timeout=3s + Click id=confirm-ok + Wait For Elements State id=confirm-modal hidden timeout=3s + +# --------------------------------------------------------------------------- +# Common UI Assertions +# --------------------------------------------------------------------------- +Page Title Should Contain + [Arguments] ${text} + [Documentation] Assert that the page title contains the given text. + ${title}= Get Title + Should Contain ${title} ${text} + +Element Should Be Visible + [Arguments] ${selector} + [Documentation] Assert that the element matching the selector is visible. + Wait For Elements State ${selector} visible timeout=5s + +Element Should Be Hidden + [Arguments] ${selector} + [Documentation] Assert that the element matching the selector is hidden. + Wait For Elements State ${selector} hidden timeout=5s + +Element Text Should Contain + [Arguments] ${selector} ${text} + [Documentation] Assert that the element's text contains the given substring. + ${element_text}= Get Text ${selector} + Should Contain ${element_text} ${text} + +Toast Should Contain + [Arguments] ${text} + [Documentation] Assert that a toast/notification contains the given text. + ${toast}= Get Text .toast, .notification + Should Contain ${toast} ${text} + +# --------------------------------------------------------------------------- +# Responsive Helpers +# --------------------------------------------------------------------------- +Set Mobile Viewport + [Documentation] Resize the browser viewport to mobile dimensions. + Set Viewport Size 375 667 + +Set Tablet Viewport + [Documentation] Resize the browser viewport to tablet dimensions. + Set Viewport Size 768 1024 + +Set Desktop Viewport + [Documentation] Resize the browser viewport to desktop dimensions. + Set Viewport Size 1280 720 diff --git a/tests/robot/run.ps1 b/tests/robot/run.ps1 new file mode 100644 index 0000000..b7e4e59 --- /dev/null +++ b/tests/robot/run.ps1 @@ -0,0 +1,78 @@ +# Run Aniworld Robot Framework tests on Windows (PowerShell) +# Usage: .\run.ps1 [-SuitePattern "api/auth.robot"] +# Examples: +# .\run.ps1 # Run all tests +# .\run.ps1 -SuitePattern "api/" # Run all API tests +# .\run.ps1 -SuitePattern "ui/" # Run all UI tests + +param( + [string]$SuitePattern = "tests/robot/" +) + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$robotDir = Split-Path -Parent $scriptDir +$projectDir = Split-Path -Parent $robotDir +$resultsDir = Join-Path $robotDir "results" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Aniworld Robot Framework Test Runner" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "Project dir: $projectDir" +Write-Host "Results dir: $resultsDir" +Write-Host "Suite pattern: $SuitePattern" +Write-Host "" + +# Ensure we're in the project directory +Set-Location $projectDir + +# Check Python +if (-not (Get-Command python -ErrorAction SilentlyContinue)) { + Write-Error "Error: python not found in PATH" + exit 1 +} + +# Install/update Robot Framework dependencies +Write-Host "Installing Robot Framework dependencies..." -ForegroundColor Yellow +& python -m pip install -q robotframework robotframework-browser robotframework-requests robotframework-jsonlibrary 2>$null + +# Initialize Browser library (Playwright) +Write-Host "Initializing Browser library (Playwright)..." -ForegroundColor Yellow +& python -m Browser.entry init 2>$null +if ($LASTEXITCODE -ne 0) { + Write-Warning "Browser library init may need manual run: 'rfbrowser init'" +} + +# Create results directory +New-Item -ItemType Directory -Force -Path $resultsDir | Out-Null + +# Run tests +Write-Host "" +Write-Host "Running Robot Framework tests..." -ForegroundColor Green +Write-Host "----------------------------------------" -ForegroundColor Green + +& robot ` + --outputdir $resultsDir ` + --logtitle "Aniworld Test Log" ` + --reporttitle "Aniworld Test Report" ` + --metadata Project:Aniworld ` + --metadata Environment:"$(& python --version)" ` + --variable BASE_URL:http://127.0.0.1:8765 ` + --variable BROWSER:chromium ` + --variable HEADLESS:True ` + $SuitePattern + +$exitCode = $LASTEXITCODE + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +if ($exitCode -eq 0) { + Write-Host "All tests PASSED" -ForegroundColor Green +} else { + Write-Host "Some tests FAILED (exit code: $exitCode)" -ForegroundColor Red +} +Write-Host "Report: $resultsDir\report.html" +Write-Host "Log: $resultsDir\log.html" +Write-Host "Output: $resultsDir\output.xml" +Write-Host "========================================" -ForegroundColor Cyan + +exit $exitCode diff --git a/tests/robot/run.sh b/tests/robot/run.sh new file mode 100644 index 0000000..1ba91d9 --- /dev/null +++ b/tests/robot/run.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Run Aniworld Robot Framework tests on Unix/Linux/macOS +# Usage: ./run.sh [suite_pattern] +# Examples: +# ./run.sh # Run all tests +# ./run.sh api/auth.robot # Run specific suite +# ./run.sh ui/ # Run all UI tests + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROBOT_DIR="$(dirname "$SCRIPT_DIR")" +PROJECT_DIR="$(dirname "$ROBOT_DIR")" +RESULTS_DIR="$ROBOT_DIR/results" + +# Default: run everything +SUITE_PATTERN="${1:-tests/robot/}" + +echo "========================================" +echo "Aniworld Robot Framework Test Runner" +echo "========================================" +echo "Project dir: $PROJECT_DIR" +echo "Results dir: $RESULTS_DIR" +echo "Suite pattern: $SUITE_PATTERN" +echo "" + +# Ensure we're in the project directory +cd "$PROJECT_DIR" + +# Check Python environment +if ! command -v python >/dev/null 2>&1; then + echo "Error: python not found in PATH" + exit 1 +fi + +# Install/update Robot Framework dependencies +echo "Installing Robot Framework dependencies..." +pip install -q robotframework robotframework-browser robotframework-requests robotframework-jsonlibrary 2>/dev/null || true + +# Initialize Browser library (Playwright) +echo "Initializing Browser library (Playwright)..." +python -m Browser.entry init 2>/dev/null || rfbrowser init 2>/dev/null || echo "Warning: Browser library init may need manual run: 'rfbrowser init'" + +# Create results directory +mkdir -p "$RESULTS_DIR" + +# Run tests +echo "" +echo "Running Robot Framework tests..." +echo "----------------------------------------" + +robot \ + --outputdir "$RESULTS_DIR" \ + --logtitle "Aniworld Test Log" \ + --reporttitle "Aniworld Test Report" \ + --metadata Project:Aniworld \ + --metadata Environment:"$(python --version 2>&1)" \ + --variable BASE_URL:http://127.0.0.1:8765 \ + --variable BROWSER:chromium \ + --variable HEADLESS:True \ + "$SUITE_PATTERN" + +EXIT_CODE=$? + +echo "" +echo "========================================" +if [ $EXIT_CODE -eq 0 ]; then + echo "All tests PASSED" +else + echo "Some tests FAILED (exit code: $EXIT_CODE)" +fi +echo "Report: $RESULTS_DIR/report.html" +echo "Log: $RESULTS_DIR/log.html" +echo "Output: $RESULTS_DIR/output.xml" +echo "========================================" + +exit $EXIT_CODE diff --git a/tests/robot/ui/anime_settings.robot b/tests/robot/ui/anime_settings.robot new file mode 100644 index 0000000..4a173f0 --- /dev/null +++ b/tests/robot/ui/anime_settings.robot @@ -0,0 +1,50 @@ +*** Settings *** +Documentation Anime settings page UI tests for Aniworld. +... Covers page load, regenerate NFO, and update series settings. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Open Browser To Page /login +... AND Perform Login +... AND Wait For Elements State id=search-input visible timeout=5s + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Anime Settings Page Load +# --------------------------------------------------------------------------- +Anime Settings Page Loads + [Documentation] Navigate to anime settings page and verify form elements. + Go To ${BASE_URL}/anime/settings?key=attack-on-titan + Wait For Elements State id=anime-settings-form visible timeout=5s + Page Title Should Contain Settings + Element Should Be Visible id=anime-settings-form + Element Should Be Visible id=regenerate-nfo-btn + Element Should Be Visible id=save-anime-settings-btn + +# --------------------------------------------------------------------------- +# Regenerate NFO +# --------------------------------------------------------------------------- +Regenerate NFO + [Documentation] Click regenerate NFO and verify success notification. + Go To ${BASE_URL}/anime/settings?key=attack-on-titan + Wait For Elements State id=regenerate-nfo-btn visible timeout=5s + Click id=regenerate-nfo-btn + Toast Should Contain NFO + +# --------------------------------------------------------------------------- +# Update Series Settings +# --------------------------------------------------------------------------- +Update Series Settings + [Documentation] Change series settings and save. + Go To ${BASE_URL}/anime/settings?key=attack-on-titan + Wait For Elements State id=anime-settings-form visible timeout=5s + Select Options By id=preferred-language-select text German + Fill Text id=custom-folder-input Attack on Titan Custom + Click id=save-anime-settings-btn + Toast Should Contain saved diff --git a/tests/robot/ui/dashboard.robot b/tests/robot/ui/dashboard.robot new file mode 100644 index 0000000..3fc0f79 --- /dev/null +++ b/tests/robot/ui/dashboard.robot @@ -0,0 +1,112 @@ +*** Settings *** +Documentation Dashboard UI tests for Aniworld. +... Covers search, theme toggle, rescan, series filters, select all, and download selected. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Open Browser To Page /login +... AND Perform Login +... AND Wait For Elements State id=search-input visible timeout=5s + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Dashboard Load +# --------------------------------------------------------------------------- +Dashboard Loads + [Documentation] Verify the main dashboard loads with key elements. + Page Title Should Contain AniWorld + Element Should Be Visible id=search-input + Element Should Be Visible id=search-btn + Element Should Be Visible id=rescan-btn + Element Should Be Visible id=theme-toggle + Element Should Be Visible id=config-btn + +# --------------------------------------------------------------------------- +# Search +# --------------------------------------------------------------------------- +Search For Anime + [Documentation] Type a query and submit search, verifying results appear. + Search For Anime attack + Element Should Be Visible id=search-results-list + ${results}= Get Element Count id=search-results-list > * + Should Be True ${results} >= 0 + +Clear Search + [Documentation] Click clear search and verify results are hidden. + Search For Anime attack + Clear Search + Element Should Be Hidden id=search-results + +# --------------------------------------------------------------------------- +# Theme Toggle +# --------------------------------------------------------------------------- +Theme Toggle To Dark + [Documentation] Click theme toggle and verify dark theme is applied. + ${before}= Get Current Theme + Should Be Equal As Strings ${before} light + Toggle Theme + ${after}= Get Current Theme + Should Be Equal As Strings ${after} dark + +Theme Toggle To Light + [Documentation] Click theme toggle twice and verify back to light. + Toggle Theme + Toggle Theme + ${theme}= Get Current Theme + Should Be Equal As Strings ${theme} light + +Theme Persists After Reload + [Documentation] Toggle to dark, reload, and verify theme persists. + Toggle Theme + Reload Page + Wait For Elements State id=search-input visible timeout=5s + ${theme}= Get Current Theme + Should Be Equal As Strings ${theme} dark + +# --------------------------------------------------------------------------- +# Rescan +# --------------------------------------------------------------------------- +Rescan Button + [Documentation] Click the rescan button and verify the status indicator changes. + Click id=rescan-btn + Wait For Elements State #rescan-status .status-dot.scanning visible timeout=5s + +# --------------------------------------------------------------------------- +# Series Filters +# --------------------------------------------------------------------------- +Show Missing Episodes Only + [Documentation] Click the missing episodes filter and verify UI updates. + Click id=show-missing-only + ${active}= Get Attribute id=show-missing-only data-active + Should Be Equal As Strings ${active} true + +Show All Series + [Documentation] Click show all and verify filter is overridden. + Click id=show-all-series + ${active}= Get Attribute id=show-all-series data-active + Should Be Equal As Strings ${active} true + +# --------------------------------------------------------------------------- +# Selection & Download +# --------------------------------------------------------------------------- +Select All Series + [Documentation] Click select all and verify all series checkboxes are checked. + Click id=select-all + ${checked}= Get Element Count .series-card input:checked + Should Be True ${checked} >= 0 + +Download Selected + [Documentation] Select a series and click download selected. + # Check first series card checkbox if any exist + ${count}= Get Element Count .series-card input[type="checkbox"] + IF ${count} > 0 + Click .series-card input[type="checkbox"] >> nth=0 + Click id=download-selected + Toast Should Contain queue + END diff --git a/tests/robot/ui/login.robot b/tests/robot/ui/login.robot new file mode 100644 index 0000000..ed8b5be --- /dev/null +++ b/tests/robot/ui/login.robot @@ -0,0 +1,85 @@ +*** Settings *** +Documentation Login UI tests for Aniworld. +... Covers login page load, valid/invalid login, rate limit UI, password visibility, and logout. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Login Page Load +# --------------------------------------------------------------------------- +Login Page Loads + [Documentation] Verify the login page loads with all form elements. + Open Browser To Page /login + Page Title Should Contain Login + Element Should Be Visible id=password-input + Element Should Be Visible id=login-submit-btn + Element Should Be Visible id=password-toggle + +# --------------------------------------------------------------------------- +# Valid Login +# --------------------------------------------------------------------------- +Login With Valid Password + [Documentation] Log in with the correct password and verify redirect to dashboard. + Open Browser To Page /login + Perform Login ${SETUP_PASSWORD} + Page Title Should Contain AniWorld + Element Should Be Visible id=search-input + +# --------------------------------------------------------------------------- +# Invalid Login +# --------------------------------------------------------------------------- +Login With Invalid Password + [Documentation] Log in with an incorrect password and verify error message. + Open Browser To Page /login + Fill Text id=password-input WrongPass123! + Click id=login-submit-btn + Element Should Be Visible id=login-error + Element Text Should Contain id=login-error invalid + +# --------------------------------------------------------------------------- +# Rate Limit UI +# --------------------------------------------------------------------------- +Login Rate Limit UI + [Documentation] Submit multiple failed logins and verify lockout message appears. + Open Browser To Page /login + FOR ${i} IN RANGE 6 + Fill Text id=password-input WrongPass123! + Click id=login-submit-btn + Wait For Elements State id=login-error visible timeout=3s + END + Element Text Should Contain id=login-error lockout + +# --------------------------------------------------------------------------- +# Password Visibility Toggle +# --------------------------------------------------------------------------- +Password Visibility Toggle + [Documentation] Click the eye icon and verify the password field type toggles. + Open Browser To Page /login + Fill Text id=password-input ${SETUP_PASSWORD} + ${type_before}= Get Attribute id=password-input type + Should Be Equal As Strings ${type_before} password + Click id=password-toggle + ${type_after}= Get Attribute id=password-input type + Should Be Equal As Strings ${type_after} text + Click id=password-toggle + ${type_final}= Get Attribute id=password-input type + Should Be Equal As Strings ${type_final} password + +# --------------------------------------------------------------------------- +# Logout +# --------------------------------------------------------------------------- +Logout + [Documentation] Log in then log out and verify redirect to login page. + Open Browser To Page /login + Perform Login ${SETUP_PASSWORD} + Perform Logout + Page Title Should Contain Login + Element Should Be Visible id=password-input diff --git a/tests/robot/ui/queue_page.robot b/tests/robot/ui/queue_page.robot new file mode 100644 index 0000000..5e0bc3d --- /dev/null +++ b/tests/robot/ui/queue_page.robot @@ -0,0 +1,106 @@ +*** Settings *** +Documentation Queue page UI tests for Aniworld. +... Covers page load, stats cards, start/stop buttons, clear confirmations, retry all, and navigation. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Open Browser To Page /login +... AND Perform Login +... AND Navigate To Queue Page + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Queue Page Load +# --------------------------------------------------------------------------- +Queue Page Loads + [Documentation] Verify the queue page loads with all sections and stats. + Page Title Should Contain Queue + Element Should Be Visible id=total-items + Element Should Be Visible id=pending-items + Element Should Be Visible id=completed-items + Element Should Be Visible id=failed-items + Element Should Be Visible id=pending-queue + Element Should Be Visible id=active-downloads + Element Should Be Visible id=completed-downloads + Element Should Be Visible id=failed-downloads + +Queue Stats Cards Display Zero Initially + [Documentation] Verify all stat cards show 0 when queue is empty. + ${total}= Get Text id=total-items + Should Be Equal As Strings ${total} 0 + ${pending}= Get Text id=pending-items + Should Be Equal As Strings ${pending} 0 + ${completed}= Get Text id=completed-items + Should Be Equal As Strings ${completed} 0 + ${failed}= Get Text id=failed-items + Should Be Equal As Strings ${failed} 0 + +# --------------------------------------------------------------------------- +# Queue Controls +# --------------------------------------------------------------------------- +Start Queue Button + [Documentation] Click start queue and verify button state changes. + ${disabled}= Get Attribute id=start-queue-btn disabled + IF '${disabled}' != 'true' + Click id=start-queue-btn + Wait For Elements State id=stop-queue-btn visible timeout=3s + END + +Stop Queue Button + [Documentation] Click stop queue and verify button state changes. + ${disabled}= Get Attribute id=stop-queue-btn disabled + IF '${disabled}' != 'true' + Click id=stop-queue-btn + Wait For Elements State id=start-queue-btn visible timeout=3s + END + +# --------------------------------------------------------------------------- +# Clear Operations +# --------------------------------------------------------------------------- +Clear Completed Confirmation + [Documentation] Click clear completed and verify confirmation modal appears. + ${disabled}= Get Attribute id=clear-completed-btn disabled + IF '${disabled}' != 'true' + Click id=clear-completed-btn + Element Should Be Visible id=confirm-modal + Element Text Should Contain id=confirm-modal clear + Click id=confirm-cancel + Element Should Be Hidden id=confirm-modal + END + +Clear Failed Confirmation + [Documentation] Click clear failed and verify confirmation modal appears. + ${disabled}= Get Attribute id=clear-failed-btn disabled + IF '${disabled}' != 'true' + Click id=clear-failed-btn + Element Should Be Visible id=confirm-modal + Element Text Should Contain id=confirm-modal clear + Click id=confirm-cancel + Element Should Be Hidden id=confirm-modal + END + +# --------------------------------------------------------------------------- +# Retry All +# --------------------------------------------------------------------------- +Retry All Failed + [Documentation] Click retry all and verify UI feedback. + ${disabled}= Get Attribute id=retry-all-btn disabled + IF '${disabled}' != 'true' + Click id=retry-all-btn + Toast Should Contain retry + END + +# --------------------------------------------------------------------------- +# Navigation +# --------------------------------------------------------------------------- +Navigation Back To Main + [Documentation] Click the back button and verify redirect to main page. + Click text=Back to Main + Wait For Elements State id=search-input visible timeout=5s + Page Title Should Contain AniWorld diff --git a/tests/robot/ui/responsive.robot b/tests/robot/ui/responsive.robot new file mode 100644 index 0000000..07a096c --- /dev/null +++ b/tests/robot/ui/responsive.robot @@ -0,0 +1,86 @@ +*** Settings *** +Documentation Responsive design UI tests for Aniworld. +... Covers mobile and tablet viewport layouts and touch interactions. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Open Browser To Page /login +... AND Perform Login +... AND Wait For Elements State id=search-input visible timeout=5s + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Mobile Viewport +# --------------------------------------------------------------------------- +Mobile Viewport Layout + [Documentation] Resize to mobile and verify layout adapts. + Set Mobile Viewport + Reload Page + Wait For Elements State id=search-input visible timeout=5s + ${width}= Evaluate window.innerWidth + Should Be True ${width} <= 480 + Element Should Be Visible id=search-input + Element Should Be Visible id=theme-toggle + +Mobile Menu Accessible + [Documentation] Verify hamburger menu or compact header on mobile. + Set Mobile Viewport + Reload Page + Wait For Elements State id=search-input visible timeout=5s + # On mobile, header actions may collapse into a menu + ${menu_visible}= Run Keyword And Return Status + ... Wait For Elements State id=mobile-menu-btn visible timeout=2s + IF ${menu_visible} + Click id=mobile-menu-btn + Element Should Be Visible id=mobile-menu + END + +# --------------------------------------------------------------------------- +# Tablet Viewport +# --------------------------------------------------------------------------- +Tablet Viewport Layout + [Documentation] Resize to tablet and verify layout adapts. + Set Tablet Viewport + Reload Page + Wait For Elements State id=search-input visible timeout=5s + ${width}= Evaluate window.innerWidth + Should Be True ${width} >= 768 + Element Should Be Visible id=search-input + Element Should Be Visible id=rescan-btn + +# --------------------------------------------------------------------------- +# Touch Interactions +# --------------------------------------------------------------------------- +Touch Buttons Clickable + [Documentation] Verify buttons remain clickable on touch-sized viewport. + Set Mobile Viewport + Reload Page + Wait For Elements State id=search-input visible timeout=5s + # Try clicking theme toggle + Click id=theme-toggle + ${theme}= Get Current Theme + Should Be Equal As Strings ${theme} dark + # Toggle back + Click id=theme-toggle + ${theme}= Get Current Theme + Should Be Equal As Strings ${theme} light + +# --------------------------------------------------------------------------- +# Desktop Viewport Reset +# --------------------------------------------------------------------------- +Desktop Viewport Reset + [Documentation] Resize back to desktop and verify normal layout. + Set Desktop Viewport + Reload Page + Wait For Elements State id=search-input visible timeout=5s + ${width}= Evaluate window.innerWidth + Should Be True ${width} >= 1280 + Element Should Be Visible id=search-input + Element Should Be Visible id=rescan-btn + Element Should Be Visible id=config-btn diff --git a/tests/robot/ui/settings_modal.robot b/tests/robot/ui/settings_modal.robot new file mode 100644 index 0000000..c2e25ed --- /dev/null +++ b/tests/robot/ui/settings_modal.robot @@ -0,0 +1,164 @@ +*** Settings *** +Documentation Settings modal UI tests for Aniworld. +... Covers open/close, edit all config sections, save, backup/restore, and export/import. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Create Anonymous Session +... AND Setup Master Password +... AND Open Browser To Page /login +... AND Perform Login +... AND Wait For Elements State id=search-input visible timeout=5s + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Modal Open/Close +# --------------------------------------------------------------------------- +Open Settings Modal + [Documentation] Click the config button and verify the modal opens. + Open Settings Modal + Element Should Be Visible id=config-modal + +Close Settings Modal Via Button + [Documentation] Click the close button and verify the modal closes. + Open Settings Modal + Close Settings Modal + Element Should Be Hidden id=config-modal + +Close Settings Modal Via Overlay + [Documentation] Click the modal overlay and verify the modal closes. + Open Settings Modal + Click id=config-modal .modal-overlay + Wait For Elements State id=config-modal hidden timeout=3s + Element Should Be Hidden id=config-modal + +Close Settings Modal Via Escape + [Documentation] Press Escape and verify the modal closes. + Open Settings Modal + Press Keys id=config-modal Escape + Wait For Elements State id=config-modal hidden timeout=3s + Element Should Be Hidden id=config-modal + +# --------------------------------------------------------------------------- +# General Settings +# --------------------------------------------------------------------------- +Edit General Settings + [Documentation] Change app name, data dir, and anime directory. + Open Settings Modal + Fill General Settings UpdatedApp data /tmp/updated_anime + Save Settings + Toast Should Contain saved + +# --------------------------------------------------------------------------- +# Scheduler Settings +# --------------------------------------------------------------------------- +Edit Scheduler Settings + [Documentation] Toggle scheduler enabled and change time/days. + Open Settings Modal + Check Checkbox id=scheduled-rescan-enabled + Fill Text id=scheduler-time-input 04:30 + # Select days (simplified - actual implementation depends on UI widgets) + Save Settings + Toast Should Contain saved + +Disable Scheduler + [Documentation] Uncheck scheduler enabled and save. + Open Settings Modal + Uncheck Checkbox id=scheduled-rescan-enabled + Save Settings + Toast Should Contain saved + +# --------------------------------------------------------------------------- +# Logging Settings +# --------------------------------------------------------------------------- +Edit Logging Settings + [Documentation] Change log level, file path, max bytes, and backup count. + Open Settings Modal + Select Options By id=logging-level-select text DEBUG + Fill Text id=logging-file-input logs/test.log + Fill Text id=logging-max-bytes-input 1048576 + Fill Text id=logging-backup-count-input 5 + Save Settings + Toast Should Contain saved + +# --------------------------------------------------------------------------- +# Backup Settings +# --------------------------------------------------------------------------- +Edit Backup Settings + [Documentation] Toggle backup enabled and change path/retention. + Open Settings Modal + Check Checkbox id=backup-enabled + Fill Text id=backup-path-input /tmp/aniworld_backups + Fill Text id=backup-keep-days-input 14 + Save Settings + Toast Should Contain saved + +# --------------------------------------------------------------------------- +# NFO Settings +# --------------------------------------------------------------------------- +Edit NFO Settings + [Documentation] Enter TMDB key and toggle auto-create/media downloads. + Open Settings Modal + Fill Text id=tmdb-api-key-input test_tmdb_key_123 + Check Checkbox id=nfo-auto-create + Check Checkbox id=nfo-download-poster + Check Checkbox id=nfo-download-logo + Check Checkbox id=nfo-download-fanart + Save Settings + Toast Should Contain saved + +# --------------------------------------------------------------------------- +# Backup Management +# --------------------------------------------------------------------------- +Create Config Backup + [Documentation] Click create backup and verify success notification. + Open Settings Modal + Click id=create-backup-btn + Toast Should Contain backup + +Restore Config Backup + [Documentation] Select a backup and click restore. + Open Settings Modal + Click id=create-backup-btn + Toast Should Contain backup + # Select first backup in list + ${count}= Get Element Count .backup-item + IF ${count} > 0 + Click .backup-item >> nth=0 + Click id=restore-backup-btn + Toast Should Contain restore + END + +Delete Config Backup + [Documentation] Select a backup and click delete. + Open Settings Modal + Click id=create-backup-btn + Toast Should Contain backup + ${count}= Get Element Count .backup-item + IF ${count} > 0 + Click .backup-item >> nth=0 + Click id=delete-backup-btn + Toast Should Contain delete + END + +# --------------------------------------------------------------------------- +# Export / Import +# --------------------------------------------------------------------------- +Export Config + [Documentation] Click export config and verify download starts. + Open Settings Modal + ${dl_promise}= Promise To Wait For Event download + Click id=export-config-btn + ${download}= Wait For ${dl_promise} + Should Not Be Empty ${download} + +Import Config + [Documentation] Upload a config file and verify it is applied. + Open Settings Modal + Upload File By Selector id=import-config-input ${CURDIR}/../fixtures/sample_config.json + Click id=import-config-btn + Toast Should Contain import diff --git a/tests/robot/ui/setup_flow.robot b/tests/robot/ui/setup_flow.robot new file mode 100644 index 0000000..4991d46 --- /dev/null +++ b/tests/robot/ui/setup_flow.robot @@ -0,0 +1,95 @@ +*** Settings *** +Documentation Setup flow UI tests for Aniworld. +... Covers setup page load, password strength, form validation, and complete setup submission. + +Resource ${CURDIR}/../resources/common.resource +Resource ${CURDIR}/../resources/ui_keywords.resource + +Test Setup Run Keywords +... Open Browser To Page /setup +... AND Wait For Elements State id=setup-form visible timeout=5s + +Test Teardown Close Browser + +*** Test Cases *** +# --------------------------------------------------------------------------- +# Setup Page Load +# --------------------------------------------------------------------------- +Setup Page Loads + [Documentation] Verify the setup page loads with all expected sections. + Page Title Should Contain Setup + Element Should Be Visible id=setup-form + Element Should Be Visible id=general-section + Element Should Be Visible id=security-section + Element Should Be Visible id=scheduler-section + Element Should Be Visible id=logging-section + Element Should Be Visible id=backup-section + Element Should Be Visible id=nfo-section + +# --------------------------------------------------------------------------- +# Password Strength Indicator +# --------------------------------------------------------------------------- +Password Strength Weak + [Documentation] Type a weak password and verify the strength indicator shows weak. + Fill Text id=master-password-input weak + ${strength}= Get Text id=password-strength + Should Contain ${strength} weak + +Password Strength Medium + [Documentation] Type a medium-strength password and verify the indicator updates. + Fill Text id=master-password-input Medium1! + ${strength}= Get Text id=password-strength + Should Contain ${strength} medium + +Password Strength Strong + [Documentation] Type a strong password and verify the indicator shows strong. + Fill Text id=master-password-input ${SETUP_PASSWORD} + ${strength}= Get Text id=password-strength + Should Contain ${strength} strong + +# --------------------------------------------------------------------------- +# Form Validation +# --------------------------------------------------------------------------- +Setup Form Validation Empty Password + [Documentation] Submit the form with an empty password and verify error message. + Fill Text id=app-name-input TestApp + Fill Text id=anime-directory-input /tmp/test + Click id=setup-submit-btn + Element Should Be Visible id=password-error + Element Text Should Contain id=password-error required + +Setup Form Validation Mismatched Passwords + [Documentation] Submit with mismatched password and confirmation. + Fill Text id=master-password-input ${SETUP_PASSWORD} + Fill Text id=confirm-password-input DifferentPass123! + Click id=setup-submit-btn + Element Should Be Visible id=confirm-password-error + Element Text Should Contain id=confirm-password-error match + +# --------------------------------------------------------------------------- +# Complete Setup Flow +# --------------------------------------------------------------------------- +Complete Setup Flow + [Documentation] Fill all setup sections and submit, verifying redirect to main page. + Fill Text id=app-name-input AniworldTest + Fill Text id=data-dir-input data + Fill Text id=anime-directory-input /tmp/aniworld_test_anime + Fill Text id=master-password-input ${SETUP_PASSWORD} + Fill Text id=confirm-password-input ${SETUP_PASSWORD} + # Disable scheduler for tests + Uncheck Checkbox id=scheduler-enabled + # Submit + Click id=setup-submit-btn + Wait For Elements State id=search-input visible timeout=10s + Page Title Should Contain AniWorld + +# --------------------------------------------------------------------------- +# Post-Setup Redirect +# --------------------------------------------------------------------------- +Setup Redirects When Already Configured + [Documentation] After setup is complete, visiting /setup should redirect to /. + Complete Setup Flow + Go To ${BASE_URL}/setup + Wait For Elements State id=search-input visible timeout=5s + ${url}= Get Url + Should Not Contain ${url} /setup