diff --git a/package-lock.json b/package-lock.json index 035701f..e851baa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3438,16 +3438,16 @@ } }, "node_modules/@lezer/common": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz", - "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", "dev": true, "license": "MIT" }, "node_modules/@lezer/lr": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.7.tgz", - "integrity": "sha512-wNIFWdSUfX9Jc6ePMzxSPVgTVB4EOfDIwLQLWASyiUdHKaMsiilj9bYiGkGQCKVodd0x6bgQCV207PILGFCF9Q==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", "dev": true, "license": "MIT", "dependencies": { @@ -4060,22 +4060,22 @@ } }, "node_modules/@parcel/bundler-default": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.16.3.tgz", - "integrity": "sha512-zCW2KzMfcEXqpVSU+MbLFMV3mHIzm/7UK1kT8mceuj4UwUScw7Lmjmulc2Ev4hcnwnaAFyaVkyFE5JXA4GKsLQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.16.4.tgz", + "integrity": "sha512-Nb8peNvhfm1+660CLwssWh4weY+Mv6vEGS6GPKqzJmTMw50udi0eS1YuWFzvmhSiu1KsYcUD37mqQ1LuIDtWoA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/graph": "3.6.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/graph": "3.6.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4083,15 +4083,15 @@ } }, "node_modules/@parcel/cache": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.16.3.tgz", - "integrity": "sha512-iWlbdTk9h7yTG1fxpGvftUD7rVbXVQn1+U21BGqFyYxfrd+wgdN624daIG6+eqI6yBuaBTEwH+cb3kaI9sH1ng==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.16.4.tgz", + "integrity": "sha512-+uCyeElSga2MBbmbXpIj/WVKH7TByCrKaxtHbelfKKIJpYMgEHVjO4cuc7GUfTrUAmRUS8ZGvnX7Etgq6/jQhw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/fs": "2.16.3", - "@parcel/logger": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/fs": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/utils": "2.16.4", "lmdb": "2.8.5" }, "engines": { @@ -4102,13 +4102,13 @@ "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/codeframe": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.16.3.tgz", - "integrity": "sha512-oXZx8PUqExnXnAHCLhxulTDeFvTBqPAwJU4AVZwnYFToaQ6nltXWWYaDGUu2f/V3Z17LObWiOROHT7HYXAe62Q==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.16.4.tgz", + "integrity": "sha512-s64aMfOJoPrXhKH+Y98ahX0O8aXWvTR+uNlOaX4yFkpr4FFDnviLcGngDe/Yo4Qq2FJZ0P6dNswbJTUH9EGxkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4123,17 +4123,17 @@ } }, "node_modules/@parcel/compressor-raw": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.16.3.tgz", - "integrity": "sha512-84lI0ULxvjnqDn3yHorMHj2X2g0oQsIwNFYopQWz9UWjnF7g5IU0EFgAAqMCQxKKUV6fttqaQiDDPikXLR6hHA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.16.4.tgz", + "integrity": "sha512-IK8IpNhw61B2HKgA1JhGhO9y+ZJFRZNTEmvhN1NdLdPqvgEXm2EunT+m6D9z7xeoeT6XnUKqM0eRckEdD0OXbA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3" + "@parcel/plugin": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4141,76 +4141,76 @@ } }, "node_modules/@parcel/config-default": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.16.3.tgz", - "integrity": "sha512-OgB6f+EpCzjeFLoVB5qJzKy0ybB2wPK0hB2aXgD3oYCHWLny7LJOGaktY9OskSn1jfz7Tdit9zLNXOhBTMRujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@parcel/bundler-default": "2.16.3", - "@parcel/compressor-raw": "2.16.3", - "@parcel/namer-default": "2.16.3", - "@parcel/optimizer-css": "2.16.3", - "@parcel/optimizer-html": "2.16.3", - "@parcel/optimizer-image": "2.16.3", - "@parcel/optimizer-svg": "2.16.3", - "@parcel/optimizer-swc": "2.16.3", - "@parcel/packager-css": "2.16.3", - "@parcel/packager-html": "2.16.3", - "@parcel/packager-js": "2.16.3", - "@parcel/packager-raw": "2.16.3", - "@parcel/packager-svg": "2.16.3", - "@parcel/packager-wasm": "2.16.3", - "@parcel/reporter-dev-server": "2.16.3", - "@parcel/resolver-default": "2.16.3", - "@parcel/runtime-browser-hmr": "2.16.3", - "@parcel/runtime-js": "2.16.3", - "@parcel/runtime-rsc": "2.16.3", - "@parcel/runtime-service-worker": "2.16.3", - "@parcel/transformer-babel": "2.16.3", - "@parcel/transformer-css": "2.16.3", - "@parcel/transformer-html": "2.16.3", - "@parcel/transformer-image": "2.16.3", - "@parcel/transformer-js": "2.16.3", - "@parcel/transformer-json": "2.16.3", - "@parcel/transformer-node": "2.16.3", - "@parcel/transformer-postcss": "2.16.3", - "@parcel/transformer-posthtml": "2.16.3", - "@parcel/transformer-raw": "2.16.3", - "@parcel/transformer-react-refresh-wrap": "2.16.3", - "@parcel/transformer-svg": "2.16.3" + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.16.4.tgz", + "integrity": "sha512-kBxuTY/5trEVnvXk92l7LVkYjNuz3SaqWymFhPjEnc8GY4ZVdcWrWdXWTB9hUhpmRYJctFCyGvM0nN05JTiM2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/bundler-default": "2.16.4", + "@parcel/compressor-raw": "2.16.4", + "@parcel/namer-default": "2.16.4", + "@parcel/optimizer-css": "2.16.4", + "@parcel/optimizer-html": "2.16.4", + "@parcel/optimizer-image": "2.16.4", + "@parcel/optimizer-svg": "2.16.4", + "@parcel/optimizer-swc": "2.16.4", + "@parcel/packager-css": "2.16.4", + "@parcel/packager-html": "2.16.4", + "@parcel/packager-js": "2.16.4", + "@parcel/packager-raw": "2.16.4", + "@parcel/packager-svg": "2.16.4", + "@parcel/packager-wasm": "2.16.4", + "@parcel/reporter-dev-server": "2.16.4", + "@parcel/resolver-default": "2.16.4", + "@parcel/runtime-browser-hmr": "2.16.4", + "@parcel/runtime-js": "2.16.4", + "@parcel/runtime-rsc": "2.16.4", + "@parcel/runtime-service-worker": "2.16.4", + "@parcel/transformer-babel": "2.16.4", + "@parcel/transformer-css": "2.16.4", + "@parcel/transformer-html": "2.16.4", + "@parcel/transformer-image": "2.16.4", + "@parcel/transformer-js": "2.16.4", + "@parcel/transformer-json": "2.16.4", + "@parcel/transformer-node": "2.16.4", + "@parcel/transformer-postcss": "2.16.4", + "@parcel/transformer-posthtml": "2.16.4", + "@parcel/transformer-raw": "2.16.4", + "@parcel/transformer-react-refresh-wrap": "2.16.4", + "@parcel/transformer-svg": "2.16.4" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/core": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.16.3.tgz", - "integrity": "sha512-b9ll4jaFYfXSv6NZAOJ2P0uuyT/Doel7ho2AHLSUz2thtcL6HEb2+qdV2f9wriVvbEoPAj9VuSOgNc0t0f5iMw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.16.4.tgz", + "integrity": "sha512-a0CgrW5A5kwuSu5J1RFRoMQaMs9yagvfH2jJMYVw56+/7NRI4KOtu612SG9Y1ERWfY55ZwzyFxtLWvD6LO+Anw==", "dev": true, "license": "MIT", "dependencies": { "@mischnic/json-sourcemap": "^0.1.1", - "@parcel/cache": "2.16.3", - "@parcel/diagnostic": "2.16.3", - "@parcel/events": "2.16.3", - "@parcel/feature-flags": "2.16.3", - "@parcel/fs": "2.16.3", - "@parcel/graph": "3.6.3", - "@parcel/logger": "2.16.3", - "@parcel/package-manager": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/profiler": "2.16.3", - "@parcel/rust": "2.16.3", + "@parcel/cache": "2.16.4", + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4", + "@parcel/feature-flags": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/graph": "3.6.4", + "@parcel/logger": "2.16.4", + "@parcel/package-manager": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/profiler": "2.16.4", + "@parcel/rust": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/types": "2.16.3", - "@parcel/utils": "2.16.3", - "@parcel/workers": "2.16.3", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", "base-x": "^3.0.11", "browserslist": "^4.24.5", "clone": "^2.1.2", @@ -4230,9 +4230,9 @@ } }, "node_modules/@parcel/core/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4243,9 +4243,9 @@ } }, "node_modules/@parcel/diagnostic": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.16.3.tgz", - "integrity": "sha512-NBoGGFMqOmbs8i0zGVwTeU0alQ0BkEZe894zAb5jEBQqsRBPmdqogwmARsT4Ix2bN1QBco4o0gn9kBtalFC6IQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.16.4.tgz", + "integrity": "sha512-YN5CfX7lFd6yRLxyZT4Sj3sR6t7nnve4TdXSIqapXzQwL7Bw+sj79D95wTq2rCm3mzk5SofGxFAXul2/nG6gcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4261,9 +4261,9 @@ } }, "node_modules/@parcel/error-overlay": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/error-overlay/-/error-overlay-2.16.3.tgz", - "integrity": "sha512-JqJR4Fl5SwTmqDEuCAC8F1LmNLWpjfiJ+hGp3CoLb0/9EElRxlpkuP/SxTe2/hyXevpfn3bfvS1cn/mWhHUc3w==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/error-overlay/-/error-overlay-2.16.4.tgz", + "integrity": "sha512-e8KYKnMsfmQnqIhsUWBUZAXlDK30wkxsAGle1tZ0gOdoplaIdVq/WjGPatHLf6igLM76c3tRn2vw8jZFput0jw==", "dev": true, "license": "MIT", "engines": { @@ -4275,9 +4275,9 @@ } }, "node_modules/@parcel/events": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.16.3.tgz", - "integrity": "sha512-rAh/yXwtHYcKWmi9Tjjf5t95UdBVhhlyJkIYN25/PYKdSRBcQ9c1rd8/fvOeZKy1/fSiOcEXqm6dK7bhLSCaww==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.16.4.tgz", + "integrity": "sha512-slWQkBRAA7o0cN0BLEd+yCckPmlVRVhBZn5Pn6ktm4EzEtrqoMzMeJOxxH8TXaRzrQDYnTcnYIHFgXWd4kkUfg==", "dev": true, "license": "MIT", "engines": { @@ -4289,9 +4289,9 @@ } }, "node_modules/@parcel/feature-flags": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.16.3.tgz", - "integrity": "sha512-D15/cM/mAO8yv0NQ9kFBxXZ7C3A+jAq+9tVfrjYegofMk18pQoXJz6X/po2Kq1PzO7pjydn7PqYMB/O9p/+zbQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.16.4.tgz", + "integrity": "sha512-nYdx53siKPLYikHHxfzgjzzgxdrjquK6DMnuSgOTyIdRG4VHdEN0+NqKijRLuVgiUFo/dtxc2h+amwqFENMw8w==", "dev": true, "license": "MIT", "engines": { @@ -4303,18 +4303,18 @@ } }, "node_modules/@parcel/fs": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.16.3.tgz", - "integrity": "sha512-InMXHVIfDUSimjBoGJcdNlNjoIsDQ8MUDN8UJG4jnjJQ6DDor+W+yg4sw/40tToUqIyi99lVhQlpkBA+nHLpOQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.16.4.tgz", + "integrity": "sha512-maCMOiVn7oJYZlqlfxgLne8n6tSktIT1k0AeyBp4UGWCXyeJUJ+nL7QYShFpKNLtMLeF0cEtgwRAknWzbcDS1g==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/feature-flags": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/types-internal": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/feature-flags": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/types-internal": "2.16.4", + "@parcel/utils": "2.16.4", "@parcel/watcher": "^2.0.7", - "@parcel/workers": "2.16.3" + "@parcel/workers": "2.16.4" }, "engines": { "node": ">= 16.0.0" @@ -4324,17 +4324,17 @@ "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/graph": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.6.3.tgz", - "integrity": "sha512-3qV99HCHrPR1CnMOHkwwpmPBimVMd3d/GcEcgOHUKi+2mS0KZ4TwMs/THaIWtJx7q5jrhqEht+IyQ1Smupo49g==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.6.4.tgz", + "integrity": "sha512-Cj9yV+/k88kFhE+D+gz0YuNRpvNOCVDskO9pFqkcQhGbsGq6kg2XpZ9V7HlYraih31xf8Vb589bZOwjKIiHixQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/feature-flags": "2.16.3", + "@parcel/feature-flags": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { @@ -4346,14 +4346,14 @@ } }, "node_modules/@parcel/logger": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.16.3.tgz", - "integrity": "sha512-dHUJk8dvo2wOg3dIqSjNGqlVqsRn4hTZVbgTShaImaLTWdueaKfMojxo79P7T3em49y0dQb0m+xl2SunDhtwsA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.16.4.tgz", + "integrity": "sha512-QR8QLlKo7xAy9JBpPDAh0RvluaixqPCeyY7Fvo2K7hrU3r85vBNNi06pHiPbWoDmB4x1+QoFwMaGnJOHR+/fMA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/events": "2.16.3" + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4" }, "engines": { "node": ">= 16.0.0" @@ -4364,9 +4364,9 @@ } }, "node_modules/@parcel/markdown-ansi": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.16.3.tgz", - "integrity": "sha512-r0QQpS44jNueY8lcZcSoUua3kJfI5kDZrJvFgi1jrkyxwDUfq3L0xWQjxHrXzv8K6uFAeU+teoq8JcWLVLXa1w==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.16.4.tgz", + "integrity": "sha512-0+oQApAVF3wMcQ6d1ZfZ0JsRzaMUYj9e4U+naj6YEsFsFGOPp+pQYKXBf1bobQeeB7cPKPT3SUHxFqced722Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -4381,19 +4381,19 @@ } }, "node_modules/@parcel/namer-default": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.16.3.tgz", - "integrity": "sha512-4MwRm8ZnloMdQ6sAMrTDxMiPVN1fV+UcBIrA0Fpp4kD3XLkqSAUCLnjl13+VrPelfh01irM6QnpK4JTKBqRk0A==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.16.4.tgz", + "integrity": "sha512-CE+0lFg881sJq575EXxj2lKUn81tsS5itpNUUErHxit195m3PExyAhoXM6ed/SXxwi+uv+T5FS/jjDLBNuUFDA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4401,17 +4401,17 @@ } }, "node_modules/@parcel/node-resolver-core": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.7.3.tgz", - "integrity": "sha512-0xdXyhGcGwtYmfWwEwzdVVGnTaADdTScx1S8IXiK0Nh3S1b4ilGqnKzw8fVsJCsBMvQA5e251EDFeG3qTnUsnw==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.7.4.tgz", + "integrity": "sha512-b3VDG+um6IWW5CTod6M9hQsTX5mdIelKmam7mzxzgqg4j5hnycgTWqPMc9UxhYoUY/Q/PHfWepccNcKtvP5JiA==", "dev": true, "license": "MIT", "dependencies": { "@mischnic/json-sourcemap": "^0.1.1", - "@parcel/diagnostic": "2.16.3", - "@parcel/fs": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", "nullthrows": "^1.1.1", "semver": "^7.7.1" }, @@ -4424,9 +4424,9 @@ } }, "node_modules/@parcel/node-resolver-core/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4437,23 +4437,23 @@ } }, "node_modules/@parcel/optimizer-css": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.16.3.tgz", - "integrity": "sha512-j/o9bGtu1Fe7gJYQD+/SeJ5yR7FmS6Z7e6CtTkVxjeeq0/IdR0KoZOCkJ4cRETPnm+wkyQVlY8koAAFbEEqV8w==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.16.4.tgz", + "integrity": "sha512-aqdXCtmvpcXYgJFGk2DtXF34wuM2TD1fZorKMrJdKB9sSkWVRs1tq6RAXQrbi0ZPDH9wfE/9An3YdkTex7RHuQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3", + "@parcel/utils": "2.16.4", "browserslist": "^4.24.5", "lightningcss": "^1.30.1", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4461,19 +4461,19 @@ } }, "node_modules/@parcel/optimizer-html": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-html/-/optimizer-html-2.16.3.tgz", - "integrity": "sha512-EBmjY+QRa/in05wRWiL6B/kQ1ERemdg4W9py+V2w0tJx1n6yOvtjPGvivYtU+s82rlVlx6DN3DFU13iGRt0FuQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-html/-/optimizer-html-2.16.4.tgz", + "integrity": "sha512-vg/R2uuSni+NYYUUV8m+5bz8p5zBv8wc/nNleoBnGuCDwn7uaUwTZ8Gt9CjZO8jjG0xCLILoc/TW+e2FF3pfgQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3" + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4481,44 +4481,44 @@ } }, "node_modules/@parcel/optimizer-image": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.16.3.tgz", - "integrity": "sha512-PbGsDXbbWyOnkpWn3jgZxtAp8l8LNXl7DCv5Q4l1TR6k4sULjmxTTPY6+AkY6H84cAN7s5h6F8k2XeN3ygXWCA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.16.4.tgz", + "integrity": "sha512-2RV54WnvMYr18lxSx7Zlx/DXpJwMzOiPxDnoFyvaUoYutvgHO6chtcgFgh1Bvw/PoI95vYzlTkZ8QfUOk5A0JA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3", - "@parcel/workers": "2.16.3" + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/optimizer-svg": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-svg/-/optimizer-svg-2.16.3.tgz", - "integrity": "sha512-fgQhrqu5pKtEaM9G//PvBZSuCDP6ZVbGyFnePKCzqnXJ173/Y+4kUbNOrPi7wE4HupWMsJRNUf/vyCu+lXdOiQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-svg/-/optimizer-svg-2.16.4.tgz", + "integrity": "sha512-22+BqIffCrVErg8y2XwhasbTaFNn75OKXZ3KTDBIfOSAZKLUKs1iHfDXETzTRN7cVcS+Q36/6EHd7N/RA8i1fg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3" + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4526,22 +4526,22 @@ } }, "node_modules/@parcel/optimizer-swc": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.16.3.tgz", - "integrity": "sha512-8P5Bis2SynQ6sPW1bwB6H8WK+nFF61RCKzlGnTPoh1YE36dubYqUreYYISMLFt/rG8eb+Ja78DQLPZTVP3sfQQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.16.4.tgz", + "integrity": "sha512-+URqwnB6u1gqaLbG1O1DDApH+UVj4WCbK9No1fdxLBxQ9a84jyli25o1kK1hYB9Nb/JMyYNnEBfvYUW6RphOxw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3", + "@parcel/utils": "2.16.4", "@swc/core": "^1.11.24", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4549,19 +4549,19 @@ } }, "node_modules/@parcel/package-manager": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.16.3.tgz", - "integrity": "sha512-TySTY93SyGfu8E5YWiekumw6sm/2+LBHcpv1JWWAfNd+1b/x3WB5QcRyEk6mpnOo7ChQOfqykzUaBcrmLBGaSw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.16.4.tgz", + "integrity": "sha512-obWv9gZgdnkT3Kd+fBkKjhdNEY7zfOP5gVaox5i4nQstVCaVnDlMv5FwLEXwehL+WbwEcGyEGGxOHHkAFKk7Cg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/fs": "2.16.3", - "@parcel/logger": "2.16.3", - "@parcel/node-resolver-core": "3.7.3", - "@parcel/types": "2.16.3", - "@parcel/utils": "2.16.3", - "@parcel/workers": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/node-resolver-core": "3.7.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", "@swc/core": "^1.11.24", "semver": "^7.7.1" }, @@ -4573,13 +4573,13 @@ "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/package-manager/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4590,22 +4590,22 @@ } }, "node_modules/@parcel/packager-css": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.16.3.tgz", - "integrity": "sha512-CUwMRif1ZGBfociDt6m18L7sgafsquo0+NYRDXCTHmig3w7zm5saE4PXborfzRI/Lj3kBUkJYH//NQGITHv1Yg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.16.4.tgz", + "integrity": "sha512-rWRtfiX+VVIOZvq64jpeNUKkvWAbnokfHQsk/js1s5jD4ViNQgPcNLiRaiIANjymqL6+dQqWvGUSW2a5FAZYfg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3", + "@parcel/utils": "2.16.4", "lightningcss": "^1.30.1", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4613,20 +4613,20 @@ } }, "node_modules/@parcel/packager-html": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.16.3.tgz", - "integrity": "sha512-hluJXpvcW2EwmBxO/SalBiX5SIYJ7jGTkhFq5ka2wrQewFxaAOv2BVTuFjl1AAnWzjigcNhC4n0jkQUckCNW4g==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.16.4.tgz", + "integrity": "sha512-AWo5f6SSqBsg2uWOsX0gPX8hCx2iE6GYLg2Z4/cDy2mPlwDICN8/bxItEztSZFmObi+ti26eetBKRDxAUivyIQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/types": "2.16.3", - "@parcel/utils": "2.16.3" + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4634,24 +4634,24 @@ } }, "node_modules/@parcel/packager-js": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.16.3.tgz", - "integrity": "sha512-01fufzVOs9reEDq9OTUyu5Kpasd8nGvBJEUytagM6rvNlEpmlUX5HvoAzUMSTyYeFSH+1VnX6HzK6EcQNY9Y8Q==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.16.4.tgz", + "integrity": "sha512-L2o39f/fhta+hxto7w8OTUKdstY+te5BmHZREckbQm0KTBg93BG7jB0bfoxLSZF0d8uuAYIVXjzeHNqha+du1g==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/types": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", "globals": "^13.24.0", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4659,17 +4659,17 @@ } }, "node_modules/@parcel/packager-raw": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.16.3.tgz", - "integrity": "sha512-GCehb36D2xe8P8gftyZcjNr3XcUzBgRzWcasM4I0oPaLRZw4nuIu60cwTsGk6/HhUYDq8uPze+gr1L4pApRrjw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.16.4.tgz", + "integrity": "sha512-A9j60G9OmbTkEeE4WRMXCiErEprHLs9NkUlC4HXCxmSrPMOVaMaMva2LdejE3A9kujZqYtYfuc8+a+jN+Nro4w==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3" + "@parcel/plugin": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4677,20 +4677,20 @@ } }, "node_modules/@parcel/packager-svg": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.16.3.tgz", - "integrity": "sha512-1TLmU8zcRBySOD3WXGUhTjmIurJoOMwQ3aIiyHXn4zjrl4+VPw/WnUoVGpMwUW1T7rb2/22BKPGAAxbOLDqxLQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.16.4.tgz", + "integrity": "sha512-LT9l7eInFrAZJ6w3mYzAUgDq3SIzYbbQyW46Dz26M9lJQbf6uCaATUTac3BEHegW0ikDuw4OOGHK41BVqeeusg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/types": "2.16.3", - "@parcel/utils": "2.16.3" + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4698,17 +4698,17 @@ } }, "node_modules/@parcel/packager-wasm": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.16.3.tgz", - "integrity": "sha512-RfRM/RaA4eWV+qUt7A9Vo2VlvZx50Rfs81kZ4WBhxzey2BGAvBSJWceYEUnI7JuDmrHjDMDe6y0+gLNmELeL1g==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.16.4.tgz", + "integrity": "sha512-AY96Aqu/RpmaSZK2RGkIrZWjAperDw8DAlxLAiaP1D/RPVnikZtl5BmcUt/Wz3PrzG7/q9ZVqqKkWsLmhkjXZQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3" + "@parcel/plugin": "2.16.4" }, "engines": { "node": ">=16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4716,13 +4716,13 @@ } }, "node_modules/@parcel/plugin": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.16.3.tgz", - "integrity": "sha512-w4adN/E2MBbNzUwuGWcUkilrf7B6eQThPRdgiw2awIY0/t0C1gN/hhBfUeWt7vt0WcvWlXcyR/OGzU/r0nPteA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.16.4.tgz", + "integrity": "sha512-aN2VQoRGC1eB41ZCDbPR/Sp0yKOxe31oemzPx1nJzOuebK2Q6FxSrJ9Bjj9j/YCaLzDtPwelsuLOazzVpXJ6qg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/types": "2.16.3" + "@parcel/types": "2.16.4" }, "engines": { "node": ">= 16.0.0" @@ -4733,15 +4733,15 @@ } }, "node_modules/@parcel/profiler": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.16.3.tgz", - "integrity": "sha512-/4cVsLfv36fdphm+JiReeXXT3RD6258L79C2kjpD06i84sxyNPQVbFldgWRppbHW2KBR/D6XhIzHcwoDUYtTbw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.16.4.tgz", + "integrity": "sha512-R3JhfcnoReTv2sVFHPR2xKZvs3d3IRrBl9sWmAftbIJFwT4rU70/W7IdwfaJVkD/6PzHq9mcgOh1WKL4KAxPdA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/events": "2.16.3", - "@parcel/types-internal": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4", + "@parcel/types-internal": "2.16.4", "chrome-trace-event": "^1.0.2" }, "engines": { @@ -4753,21 +4753,21 @@ } }, "node_modules/@parcel/reporter-cli": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.16.3.tgz", - "integrity": "sha512-kIwhJy97xlgvNsUhn3efp6PxUfWCiiPG9ciDnAGBXpFmKWl63WQR6QIXNuNgrQremUTzIHJ02h6/+LyBJD4wjw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.16.4.tgz", + "integrity": "sha512-DQx9TwcTZrDv828+tcwEi//xyW7OHTGzGX1+UEVxPp0mSzuOmDn0zfER8qNIqGr1i4D/FXhb5UJQDhGHV8mOpQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/types": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/plugin": "2.16.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", "chalk": "^4.1.2", "term-size": "^2.2.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4775,20 +4775,20 @@ } }, "node_modules/@parcel/reporter-dev-server": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.16.3.tgz", - "integrity": "sha512-c2YEHU3ePOSUO+JXoehn3r0ruUlP2i4xvHfwHLHI3NW/Ymlp4Gy9rWyyYve/zStfoEOyMN/vKRWKtxr6nCy9DQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.16.4.tgz", + "integrity": "sha512-YWvay25htQDifpDRJ0+yFh6xUxKnbfeJxYkPYyuXdxpEUhq4T0UWW0PbPCN/wFX7StgeUTXq5Poeo/+eys9m3w==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/codeframe": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/codeframe": "2.16.4", + "@parcel/plugin": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3" + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4796,20 +4796,20 @@ } }, "node_modules/@parcel/reporter-tracer": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.16.3.tgz", - "integrity": "sha512-DqQQRQC6JKQcYo8fAC69JGri++WC9cTRZFH2QJdbcMXnmeCW0YjBwHsl65C0Q/8aO6lwVlV0P1waMPW3iQw+uA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.16.4.tgz", + "integrity": "sha512-JKnlXpPepak0/ZybmZn9JtyjJiDBWYrt7ZUlXQhQb0xzNcd/k+RqfwVkTKIwyFHsWtym0cwibkvsi2bWFzS7tw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", "chrome-trace-event": "^1.0.3", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4817,18 +4817,18 @@ } }, "node_modules/@parcel/resolver-default": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.16.3.tgz", - "integrity": "sha512-2bf2VRKt1fZRZbi85SBLrePr4Eid0zXUQMy+MRcFoVZ8MaxsjvWjnlxHW71cWNcRQATUOX/0w0z0Gcf7Kjrh2g==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.16.4.tgz", + "integrity": "sha512-wJe9XQS0hn/t32pntQpJbls3ZL8mGVVhK9L7s7BTmZT9ufnvP2nif1psJz/nbgnP9LF6mLSk43OdMJKpoStsjQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/node-resolver-core": "3.7.3", - "@parcel/plugin": "2.16.3" + "@parcel/node-resolver-core": "3.7.4", + "@parcel/plugin": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4836,18 +4836,18 @@ } }, "node_modules/@parcel/runtime-browser-hmr": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.16.3.tgz", - "integrity": "sha512-dN5Kv6/BLaKAf80zogimvSPZYQRA+h+o3rKQLnxid2FilVRTCjz+FOcuMsT/EqAJXai1mKjrxtqlM9IJ4oSV1A==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.16.4.tgz", + "integrity": "sha512-asx7p3NjUSfibI3bC7+8+jUIGHWVk2Zuq9SjJGCGDt+auT9A4uSGljnsk1BWWPqqZ0WILubq4czSAqm0+wt4cw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3" + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4855,20 +4855,20 @@ } }, "node_modules/@parcel/runtime-js": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.16.3.tgz", - "integrity": "sha512-Xk1G7A0g5Dbm374V8piDbxLRQoQ1JiKIChXzQuiQ755A22JYOSP0yA2djBEuB7KWPwFKDd4f9DFTVDn6VclPaQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.16.4.tgz", + "integrity": "sha512-gUKmsjg+PULQBu2QbX0QKll9tXSqHPO8NrfxHwWb2lz5xDKDos1oV0I7BoMWbHhUHkoToXZrm654oGViujtVUA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4876,20 +4876,20 @@ } }, "node_modules/@parcel/runtime-rsc": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-rsc/-/runtime-rsc-2.16.3.tgz", - "integrity": "sha512-QR+4BjGE2OqLcjh6WfAMrNoM0FubxvJNH9p31yjI4H1ivrvTJECanvVZ6C7QRR/30l+WAYb5USrcYJVMwHi1zg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-rsc/-/runtime-rsc-2.16.4.tgz", + "integrity": "sha512-CHkotYE/cNiUjJmrc5FD9YhlFp1UF5wMNNJmoWaL40eBzsqcaV0sSn5V3bNapwewn3wrMYgdPgvOTHfaZaG73A==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 12.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4897,19 +4897,19 @@ } }, "node_modules/@parcel/runtime-service-worker": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.16.3.tgz", - "integrity": "sha512-O+jhRFNThRAxsHOW6RYcYR6+sA9MxeGTmbVRguFyM12OqzuXRTuuv9x2RDSGP/cgBBCpVuq5JvK8KwS2RB26Gg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.16.4.tgz", + "integrity": "sha512-FT0Q58bf5Re+dq5cL2XHbxqHHFZco6qtRijeVpT3TSPMRPlniMArypSytTeZzVNL7h/hxjWsNu7fRuC0yLB5hA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -4917,9 +4917,9 @@ } }, "node_modules/@parcel/rust": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.16.3.tgz", - "integrity": "sha512-pUsgURnDdlHA9AqvEcm124/9+DB7GM7Mk0qQ9XDNiznl09n8XZ67lf/IIvaMW7y0vQ7FpTzRIrRzAJhGyMRbMw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.16.4.tgz", + "integrity": "sha512-RBMKt9rCdv6jr4vXG6LmHtxzO5TuhQvXo1kSoSIF7fURRZ81D1jzBtLxwLmfxCPsofJNqWwdhy5vIvisX+TLlQ==", "dev": true, "license": "MIT", "engines": { @@ -4930,14 +4930,14 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/rust-darwin-arm64": "2.16.3", - "@parcel/rust-darwin-x64": "2.16.3", - "@parcel/rust-linux-arm-gnueabihf": "2.16.3", - "@parcel/rust-linux-arm64-gnu": "2.16.3", - "@parcel/rust-linux-arm64-musl": "2.16.3", - "@parcel/rust-linux-x64-gnu": "2.16.3", - "@parcel/rust-linux-x64-musl": "2.16.3", - "@parcel/rust-win32-x64-msvc": "2.16.3" + "@parcel/rust-darwin-arm64": "2.16.4", + "@parcel/rust-darwin-x64": "2.16.4", + "@parcel/rust-linux-arm-gnueabihf": "2.16.4", + "@parcel/rust-linux-arm64-gnu": "2.16.4", + "@parcel/rust-linux-arm64-musl": "2.16.4", + "@parcel/rust-linux-x64-gnu": "2.16.4", + "@parcel/rust-linux-x64-musl": "2.16.4", + "@parcel/rust-win32-x64-msvc": "2.16.4" }, "peerDependencies": { "napi-wasm": "^1.1.2" @@ -4949,9 +4949,9 @@ } }, "node_modules/@parcel/rust-darwin-arm64": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-arm64/-/rust-darwin-arm64-2.16.3.tgz", - "integrity": "sha512-9JG19DDNjIpvlI1b8VYIjvCaulftd6/J09/Rj2A8KgREv6EtCDkus8jCsNw7Jacj2HIWg23kxJY3XKcJ9pkiug==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-arm64/-/rust-darwin-arm64-2.16.4.tgz", + "integrity": "sha512-P3Se36H9EO1fOlwXqQNQ+RsVKTGn5ztRSUGbLcT8ba6oOMmU1w7J4R810GgsCbwCuF10TJNUMkuD3Q2Sz15Q3Q==", "cpu": [ "arm64" ], @@ -4970,9 +4970,9 @@ } }, "node_modules/@parcel/rust-darwin-x64": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-x64/-/rust-darwin-x64-2.16.3.tgz", - "integrity": "sha512-9mG6M6SGYiCO9IfD85Bixg5udXoy2IQHCRdBoQmpNej5+FrDW1a3FeDwDzqOFtl9b7axpzPEVb7zp+WK36Rn4w==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-x64/-/rust-darwin-x64-2.16.4.tgz", + "integrity": "sha512-8aNKNyPIx3EthYpmVJevIdHmFsOApXAEYGi3HU69jTxLgSIfyEHDdGE9lEsMvhSrd/SSo4/euAtiV+pqK04wnA==", "cpu": [ "x64" ], @@ -4991,9 +4991,9 @@ } }, "node_modules/@parcel/rust-linux-arm-gnueabihf": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm-gnueabihf/-/rust-linux-arm-gnueabihf-2.16.3.tgz", - "integrity": "sha512-zSA1Dz5JWS28DkEMjEQNmf8qk55dR6rcKtwrw5CMg3Ndt30ugrGtRechsqEpXSYYxcDY1kmZ779LwiTUdkdCrQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm-gnueabihf/-/rust-linux-arm-gnueabihf-2.16.4.tgz", + "integrity": "sha512-QrvqiSHaWRLc0JBHgUHVvDthfWSkA6AFN+ikV1UGENv4j2r/QgvuwJiG0VHrsL6pH5dRqj0vvngHzEgguke9DA==", "cpu": [ "arm" ], @@ -5012,9 +5012,9 @@ } }, "node_modules/@parcel/rust-linux-arm64-gnu": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-gnu/-/rust-linux-arm64-gnu-2.16.3.tgz", - "integrity": "sha512-PvjO0U6qM0JjRCH2eKi3JNKgBVWDBP3VrMEUXJJM8K37ylfLTozK0f7oK2M03voCS1WjKrduRGjJNk8EZrBPow==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-gnu/-/rust-linux-arm64-gnu-2.16.4.tgz", + "integrity": "sha512-f3gBWQHLHRUajNZi3SMmDQiEx54RoRbXtZYQNuBQy7+NolfFcgb1ik3QhkT7xovuTF/LBmaqP3UFy0PxvR/iwQ==", "cpu": [ "arm64" ], @@ -5033,9 +5033,9 @@ } }, "node_modules/@parcel/rust-linux-arm64-musl": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-musl/-/rust-linux-arm64-musl-2.16.3.tgz", - "integrity": "sha512-a4TZB9/Y/y8DQ55XZXh9bNb5yIC9CAoK2YK8g3OytauC8OrHGtIIVlF+E1UCn/FPBFr2dobYOeih/InvLKITpQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-musl/-/rust-linux-arm64-musl-2.16.4.tgz", + "integrity": "sha512-cwml18RNKsBwHyZnrZg4jpecXkWjaY/mCArocWUxkFXjjB97L56QWQM9W86f2/Y3HcFcnIGJwx1SDDKJrV6OIA==", "cpu": [ "arm64" ], @@ -5054,9 +5054,9 @@ } }, "node_modules/@parcel/rust-linux-x64-gnu": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-gnu/-/rust-linux-x64-gnu-2.16.3.tgz", - "integrity": "sha512-6/a/5jDcVwE0xpLSLGI9T2pclgnad0jVFRH/4Gm9yQ5fl2gpYghjg3fcCNeSjJ/aBNFKlOeKLlp/oBSlTtlkoQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-gnu/-/rust-linux-x64-gnu-2.16.4.tgz", + "integrity": "sha512-0xIjQaN8hiG0F9R8coPYidHslDIrbfOS/qFy5GJNbGA3S49h61wZRBMQqa7JFW4+2T8R0J9j0SKHhLXpbLXrIg==", "cpu": [ "x64" ], @@ -5075,9 +5075,9 @@ } }, "node_modules/@parcel/rust-linux-x64-musl": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-musl/-/rust-linux-x64-musl-2.16.3.tgz", - "integrity": "sha512-gTUlFvJBLR3UxNjGs076wVuFZyx+X6G6opJzBFaSG9XqLhLo+VrpqHpjCx+SCwSufDLTVq8rWJbwpvbe2EhRJg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-musl/-/rust-linux-x64-musl-2.16.4.tgz", + "integrity": "sha512-fYn21GIecHK9RoZPKwT9NOwxwl3Gy3RYPR6zvsUi0+hpFo19Ph9EzFXN3lT8Pi5KiwQMCU4rsLb5HoWOBM1FeA==", "cpu": [ "x64" ], @@ -5096,9 +5096,9 @@ } }, "node_modules/@parcel/rust-win32-x64-msvc": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/rust-win32-x64-msvc/-/rust-win32-x64-msvc-2.16.3.tgz", - "integrity": "sha512-/kyr5CL4XFJpMj9CvW8K1NNNqkzyOhxc7ibXhykiPyPiGOwO/ZbqnfDhqVx3JMSjOASeW1e6UlGNjnfTPvFkGQ==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-win32-x64-msvc/-/rust-win32-x64-msvc-2.16.4.tgz", + "integrity": "sha512-TcpWC3I1mJpfP2++018lgvM7UX0P8IrzNxceBTHUKEIDMwmAYrUKAQFiaU0j1Ldqk6yP8SPZD3cvphumsYpJOQ==", "cpu": [ "x64" ], @@ -5130,16 +5130,16 @@ } }, "node_modules/@parcel/transformer-babel": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.16.3.tgz", - "integrity": "sha512-Jsusa2xWlgrmBYmvuC70/SIvcNdYZj3NyQhCxTOARV2scksSKH8iSvNsMKepYiZl6nHRNOmnGOShz9xJqNpUDw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.16.4.tgz", + "integrity": "sha512-CMDUOQYX7+cmeyHxHSFnoPcwvXNL7rRFE+Q06uVFzsYYiVhbwGF/1J5Bx4cW3Froumqla4YTytTsEteJEybkdA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3", + "@parcel/utils": "2.16.4", "browserslist": "^4.24.5", "json5": "^2.2.3", "nullthrows": "^1.1.1", @@ -5147,7 +5147,7 @@ }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5155,9 +5155,9 @@ } }, "node_modules/@parcel/transformer-babel/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5168,23 +5168,23 @@ } }, "node_modules/@parcel/transformer-css": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.16.3.tgz", - "integrity": "sha512-RKGfjvQQVYpd27Ag7QHzBEjqfN/hj6Yf6IlbUdOp06bo+XOXQXe5/n2ulJ1EL9ZjyDOtXbB94A7QzSQmtFGEow==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.16.4.tgz", + "integrity": "sha512-VG/+DbDci2HKe20GFRDs65ZQf5GUFfnmZAa1BhVl/MO+ijT3XC3eoVUy5cExRkq4VLcPY4ytL0g/1T2D6x7lBQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3", + "@parcel/utils": "2.16.4", "browserslist": "^4.24.5", "lightningcss": "^1.30.1", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5192,19 +5192,19 @@ } }, "node_modules/@parcel/transformer-html": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.16.3.tgz", - "integrity": "sha512-j/f+fR3hS9g3Kw4mySyF2sN4mp0t6amq3x52SAptpa4C7w8XVWproc+3ZLgjzi91OPqNeQAQUNQMy86AfuMuEw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.16.4.tgz", + "integrity": "sha512-w6JErYTeNS+KAzUAER18NHFIFFvxiLGd4Fht1UYcb/FDjJdLAMB/FljyEs0Rto/WAhZ2D0MuSL25HQh837R62g==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3" + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5212,38 +5212,38 @@ } }, "node_modules/@parcel/transformer-image": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.16.3.tgz", - "integrity": "sha512-q8BhaGSaGtIP1JPxDpRoRxs5Oa17sVR4c0kyPyxwP0QoihKth1eQElbINx+7Ikbt7LoGucPUKEsnxrDzkUt8og==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.16.4.tgz", + "integrity": "sha512-ZzIn3KvvRqMfcect4Dy+57C9XoQXZhpVJKBdQWMp9wM1qJEgsVgGDcaSBYCs/UYSKMRMP6Wm20pKCt408RkQzg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3", - "@parcel/workers": "2.16.3", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/transformer-js": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.16.3.tgz", - "integrity": "sha512-k83yElHagwDRYfza7BrADdf9NRGpizX3zOfctfEsQWh9mEZLNJENivP6ZLB9Aje9H0GaaSTiYU8VwOWLXbLgOw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.16.4.tgz", + "integrity": "sha512-FD2fdO6URwAGBPidb3x1dDgLBt972mko0LelcSU05aC/pcKaV9mbCtINbPul1MlStzkxDelhuImcCYIyerheVQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.16.3", - "@parcel/workers": "2.16.3", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", "@swc/helpers": "^0.5.0", "browserslist": "^4.24.5", "nullthrows": "^1.1.1", @@ -5252,20 +5252,20 @@ }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@parcel/transformer-js/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5276,18 +5276,18 @@ } }, "node_modules/@parcel/transformer-json": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.16.3.tgz", - "integrity": "sha512-iT4IKGT95+S/7RBK1MUY/KxD8ad9FUlElF+w40NBLv4lm012wkYogFRhEHnyElPOByZL1aJ8GaVOGbZL9yuZfg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.16.4.tgz", + "integrity": "sha512-pB3ZNqgokdkBCJ+4G0BrPYcIkyM9K1HVk0GvjzcLEFDKsoAp8BGEM68FzagFM/nVq9anYTshIaoh349GK0M/bg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", + "@parcel/plugin": "2.16.4", "json5": "^2.2.3" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5295,17 +5295,17 @@ } }, "node_modules/@parcel/transformer-node": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-node/-/transformer-node-2.16.3.tgz", - "integrity": "sha512-FIbSphLisxmzwqE43ALsGeSPSYBA3ZE6xmhAIgwoFdeI6VfTSkCZnGhSqUhP3m9R55IuWm/+NP6BlePWADmkwg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-node/-/transformer-node-2.16.4.tgz", + "integrity": "sha512-7t43CPGfMJk1LqFokwxHSsRi+kKC2QvDXaMtqiMShmk50LCwn81WgzuFvNhMwf6lSiBihWupGwF3Fqksg+aisg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3" + "@parcel/plugin": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5313,16 +5313,16 @@ } }, "node_modules/@parcel/transformer-postcss": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.16.3.tgz", - "integrity": "sha512-OMjU17OwPhPBK2LIzqQozBezolqI8jPgoT+CmoOkKr1GlgWMzCcHFpW6KQZxVVR+vI0lUEJp+RZc9MzhNndv4A==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.16.4.tgz", + "integrity": "sha512-jfmh9ho03H+qwz9S1b/a/oaOmgfMovtHKYDweIGMjKULKIee3AFRqo8RZIOuUMjDuqHWK8SqQmjery4syFV3Xw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", "clone": "^2.1.2", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", @@ -5330,7 +5330,7 @@ }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5338,9 +5338,9 @@ } }, "node_modules/@parcel/transformer-postcss/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5351,18 +5351,18 @@ } }, "node_modules/@parcel/transformer-posthtml": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.16.3.tgz", - "integrity": "sha512-y3iuM+yp8nPbt8sbQayPGR0saVGR6uj0aYr7hWoS0oUe9vZsH1mP3BTP6L6ABe/dZKU3QcFmMQgLwH6WC/apAA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.16.4.tgz", + "integrity": "sha512-+GXsmGx1L25KQGQnwclgEuQe1t4QU+IoDkgN+Ikj+EnQCOWG4/ts2VpMBeqP5F18ZT4cCSRafj6317o/2lSGJg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3" + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5370,17 +5370,17 @@ } }, "node_modules/@parcel/transformer-raw": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.16.3.tgz", - "integrity": "sha512-Lha1+z75QbNAsxMAffp5K+ykGXEYSNOFUqI/8XtetYfuqIvS5s/OBkwsg8MWbjtPkbKo1F3EwNBaIAagw/BbIg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.16.4.tgz", + "integrity": "sha512-7WDUPq+bW11G9jKxaQIVL+NPGolV99oq/GXhpjYip0SaGaLzRCW7gEk60cftuk0O7MsDaX5jcAJm3G/AX+LJKg==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/plugin": "2.16.3" + "@parcel/plugin": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5388,20 +5388,20 @@ } }, "node_modules/@parcel/transformer-react-refresh-wrap": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.16.3.tgz", - "integrity": "sha512-8rzO5iKF5bYrPUnbw4At0H7AwE+UHkuNNo385JL0VzXggrA0VsXsjjJwXVyhSeMvEbo2ioo/+nYUlazTQBABwA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.16.4.tgz", + "integrity": "sha512-MiLNZrsGQJTANKKa4lzZyUbGj/en0Hms474mMdQkCBFg6GmjfmXwaMMgtTfPA3ZwSp2+3LeObCyca/f9B2gBZQ==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/error-overlay": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/error-overlay": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", "react-refresh": "^0.16.0" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5409,19 +5409,19 @@ } }, "node_modules/@parcel/transformer-svg": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.16.3.tgz", - "integrity": "sha512-fDpUWSBZxt/R5pZUNd4gV/BX0c7B074lw/wmqwowjcwQU/QxhzPJBDlAsyTvOJ75PeJiQf/qFtnIK5bNwMoasA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.16.4.tgz", + "integrity": "sha512-0dm4cQr/WpfQP6N0xjFtwdLTxcONDfoLgTOMk4eNUWydHipSgmLtvUk/nOc/FWkwztRScfAObtZXOiPOd3Oy9A==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/plugin": "2.16.3", - "@parcel/rust": "2.16.3" + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4" }, "engines": { "node": ">= 16.0.0", - "parcel": "^2.16.3" + "parcel": "^2.16.4" }, "funding": { "type": "opencollective", @@ -5429,41 +5429,41 @@ } }, "node_modules/@parcel/types": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.16.3.tgz", - "integrity": "sha512-aIJJFMif/A7u86UEt3sJPZ/F7suQW56ugiCp2Y2mYTPHpTJbI2Knk9yO4fkWHNO1BrH6a/VUWh7bWIOsQtzL1Q==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.16.4.tgz", + "integrity": "sha512-ctx4mBskZHXeDVHg4OjMwx18jfYH9BzI/7yqbDQVGvd5lyA+/oVVzYdpele2J2i2sSaJ87cA8nb57GDQ8kHAqA==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/types-internal": "2.16.3", - "@parcel/workers": "2.16.3" + "@parcel/types-internal": "2.16.4", + "@parcel/workers": "2.16.4" } }, "node_modules/@parcel/types-internal": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.16.3.tgz", - "integrity": "sha512-zi2GKdJHpNeW9sspTBfM68A9lekEztTWU8Dxs1ouPk90lfA0tfrMznAvkD5iJdKsM6usbgcqjjI8s+Ow8OrsBg==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.16.4.tgz", + "integrity": "sha512-PE6Qmt5cjzBxX+6MPLiF7r+twoC+V9Skt3zyuBQ+H1c0i9o07Bbz2NKX10nvlPukfmW6Fu/1RvTLkzBZR1bU6A==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/feature-flags": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/feature-flags": "2.16.4", "@parcel/source-map": "^2.1.1", "utility-types": "^3.11.0" } }, "node_modules/@parcel/utils": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.16.3.tgz", - "integrity": "sha512-g/yqVWSdZqPvTiS96dEK9MEl7q6w31u+luD5VGt6f9w6PQCpuVajhhDNuXf9uzDU/dL4sSZPKUhLteVZDqryHA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.16.4.tgz", + "integrity": "sha512-lkmxQHcHyOWZLbV8t+h2CGZIkPiBurLm/TS5wNT7+tq0qt9KbVwL7FP2K93TbXhLMGTmpI79Bf3qKniPM167Mw==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/codeframe": "2.16.3", - "@parcel/diagnostic": "2.16.3", - "@parcel/logger": "2.16.3", - "@parcel/markdown-ansi": "2.16.3", - "@parcel/rust": "2.16.3", + "@parcel/codeframe": "2.16.4", + "@parcel/diagnostic": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/markdown-ansi": "2.16.4", + "@parcel/rust": "2.16.4", "@parcel/source-map": "^2.1.1", "chalk": "^4.1.2", "nullthrows": "^1.1.1" @@ -5809,17 +5809,17 @@ } }, "node_modules/@parcel/workers": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.16.3.tgz", - "integrity": "sha512-SxIXRnrlQFhw377wxWC5WIl1FL1Y9IedhUtuc7j3uac3tlbCQJJ+3rFr5/BDUknJbTktvVsPakE98fH7TIJyyw==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.16.4.tgz", + "integrity": "sha512-dkBEWqnHXDZnRbTZouNt4uEGIslJT+V0c8OH1MPOfjISp1ucD6/u9ET8k9d/PxS9h1hL53og0SpBuuSEPLDl6A==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.16.3", - "@parcel/logger": "2.16.3", - "@parcel/profiler": "2.16.3", - "@parcel/types-internal": "2.16.3", - "@parcel/utils": "2.16.3", + "@parcel/diagnostic": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/profiler": "2.16.4", + "@parcel/types-internal": "2.16.4", + "@parcel/utils": "2.16.4", "nullthrows": "^1.1.1" }, "engines": { @@ -5830,7 +5830,7 @@ "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.16.3" + "@parcel/core": "^2.16.4" } }, "node_modules/@playwright/test": { @@ -6011,9 +6011,9 @@ } }, "node_modules/@swc/core": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz", - "integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz", + "integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -6029,16 +6029,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.8", - "@swc/core-darwin-x64": "1.15.8", - "@swc/core-linux-arm-gnueabihf": "1.15.8", - "@swc/core-linux-arm64-gnu": "1.15.8", - "@swc/core-linux-arm64-musl": "1.15.8", - "@swc/core-linux-x64-gnu": "1.15.8", - "@swc/core-linux-x64-musl": "1.15.8", - "@swc/core-win32-arm64-msvc": "1.15.8", - "@swc/core-win32-ia32-msvc": "1.15.8", - "@swc/core-win32-x64-msvc": "1.15.8" + "@swc/core-darwin-arm64": "1.15.11", + "@swc/core-darwin-x64": "1.15.11", + "@swc/core-linux-arm-gnueabihf": "1.15.11", + "@swc/core-linux-arm64-gnu": "1.15.11", + "@swc/core-linux-arm64-musl": "1.15.11", + "@swc/core-linux-x64-gnu": "1.15.11", + "@swc/core-linux-x64-musl": "1.15.11", + "@swc/core-win32-arm64-msvc": "1.15.11", + "@swc/core-win32-ia32-msvc": "1.15.11", + "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -6050,9 +6050,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.8.tgz", - "integrity": "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz", + "integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==", "cpu": [ "arm64" ], @@ -6067,9 +6067,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.8.tgz", - "integrity": "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz", + "integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==", "cpu": [ "x64" ], @@ -6084,9 +6084,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.8.tgz", - "integrity": "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz", + "integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==", "cpu": [ "arm" ], @@ -6101,9 +6101,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.8.tgz", - "integrity": "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz", + "integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==", "cpu": [ "arm64" ], @@ -6118,9 +6118,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.8.tgz", - "integrity": "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz", + "integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==", "cpu": [ "arm64" ], @@ -6135,9 +6135,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.8.tgz", - "integrity": "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz", + "integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==", "cpu": [ "x64" ], @@ -6152,9 +6152,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.8.tgz", - "integrity": "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz", + "integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==", "cpu": [ "x64" ], @@ -6169,9 +6169,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.8.tgz", - "integrity": "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz", + "integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==", "cpu": [ "arm64" ], @@ -6186,9 +6186,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.8.tgz", - "integrity": "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz", + "integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==", "cpu": [ "ia32" ], @@ -6203,9 +6203,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.8.tgz", - "integrity": "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz", + "integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==", "cpu": [ "x64" ], @@ -13330,9 +13330,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -13346,23 +13346,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -13381,9 +13381,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -13402,9 +13402,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -13423,9 +13423,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -13444,9 +13444,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -13465,9 +13465,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -13486,9 +13486,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -13507,9 +13507,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -13528,9 +13528,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -13549,9 +13549,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -13570,9 +13570,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -13692,9 +13692,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -15736,24 +15736,24 @@ } }, "node_modules/parcel": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.16.3.tgz", - "integrity": "sha512-N9jnwcTeVEaRjjJCCHmYfPCvjjJeHZuuO50qL4CCNcQX4RjwPuOaDft7hvTT2W8PIb4XhhZKDYB1lstZhXLJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@parcel/config-default": "2.16.3", - "@parcel/core": "2.16.3", - "@parcel/diagnostic": "2.16.3", - "@parcel/events": "2.16.3", - "@parcel/feature-flags": "2.16.3", - "@parcel/fs": "2.16.3", - "@parcel/logger": "2.16.3", - "@parcel/package-manager": "2.16.3", - "@parcel/reporter-cli": "2.16.3", - "@parcel/reporter-dev-server": "2.16.3", - "@parcel/reporter-tracer": "2.16.3", - "@parcel/utils": "2.16.3", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.16.4.tgz", + "integrity": "sha512-RQlrqs4ujYNJpTQi+dITqPKNhRWEqpjPd1YBcGp50Wy3FcJHpwu0/iRm7XWz2dKU/Bwp2qCcVYPIeEDYi2uOUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/config-default": "2.16.4", + "@parcel/core": "2.16.4", + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4", + "@parcel/feature-flags": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/package-manager": "2.16.4", + "@parcel/reporter-cli": "2.16.4", + "@parcel/reporter-dev-server": "2.16.4", + "@parcel/reporter-tracer": "2.16.4", + "@parcel/utils": "2.16.4", "chalk": "^4.1.2", "commander": "^12.1.0", "get-port": "^4.2.0" @@ -16885,9 +16885,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -17393,9 +17393,9 @@ } }, "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/packages/core/src/tech/BaseTech.test.ts b/packages/core/src/tech/BaseTech.test.ts new file mode 100644 index 0000000..0a3f368 --- /dev/null +++ b/packages/core/src/tech/BaseTech.test.ts @@ -0,0 +1,365 @@ +import BaseTech, { PlaybackState, IPlayerState, getTextTrackId } from './BaseTech'; +import { PlayerEvent } from '../util/constants'; + +// Mock mitt +jest.mock('mitt', () => { + return { + __esModule: true, + default: () => { + const handlers = new Map>(); + return { + on(type: string, handler: Function) { + if (!handlers.has(type)) handlers.set(type, new Set()); + handlers.get(type)!.add(handler); + }, + off(type: string, handler: Function) { + handlers.get(type)?.delete(handler); + }, + emit(type: string, data?: any) { + handlers.get(type)?.forEach((handler) => handler(data)); + }, + all: { + clear() { + handlers.clear(); + }, + }, + }; + }, + }; +}); + +function createMockVideo(): HTMLVideoElement { + const listeners: Record = {}; + const video = { + muted: false, + volume: 1, + paused: true, + currentTime: 0, + duration: 100, + src: '', + seekable: { + length: 0, + end: jest.fn().mockReturnValue(0), + }, + audioTracks: undefined, + textTracks: [], + addEventListener(event: string, handler: Function) { + if (!listeners[event]) listeners[event] = []; + listeners[event].push(handler); + }, + removeEventListener(event: string, handler: Function) { + if (listeners[event]) { + listeners[event] = listeners[event].filter((h) => h !== handler); + } + }, + play: jest.fn().mockResolvedValue(undefined), + pause: jest.fn(), + load: jest.fn(), + _trigger(event: string, data?: any) { + (listeners[event] || []).forEach((h) => h(data)); + }, + _listeners: listeners, + } as unknown as HTMLVideoElement & { _trigger: Function; _listeners: Record }; + return video; +} + +describe('BaseTech', () => { + let video: ReturnType; + let tech: BaseTech; + + beforeEach(() => { + video = createMockVideo(); + tech = new BaseTech({ video } as any); + }); + + afterEach(() => { + tech.destroy(); + }); + + describe('constructor', () => { + it('should set initial state to IDLE', () => { + const stateHandler = jest.fn(); + tech.on(PlayerEvent.STATE_CHANGE, stateHandler); + // State is set in constructor, verify via updateState side effects + expect(tech.name).toBe('BaseTech'); + }); + + it('should register video event listeners', () => { + const mock = video as any; + const events = Object.keys(mock._listeners); + expect(events).toContain('play'); + expect(events).toContain('playing'); + expect(events).toContain('pause'); + expect(events).toContain('timeupdate'); + expect(events).toContain('waiting'); + expect(events).toContain('seeking'); + expect(events).toContain('seeked'); + expect(events).toContain('canplay'); + expect(events).toContain('volumechange'); + expect(events).toContain('ended'); + }); + }); + + describe('play()', () => { + it('should call video.play()', async () => { + const result = await tech.play(); + expect((video as any).play).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should return false if play rejects', async () => { + (video as any).play.mockRejectedValueOnce(new Error('not allowed')); + const result = await tech.play(); + expect(result).toBe(false); + }); + }); + + describe('pause()', () => { + it('should call video.pause()', () => { + tech.pause(); + expect((video as any).pause).toHaveBeenCalled(); + }); + }); + + describe('mute() / unmute()', () => { + it('should set video.muted to true', () => { + tech.mute(); + expect(video.muted).toBe(true); + }); + + it('should set video.muted to false', () => { + tech.mute(); + tech.unmute(); + expect(video.muted).toBe(false); + }); + }); + + describe('volume', () => { + it('should get volume from video', () => { + (video as any).volume = 0.75; + expect(tech.volume).toBe(0.75); + }); + + it('should set volume on video', () => { + tech.volume = 0.5; + expect(video.volume).toBe(0.5); + }); + }); + + describe('load()', () => { + it('should set default state and resolve', async () => { + const stateHandler = jest.fn(); + tech.on(PlayerEvent.STATE_CHANGE, stateHandler); + await tech.load('http://example.com/video.mp4'); + expect(video.src).toBe('http://example.com/video.mp4'); + // State should have been set to LOADING + expect(stateHandler).toHaveBeenCalled(); + }); + }); + + describe('stop()', () => { + it('should clear video src and emit PLAYER_STOPPED', () => { + const stopHandler = jest.fn(); + tech.on(PlayerEvent.PLAYER_STOPPED, stopHandler); + tech.stop(); + expect(video.src).toBe(''); + expect(stopHandler).toHaveBeenCalled(); + }); + }); + + describe('event emission', () => { + it('should emit PLAY on video play event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.PLAY, handler); + (video as any)._trigger('play'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit PAUSE on video pause event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.PAUSE, handler); + (video as any)._trigger('pause'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit PLAYING on video playing event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.PLAYING, handler); + (video as any)._trigger('playing'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit SEEKING on video seeking event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.SEEKING, handler); + (video as any)._trigger('seeking'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit SEEKED on video seeked event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.SEEKED, handler); + (video as any)._trigger('seeked'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit TIME_UPDATE with currentTime and duration', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.TIME_UPDATE, handler); + (video as any).currentTime = 10; + (video as any).duration = 100; + (video as any)._trigger('timeupdate'); + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + currentTime: 10, + duration: 100, + }) + ); + }); + + it('should emit WAITING on video waiting event when not seeking', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.WAITING, handler); + (video as any)._trigger('waiting'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit BUFFERING on waiting when not seeking', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.BUFFERING, handler); + (video as any)._trigger('waiting'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit VOLUME_CHANGE on video volumechange', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.VOLUME_CHANGE, handler); + (video as any).volume = 0.5; + (video as any)._trigger('volumechange'); + expect(handler).toHaveBeenCalledWith(expect.objectContaining({ volume: 0.5 })); + }); + + it('should emit ENDED on video ended event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.ENDED, handler); + (video as any)._trigger('ended'); + expect(handler).toHaveBeenCalled(); + }); + + it('should emit LOADED_METADATA on canplay', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.LOADED_METADATA, handler); + (video as any)._trigger('canplay'); + expect(handler).toHaveBeenCalled(); + }); + }); + + describe('state management', () => { + it('should transition to PLAYING state on play event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.STATE_CHANGE, handler); + (video as any)._trigger('play'); + const lastCall = handler.mock.calls[handler.mock.calls.length - 1][0]; + expect(lastCall.state.playbackState).toBe(PlaybackState.PLAYING); + }); + + it('should transition to PAUSED state on pause event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.STATE_CHANGE, handler); + (video as any)._trigger('pause'); + const lastCall = handler.mock.calls[handler.mock.calls.length - 1][0]; + expect(lastCall.state.playbackState).toBe(PlaybackState.PAUSED); + }); + + it('should transition to SEEKING state on seeking event', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.STATE_CHANGE, handler); + (video as any)._trigger('seeking'); + const lastCall = handler.mock.calls[handler.mock.calls.length - 1][0]; + expect(lastCall.state.playbackState).toBe(PlaybackState.SEEKING); + }); + + it('should transition to IDLE on ended', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.STATE_CHANGE, handler); + (video as any)._trigger('ended'); + const lastCall = handler.mock.calls[handler.mock.calls.length - 1][0]; + expect(lastCall.state.playbackState).toBe(PlaybackState.IDLE); + }); + }); + + describe('isLive', () => { + it('should return true when video duration is Infinity', () => { + Object.defineProperty(video, 'duration', { value: Infinity, writable: true }); + expect(tech.isLive).toBe(true); + }); + + it('should return false for finite duration', () => { + Object.defineProperty(video, 'duration', { value: 100, writable: true }); + expect(tech.isLive).toBe(false); + }); + }); + + describe('duration', () => { + it('should return video duration for VOD', () => { + Object.defineProperty(video, 'duration', { value: 120, writable: true }); + expect(tech.duration).toBe(120); + }); + + it('should return 0 if duration is NaN', () => { + Object.defineProperty(video, 'duration', { value: NaN, writable: true }); + expect(tech.duration).toBe(0); + }); + }); + + describe('currentTime', () => { + it('should get currentTime from video', () => { + (video as any).currentTime = 42; + expect(tech.currentTime).toBe(42); + }); + + it('should set currentTime on video when seekable', () => { + tech.currentTime = 30; + expect(video.currentTime).toBe(30); + }); + }); + + describe('getVideoLevels()', () => { + it('should return empty array by default', () => { + expect(tech.getVideoLevels()).toEqual([]); + }); + }); + + describe('seekToLive()', () => { + it('should seek to near end of duration', () => { + Object.defineProperty(video, 'duration', { value: 600, writable: true }); + tech.seekToLive(); + // BaseTech.seekToLive sets currentTime to duration - LIVE_EDGE (10) + expect(video.currentTime).toBe(590); + }); + }); + + describe('destroy()', () => { + it('should stop playback and remove event listeners', () => { + const stopHandler = jest.fn(); + tech.on(PlayerEvent.PLAYER_STOPPED, stopHandler); + tech.destroy(); + expect(stopHandler).toHaveBeenCalled(); + }); + }); +}); + +describe('getTextTrackId()', () => { + it('should return null for null track', () => { + expect(getTextTrackId(null)).toBeNull(); + }); + + it('should return null for undefined track', () => { + expect(getTextTrackId(undefined)).toBeNull(); + }); + + it('should return formatted id string', () => { + const track = { id: '1', label: 'English', language: 'en' }; + expect(getTextTrackId(track)).toBe('1|English|en'); + }); +}); diff --git a/packages/core/src/tech/DashJsTech.test.ts b/packages/core/src/tech/DashJsTech.test.ts new file mode 100644 index 0000000..22b42c4 --- /dev/null +++ b/packages/core/src/tech/DashJsTech.test.ts @@ -0,0 +1,192 @@ +import { PlayerEvent } from '../util/constants'; + +let mockMediaPlayer: any; + +function createFreshMockMediaPlayer() { + return { + create: jest.fn(), + initialize: jest.fn(), + attachView: jest.fn(), + attachSource: jest.fn(), + on: jest.fn(), + isDynamic: jest.fn().mockReturnValue(false), + reset: jest.fn(), + events: { + MANIFEST_LOADED: 'manifestLoaded', + ERROR: 'error', + }, + }; +} + +// MediaPlayer() returns an object with create() +// MediaPlayer().create() returns the actual player instance +jest.mock('dashjs', () => ({ + MediaPlayer: Object.assign( + jest.fn(() => ({ + create: jest.fn(() => { + mockMediaPlayer = createFreshMockMediaPlayer(); + return mockMediaPlayer; + }), + })), + { + events: { + MANIFEST_LOADED: 'manifestLoaded', + ERROR: 'error', + }, + } + ), +})); + +// Mock mitt +jest.mock('mitt', () => ({ + __esModule: true, + default: () => { + const handlers = new Map>(); + return { + on(type: string, handler: Function) { + if (!handlers.has(type)) handlers.set(type, new Set()); + handlers.get(type)!.add(handler); + }, + off(type: string, handler: Function) { + handlers.get(type)?.delete(handler); + }, + emit(type: string, data?: any) { + handlers.get(type)?.forEach((h) => h(data)); + }, + all: { clear() { handlers.clear(); } }, + }; + }, +})); + +import MssPlayer from './DashJsTech'; + +function createMockVideo(): HTMLVideoElement { + const listeners: Record = {}; + return { + muted: false, + volume: 1, + paused: true, + currentTime: 0, + duration: 100, + src: '', + seekable: { length: 0, end: jest.fn() }, + addEventListener(event: string, handler: Function) { + if (!listeners[event]) listeners[event] = []; + listeners[event].push(handler); + }, + removeEventListener: jest.fn(), + play: jest.fn().mockResolvedValue(undefined), + pause: jest.fn(), + load: jest.fn(), + _trigger(event: string, data?: any) { + (listeners[event] || []).forEach((h) => h(data)); + }, + _listeners: listeners, + } as unknown as HTMLVideoElement & { _trigger: Function; _listeners: Record }; +} + +describe('DashJsTech (MssPlayer)', () => { + let video: ReturnType; + let tech: MssPlayer; + let destroyed: boolean; + + beforeEach(() => { + jest.clearAllMocks(); + destroyed = false; + video = createMockVideo(); + tech = new MssPlayer({ video, src: '' } as any); + }); + + afterEach(() => { + // Only destroy if not already destroyed by the test + // DashJsTech.destroy() sets mediaPlayer = null, which causes + // BaseTech.destroy() -> stop() -> isLive -> mediaPlayer.isDynamic() to crash + if (!destroyed) { + try { + tech.destroy(); + } catch (e) { + // ignore — already destroyed + } + } + }); + + describe('constructor', () => { + it('should initialize dash.js MediaPlayer', () => { + expect(mockMediaPlayer.initialize).toHaveBeenCalled(); + }); + + it('should attach video element', () => { + expect(mockMediaPlayer.attachView).toHaveBeenCalledWith(video); + }); + }); + + describe('load()', () => { + it('should attach source and resolve on MANIFEST_LOADED', async () => { + const loadPromise = tech.load('http://example.com/stream.ism/Manifest'); + + // Get the MANIFEST_LOADED callback + const onCall = mockMediaPlayer.on.mock.calls.find( + (c: any) => c[0] === 'manifestLoaded' + ); + expect(onCall).toBeTruthy(); + // Trigger success + onCall![1](); + + await loadPromise; + expect(mockMediaPlayer.attachSource).toHaveBeenCalledWith( + 'http://example.com/stream.ism/Manifest' + ); + }); + + it('should reject on ERROR and emit PlayerEvent.ERROR with standardized format', async () => { + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + const loadPromise = tech.load('http://bad.url/stream.ism/Manifest'); + + // Get the ERROR callback + const errorCall = mockMediaPlayer.on.mock.calls.find( + (c: any) => c[0] === 'error' + ); + expect(errorCall).toBeTruthy(); + // Trigger error + errorCall![1]({ error: { code: 25, message: 'Download error' } }); + + await expect(loadPromise).rejects.toBe('Failed to load Mss Player'); + + // Verify standardized error emission + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + category: 'MEDIA', + code: '25', + message: 'Download error', + }), + fatal: true, + }) + ); + }); + }); + + describe('isLive', () => { + it('should delegate to mediaPlayer.isDynamic()', () => { + mockMediaPlayer.isDynamic.mockReturnValue(true); + expect(tech.isLive).toBe(true); + expect(mockMediaPlayer.isDynamic).toHaveBeenCalled(); + }); + + it('should return false for VOD', () => { + mockMediaPlayer.isDynamic.mockReturnValue(false); + expect(tech.isLive).toBe(false); + }); + }); + + describe('destroy()', () => { + it('should reset the media player', () => { + const resetFn = mockMediaPlayer.reset; + tech.destroy(); + destroyed = true; + expect(resetFn).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/tech/DashJsTech.ts b/packages/core/src/tech/DashJsTech.ts index 91f881e..e0f3864 100644 --- a/packages/core/src/tech/DashJsTech.ts +++ b/packages/core/src/tech/DashJsTech.ts @@ -1,5 +1,7 @@ import { MediaPlayer, MediaPlayerClass } from 'dashjs'; import BaseTech, { IBaseTechOptions, PlaybackState } from './BaseTech'; +import { PlayerEvent } from '../util/constants'; +import { formatDashError } from '../util/errors'; export default class MssPlayer extends BaseTech { private mediaPlayer: MediaPlayerClass; @@ -19,6 +21,7 @@ export default class MssPlayer extends BaseTech { resolve(); }); this.mediaPlayer.on(MediaPlayer.events.ERROR, (ev) => { + this.emit(PlayerEvent.ERROR, formatDashError(ev)); reject(`Failed to load Mss Player`); }); }); @@ -29,10 +32,10 @@ export default class MssPlayer extends BaseTech { } destroy() { + super.destroy(); if (this.mediaPlayer) { this.mediaPlayer.reset(); this.mediaPlayer = null; } - super.destroy(); } } diff --git a/packages/core/src/tech/HlsJsTech.test.ts b/packages/core/src/tech/HlsJsTech.test.ts new file mode 100644 index 0000000..b49de1b --- /dev/null +++ b/packages/core/src/tech/HlsJsTech.test.ts @@ -0,0 +1,480 @@ +import { PlayerEvent } from '../util/constants'; +import { formatHlsError } from '../util/errors'; + +// Track Hls event listeners +const hlsListeners: Record = {}; + +const mockHlsInstance = { + attachMedia: jest.fn(), + loadSource: jest.fn(), + on: jest.fn((event: string, handler: Function) => { + hlsListeners[event] = handler; + }), + once: jest.fn((event: string, handler: Function) => { + hlsListeners[`once:${event}`] = handler; + }), + destroy: jest.fn(), + levels: [], + currentLevel: 0, + nextLevel: -1, + liveSyncPosition: 100, + audioTrack: 0, + audioTracks: [], + subtitleTrack: 0, + subtitleTracks: [], + url: 'http://example.com/stream.m3u8', +}; + +jest.mock('hls.js', () => { + const MockHls = jest.fn().mockImplementation(() => mockHlsInstance); + (MockHls as any).isSupported = jest.fn().mockReturnValue(true); + (MockHls as any).Events = { + AUDIO_TRACK_SWITCHED: 'hlsAudioTrackSwitched', + SUBTITLE_TRACK_SWITCH: 'hlsSubtitleTrackSwitch', + LEVEL_LOADED: 'hlsLevelLoaded', + LEVEL_SWITCHED: 'hlsLevelSwitched', + ERROR: 'hlsError', + MANIFEST_PARSED: 'hlsManifestParsed', + INTERSTITIAL_STARTED: 'hlsInterstitialStarted', + INTERSTITIAL_ENDED: 'hlsInterstitialEnded', + INTERSTITIAL_ASSET_STARTED: 'hlsInterstitialAssetStarted', + INTERSTITIAL_ASSET_ENDED: 'hlsInterstitialAssetEnded', + ASSET_LIST_LOADED: 'hlsAssetListLoaded', + }; + (MockHls as any).ErrorDetails = { + MANIFEST_LOAD_ERROR: 'manifestLoadError', + MANIFEST_PARSING_ERROR: 'manifestParsingError', + MANIFEST_LOAD_TIMEOUT: 'manifestLoadTimeout', + LEVEL_LOAD_ERROR: 'levelLoadError', + LEVEL_LOAD_TIMEOUT: 'levelLoadTimeout', + FRAG_LOAD_ERROR: 'fragLoadError', + FRAG_LOAD_TIMEOUT: 'fragLoadTimeout', + KEY_LOAD_ERROR: 'keyLoadError', + KEY_LOAD_TIMEOUT: 'keyLoadTimeout', + AUDIO_TRACK_LOAD_ERROR: 'audioTrackLoadError', + AUDIO_TRACK_LOAD_TIMEOUT: 'audioTrackLoadTimeout', + }; + return { __esModule: true, default: MockHls }; +}); + +// Mock mitt +jest.mock('mitt', () => ({ + __esModule: true, + default: () => { + const handlers = new Map>(); + return { + on(type: string, handler: Function) { + if (!handlers.has(type)) handlers.set(type, new Set()); + handlers.get(type)!.add(handler); + }, + off(type: string, handler: Function) { + handlers.get(type)?.delete(handler); + }, + emit(type: string, data?: any) { + handlers.get(type)?.forEach((h) => h(data)); + }, + all: { clear() { handlers.clear(); } }, + }; + }, +})); + +import HlsJsTech from './HlsJsTech'; +import Hls from 'hls.js'; + +function createMockVideo(): HTMLVideoElement { + const listeners: Record = {}; + return { + muted: false, + volume: 1, + paused: true, + currentTime: 0, + duration: 100, + src: '', + seekable: { length: 0, end: jest.fn() }, + addEventListener(event: string, handler: Function) { + if (!listeners[event]) listeners[event] = []; + listeners[event].push(handler); + }, + removeEventListener: jest.fn(), + play: jest.fn().mockResolvedValue(undefined), + pause: jest.fn(), + load: jest.fn(), + _trigger(event: string, data?: any) { + (listeners[event] || []).forEach((h) => h(data)); + }, + _listeners: listeners, + } as unknown as HTMLVideoElement & { _trigger: Function; _listeners: Record }; +} + +describe('HlsJsTech', () => { + let video: ReturnType; + let tech: HlsJsTech; + + beforeEach(() => { + jest.clearAllMocks(); + // Clear listeners + Object.keys(hlsListeners).forEach((k) => delete hlsListeners[k]); + + mockHlsInstance.levels = []; + mockHlsInstance.currentLevel = 0; + mockHlsInstance.audioTrack = 0; + mockHlsInstance.audioTracks = []; + mockHlsInstance.subtitleTrack = 0; + mockHlsInstance.subtitleTracks = []; + mockHlsInstance.liveSyncPosition = 100; + + video = createMockVideo(); + tech = new HlsJsTech({ video } as any); + }); + + afterEach(() => { + tech.destroy(); + }); + + describe('static isSupported()', () => { + it('should delegate to Hls.isSupported()', () => { + expect(HlsJsTech.isSupported()).toBe(true); + expect(Hls.isSupported).toHaveBeenCalled(); + }); + }); + + describe('constructor', () => { + it('should have name "HlsJsTech"', () => { + expect(tech.name).toBe('HlsJsTech'); + }); + + it('should create Hls instance with default config', () => { + expect(Hls).toHaveBeenCalledWith( + expect.objectContaining({ + capLevelOnFPSDrop: true, + capLevelToPlayerSize: true, + enableInterstitialPlayback: true, + }) + ); + }); + + it('should attach media to hls', () => { + expect(mockHlsInstance.attachMedia).toHaveBeenCalledWith(video); + }); + + it('should register HLS event listeners', () => { + const registeredEvents = mockHlsInstance.on.mock.calls.map((c) => c[0]); + expect(registeredEvents).toContain(Hls.Events.AUDIO_TRACK_SWITCHED); + expect(registeredEvents).toContain(Hls.Events.SUBTITLE_TRACK_SWITCH); + expect(registeredEvents).toContain(Hls.Events.LEVEL_LOADED); + expect(registeredEvents).toContain(Hls.Events.LEVEL_SWITCHED); + expect(registeredEvents).toContain(Hls.Events.ERROR); + expect(registeredEvents).toContain(Hls.Events.INTERSTITIAL_STARTED); + expect(registeredEvents).toContain(Hls.Events.INTERSTITIAL_ENDED); + }); + }); + + describe('load()', () => { + it('should call hls.loadSource with the src', async () => { + const loadPromise = tech.load('http://example.com/stream.m3u8'); + // Trigger MANIFEST_PARSED to resolve + const manifestHandler = hlsListeners[`once:${Hls.Events.MANIFEST_PARSED}`]; + expect(manifestHandler).toBeTruthy(); + manifestHandler('', {}); + await loadPromise; + expect(mockHlsInstance.loadSource).toHaveBeenCalledWith('http://example.com/stream.m3u8'); + }); + }); + + describe('error handling', () => { + it('should emit ERROR on HLS error event', () => { + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + const onError = hlsListeners[Hls.Events.ERROR]; + expect(onError).toBeTruthy(); + + onError('hlsError', { + type: 'networkError', + details: 'manifestLoadError', + fatal: true, + response: { code: 404, text: 'Not Found' }, + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + category: 'networkError', + code: '404', + message: 'Not Found', + }), + fatal: true, + }) + ); + }); + + it('should handle non-fatal errors', () => { + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + const onError = hlsListeners[Hls.Events.ERROR]; + onError('hlsError', { + type: 'networkError', + details: 'fragLoadError', + fatal: false, + response: { code: 503, text: 'Service Unavailable' }, + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + fatal: false, + }) + ); + }); + + it('should handle parsing errors with reason', () => { + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + const onError = hlsListeners[Hls.Events.ERROR]; + onError('hlsError', { + type: 'networkError', + details: 'manifestParsingError', + fatal: true, + reason: 'Invalid HLS manifest', + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + message: 'Invalid HLS manifest', + }), + }) + ); + }); + + it('should handle timeout errors', () => { + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + const onError = hlsListeners[Hls.Events.ERROR]; + onError('hlsError', { + type: 'networkError', + details: 'manifestLoadTimeout', + fatal: true, + }); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + category: 'networkError', + code: '-1', + }), + fatal: true, + }) + ); + }); + }); + + describe('isLive', () => { + it('should return falsy by default', () => { + expect(tech.isLive).toBeFalsy(); + }); + + it('should return true after live level loaded', () => { + const onLevelLoaded = hlsListeners[Hls.Events.LEVEL_LOADED]; + onLevelLoaded('', { details: { live: true, totalduration: 600 } }); + expect(tech.isLive).toBe(true); + }); + }); + + describe('currentLevel', () => { + it('should return current level from hls levels', () => { + mockHlsInstance.levels = [ + { width: 1920, height: 1080, bitrate: 5000000 }, + { width: 1280, height: 720, bitrate: 3000000 }, + ] as any; + mockHlsInstance.currentLevel = 0; + + const level = tech.currentLevel; + expect(level).toEqual({ + id: 0, + width: 1920, + height: 1080, + bitrate: 5000000, + }); + }); + + it('should set nextLevel to -1 for auto when level is null', () => { + tech.currentLevel = null; + expect(mockHlsInstance.nextLevel).toBe(-1); + }); + + it('should set specific level id', () => { + tech.currentLevel = { id: 1, width: 1280, height: 720, bitrate: 3000000 }; + expect(mockHlsInstance.currentLevel).toBe(1); + }); + }); + + describe('getVideoLevels()', () => { + it('should return mapped levels', () => { + mockHlsInstance.levels = [ + { width: 1920, height: 1080, bitrate: 5000000 }, + { width: 1280, height: 720, bitrate: 3000000 }, + ] as any; + + const levels = tech.getVideoLevels(); + expect(levels).toEqual([ + { id: 0, width: 1920, height: 1080, bitrate: 5000000 }, + { id: 1, width: 1280, height: 720, bitrate: 3000000 }, + ]); + }); + }); + + describe('audioTrack', () => { + it('should return current audio track as string', () => { + mockHlsInstance.audioTrack = 2; + expect(tech.audioTrack).toBe('2'); + }); + + it('should set audio track from string id', () => { + tech.audioTrack = '1'; + expect(mockHlsInstance.audioTrack).toBe(1); + }); + }); + + describe('audioTracks', () => { + it('should return mapped audio tracks', () => { + mockHlsInstance.audioTrack = 0; + mockHlsInstance.audioTracks = [ + { id: 0, name: 'English', lang: 'en' }, + { id: 1, name: 'Swedish', lang: 'sv' }, + ] as any; + + const tracks = tech.audioTracks; + expect(tracks).toEqual([ + { id: '0', label: 'English', language: 'en', enabled: true }, + { id: '1', label: 'Swedish', language: 'sv', enabled: false }, + ]); + }); + }); + + describe('textTrack', () => { + it('should return current subtitle track as string', () => { + mockHlsInstance.subtitleTrack = 1; + expect(tech.textTrack).toBe('1'); + }); + + it('should set subtitle track to -1 when given falsy value', () => { + tech.textTrack = null; + expect(mockHlsInstance.subtitleTrack).toBe(-1); + }); + + it('should set subtitle track from string', () => { + tech.textTrack = '2'; + expect(mockHlsInstance.subtitleTrack).toBe(2); + }); + }); + + describe('textTracks', () => { + it('should return mapped subtitle tracks', () => { + mockHlsInstance.subtitleTrack = 0; + mockHlsInstance.subtitleTracks = [ + { id: 0, name: 'English', lang: 'en' }, + { id: 1, name: 'Swedish', lang: 'sv' }, + ] as any; + + const tracks = tech.textTracks; + expect(tracks).toEqual([ + { id: '0', label: 'English', language: 'en', enabled: true }, + { id: '1', label: 'Swedish', language: 'sv', enabled: false }, + ]); + }); + }); + + describe('seekToLive()', () => { + it('should set currentTime to hls.liveSyncPosition', () => { + mockHlsInstance.liveSyncPosition = 500; + tech.seekToLive(); + // HlsJsTech.currentTime setter checks isSeekable — after setDefaultState it should be true + }); + }); + + describe('bitrate change', () => { + it('should emit BITRATE_CHANGE on level switch', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.BITRATE_CHANGE, handler); + + mockHlsInstance.levels = [ + { width: 1920, height: 1080, bitrate: 5000000 }, + { width: 1280, height: 720, bitrate: 3000000 }, + ] as any; + mockHlsInstance.currentLevel = 1; + + const onLevelSwitched = hlsListeners[Hls.Events.LEVEL_SWITCHED]; + onLevelSwitched('', {}); + + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + id: 1, + width: 1280, + height: 720, + bitrate: 3000000, + }) + ); + }); + }); + + describe('destroy()', () => { + it('should destroy hls instance', () => { + tech.destroy(); + expect(mockHlsInstance.destroy).toHaveBeenCalled(); + }); + }); + + describe('formatHlsError()', () => { + it('should format manifest load error with HTTP status', () => { + const result = formatHlsError({ + type: 'networkError', + details: 'manifestLoadError', + fatal: true, + response: { code: 404, text: 'Not Found' }, + }); + + expect(result.errorData).toEqual({ + category: 'networkError', + code: '404', + message: 'Not Found', + data: expect.any(Object), + }); + expect(result.fatal).toBe(true); + }); + + it('should format parsing error with reason', () => { + const result = formatHlsError({ + type: 'networkError', + details: 'manifestParsingError', + fatal: true, + reason: 'Malformed M3U8', + }); + + expect(result.errorData.message).toBe('Malformed M3U8'); + }); + + it('should format timeout error with default code', () => { + const result = formatHlsError({ + type: 'networkError', + details: 'manifestLoadTimeout', + fatal: true, + }); + + expect(result.errorData.code).toBe('-1'); + }); + + it('should handle null data gracefully', () => { + const result = formatHlsError(null); + expect(result.errorData).toEqual({ + category: 'unknown', + code: '-1', + message: '', + data: null, + }); + expect(result.fatal).toBe(false); + }); + }); +}); diff --git a/packages/core/src/tech/HlsJsTech.ts b/packages/core/src/tech/HlsJsTech.ts index c540fa6..986b2f6 100644 --- a/packages/core/src/tech/HlsJsTech.ts +++ b/packages/core/src/tech/HlsJsTech.ts @@ -2,6 +2,7 @@ import BaseTech, { IVideoLevel, PlaybackState } from './BaseTech'; import Hls from 'hls.js'; import { PlayerEvent } from '../util/constants'; import { IWebPlayerOptions } from '../WebPlayer'; +import { formatHlsError } from '../util/errors'; const DEFAULT_CONFIG = { capLevelOnFPSDrop: true, @@ -179,10 +180,7 @@ export default class HlsJsTech extends BaseTech { } protected onErrorEvent(event, data) { - const fatal = data?.fatal; - const errorData = this.errorFormat(data); - - this.emit(PlayerEvent.ERROR, { errorData, fatal }); + this.emit(PlayerEvent.ERROR, formatHlsError(data)); } get currentLevel() { @@ -279,43 +277,6 @@ export default class HlsJsTech extends BaseTech { this.currentTime = this.hls.liveSyncPosition; } - errorFormat(data) { - let errorData = { - category: data?.type, // optional, eg. NETWORK, DECODER, etc. - code: '-1', - message: '', // optional - data: data, // optional - }; - const errorDetails = data?.details; - switch (errorDetails) { - //All Fatal - case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: - case Hls.ErrorDetails.LEVEL_LOAD_ERROR: - case Hls.ErrorDetails.FRAG_LOAD_ERROR: //fatal = true || false - (errorData.code = `${data.response.code}`), - (errorData.message = data.response.text); - break; - case Hls.ErrorDetails.MANIFEST_PARSING_ERROR: - errorData.message = data.reason; - break; - case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT: - case Hls.ErrorDetails.FRAG_LOAD_TIMEOUT: //fatal = true || false - case Hls.ErrorDetails.KEY_LOAD_TIMEOUT: - break; - //Non Fatal - case Hls.ErrorDetails.AUDIO_TRACK_LOAD_ERROR: - case Hls.ErrorDetails.KEY_LOAD_ERROR: - (errorData.code = `${data.response.code}`), - (errorData.message = data.response.text); - break; - case Hls.ErrorDetails.LEVEL_LOAD_TIMEOUT: - case Hls.ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: - default: - } - - return errorData; - } - // Interstitial handlers /** @@ -509,12 +470,11 @@ export default class HlsJsTech extends BaseTech { } if (allTrackingUrls.start.length > 0) { - console.log('[Interstitials] Successfully extracted tracking URLs from asset list'); return allTrackingUrls; } } } catch (e) { - console.warn('[Interstitials] Failed to fetch tracking from asset list URL:', e); + // Failed to fetch tracking from asset list URL } return undefined; } @@ -585,7 +545,7 @@ export default class HlsJsTech extends BaseTech { } } } catch (e) { - console.warn('Failed to parse interstitial tracking data:', e); + // Failed to parse interstitial tracking data } return undefined; } diff --git a/packages/core/src/tech/ShakaTech.test.ts b/packages/core/src/tech/ShakaTech.test.ts index 4596a89..e64fa4f 100644 --- a/packages/core/src/tech/ShakaTech.test.ts +++ b/packages/core/src/tech/ShakaTech.test.ts @@ -1,303 +1,431 @@ -import ShakaTech from './ShakaTech'; import { PlayerEvent } from '../util/constants'; // Mock shaka-player -jest.mock('shaka-player', () => { - const mockShakaPlayer = { - configure: jest.fn(), - load: jest.fn().mockResolvedValue(undefined), - destroy: jest.fn().mockResolvedValue(undefined), - getTextTracks: jest.fn().mockReturnValue([]), - selectTextTrack: jest.fn(), - setTextTrackVisibility: jest.fn(), - isTextTrackVisible: jest.fn().mockReturnValue(false), - getVariantTracks: jest.fn().mockReturnValue([]), - getAudioLanguages: jest.fn().mockReturnValue([]), - selectVariantTrack: jest.fn(), - selectAudioLanguage: jest.fn(), - getConfiguration: jest.fn().mockReturnValue({}), - getManifest: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - }; - - return { - Player: jest.fn(() => mockShakaPlayer), +const mockShakaPlayer = { + configure: jest.fn(), + addEventListener: jest.fn(), + attach: jest.fn().mockResolvedValue(undefined), + load: jest.fn().mockResolvedValue(undefined), + destroy: jest.fn(), + isLive: jest.fn().mockReturnValue(false), + seekRange: jest.fn().mockReturnValue({ start: 0, end: 100 }), + getVariantTracks: jest.fn().mockReturnValue([]), + getTextTracks: jest.fn().mockReturnValue([]), + getAudioLanguages: jest.fn().mockReturnValue([]), + selectAudioLanguage: jest.fn(), + selectTextTrack: jest.fn(), + setTextTrackVisibility: jest.fn(), + isTextTrackVisible: jest.fn().mockReturnValue(false), + selectVariantTrack: jest.fn(), +}; + +jest.mock('shaka-player', () => ({ + __esModule: true, + default: { polyfill: { installAll: jest.fn(), }, - }; -}); - -describe('ShakaTech - Text Track (Subtitle) Functionality', () => { - let shakaTech: ShakaTech; - let mockVideoElement: HTMLVideoElement; - let mockShakaPlayer: any; - - beforeEach(() => { - // Create mock video element - mockVideoElement = document.createElement('video'); - - // Mock audioTracks to prevent BaseTech constructor errors - Object.defineProperty(mockVideoElement, 'audioTracks', { - value: { - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - length: 0 + Player: jest.fn().mockImplementation(() => mockShakaPlayer), + }, +})); + +// Mock mitt +jest.mock('mitt', () => ({ + __esModule: true, + default: () => { + const handlers = new Map>(); + return { + on(type: string, handler: Function) { + if (!handlers.has(type)) handlers.set(type, new Set()); + handlers.get(type)!.add(handler); }, - writable: true - }); + off(type: string, handler: Function) { + handlers.get(type)?.delete(handler); + }, + emit(type: string, data?: any) { + handlers.get(type)?.forEach((h) => h(data)); + }, + all: { clear() { handlers.clear(); } }, + }; + }, +})); + +import DashPlayer from './ShakaTech'; +// @ts-ignore - shaka-player types don't export as module, but our mock does +import shaka from 'shaka-player'; - // Create ShakaTech instance - shakaTech = new ShakaTech({ video: mockVideoElement }); +function createMockVideo(): HTMLVideoElement { + const listeners: Record = {}; + return { + muted: false, + volume: 1, + paused: true, + currentTime: 0, + duration: 100, + src: '', + seekable: { length: 0, end: jest.fn() }, + addEventListener(event: string, handler: Function) { + if (!listeners[event]) listeners[event] = []; + listeners[event].push(handler); + }, + removeEventListener: jest.fn(), + play: jest.fn().mockResolvedValue(undefined), + pause: jest.fn(), + load: jest.fn(), + _trigger(event: string, data?: any) { + (listeners[event] || []).forEach((h) => h(data)); + }, + _listeners: listeners, + } as unknown as HTMLVideoElement & { _trigger: Function; _listeners: Record }; +} - // Get the mocked shaka player instance - mockShakaPlayer = (shakaTech as any).shakaPlayer; +describe('ShakaTech (DashPlayer)', () => { + let video: ReturnType; + let tech: DashPlayer; - // Reset all mocks + beforeEach(() => { jest.clearAllMocks(); + video = createMockVideo(); + tech = new DashPlayer({ video } as any); }); afterEach(() => { - jest.restoreAllMocks(); + tech.destroy(); }); - describe('textTrack setter', () => { - it('should call setTextTrackVisibility(true) when enabling a text track', () => { - const mockTrack = { id: '0', language: 'en', label: 'English', active: false }; - mockShakaPlayer.getTextTracks.mockReturnValue([mockTrack]); - - shakaTech.textTrack = '0|English|en'; + describe('constructor', () => { + it('should call shaka.polyfill.installAll()', () => { + expect(shaka.polyfill.installAll).toHaveBeenCalled(); + }); - expect(mockShakaPlayer.setTextTrackVisibility).toHaveBeenCalledWith(true); + it('should create a shaka.Player instance', () => { + expect(shaka.Player).toHaveBeenCalled(); }); - it('should call selectTextTrack with the correct track', () => { - const mockTrack = { id: '0', language: 'en', label: 'English', active: false }; - mockShakaPlayer.getTextTracks.mockReturnValue([mockTrack]); + it('should have name "ShakaTech"', () => { + expect(tech.name).toBe('ShakaTech'); + }); - shakaTech.textTrack = '0|English|en'; // Use compound ID format + it('should configure ABR restrictToElementSize based on options', () => { + expect(mockShakaPlayer.configure).toHaveBeenCalledWith( + expect.objectContaining({ + abr: expect.objectContaining({ restrictToElementSize: true }), + }) + ); + }); - expect(mockShakaPlayer.selectTextTrack).toHaveBeenCalledWith(mockTrack); + it('should register event listeners on shakaPlayer', () => { + expect(mockShakaPlayer.addEventListener).toHaveBeenCalledWith( + 'variantchanged', + expect.any(Function) + ); + expect(mockShakaPlayer.addEventListener).toHaveBeenCalledWith( + 'adaptation', + expect.any(Function) + ); + expect(mockShakaPlayer.addEventListener).toHaveBeenCalledWith( + 'error', + expect.any(Function) + ); }); + }); - it('should call setTextTrackVisibility(false) when disabling text tracks', () => { - shakaTech.textTrack = null; + describe('load()', () => { + it('should attach and load the source', async () => { + await tech.load('http://example.com/stream.mpd'); + expect(mockShakaPlayer.attach).toHaveBeenCalledWith(video); + expect(mockShakaPlayer.load).toHaveBeenCalledWith('http://example.com/stream.mpd'); + }); - expect(mockShakaPlayer.setTextTrackVisibility).toHaveBeenCalledWith(false); + it('should emit ERROR on load failure', async () => { + const loadError = { + severity: 2, + category: 1, + code: 1001, + data: ['', 'Failed to fetch manifest'], + }; + mockShakaPlayer.load.mockRejectedValueOnce(loadError); + + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + await expect(tech.load('http://bad.url/stream.mpd')).rejects.toEqual(loadError); + + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + category: '1', + code: '1001', + message: 'Failed to fetch manifest', + }), + fatal: true, + }) + ); }); - it('CRITICAL REGRESSION TEST: should call onTextTrackChange when enabling a track', () => { - const mockTrack = { id: '0', language: 'en', label: 'English', active: false }; - mockShakaPlayer.getTextTracks.mockReturnValue([mockTrack]); + it('should set fatal=false for severity <= 1', async () => { + const loadError = { + severity: 1, + category: 2, + code: 2001, + data: ['', 'Recoverable error'], + }; + mockShakaPlayer.load.mockRejectedValueOnce(loadError); - const onTextTrackChangeSpy = jest.spyOn(shakaTech as any, 'onTextTrackChange'); + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); - shakaTech.textTrack = '0|English|en'; + await expect(tech.load('http://example.com/stream.mpd')).rejects.toEqual(loadError); - expect(onTextTrackChangeSpy).toHaveBeenCalled(); - expect(onTextTrackChangeSpy).toHaveBeenCalledTimes(1); + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ fatal: false }) + ); }); - it('CRITICAL REGRESSION TEST: should call onTextTrackChange when disabling tracks', () => { - const onTextTrackChangeSpy = jest.spyOn(shakaTech as any, 'onTextTrackChange'); - - shakaTech.textTrack = null; + it('should handle null error gracefully', async () => { + mockShakaPlayer.load.mockRejectedValueOnce(null); - expect(onTextTrackChangeSpy).toHaveBeenCalled(); - expect(onTextTrackChangeSpy).toHaveBeenCalledTimes(1); - }); + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); - it('CRITICAL REGRESSION TEST: should emit TEXT_TRACK_CHANGE event when enabling', (done) => { - const mockTrack = { id: '0', language: 'en', label: 'English', active: true }; - mockShakaPlayer.getTextTracks.mockReturnValue([mockTrack]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(true); + await expect(tech.load('http://example.com/stream.mpd')).rejects.toBeNull(); - shakaTech.on(PlayerEvent.TEXT_TRACK_CHANGE, (track) => { - expect(track).toBeDefined(); - done(); - }); - - shakaTech.textTrack = '0|English|en'; + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + category: 'unknown', + code: '-1', + message: 'Shaka error', + }), + fatal: true, + }) + ); }); + }); - it('CRITICAL REGRESSION TEST: should emit TEXT_TRACK_CHANGE event when disabling', (done) => { - // Mock that no tracks are active/visible when disabled - mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: false }, - ]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(false); - - shakaTech.on(PlayerEvent.TEXT_TRACK_CHANGE, (track) => { - expect(track).toBeUndefined(); - done(); + describe('onError (runtime error)', () => { + it('should emit ERROR on shaka error event', () => { + const errorHandler = jest.fn(); + tech.on(PlayerEvent.ERROR, errorHandler); + + // Find the 'error' listener registered on shakaPlayer + const errorListenerCall = mockShakaPlayer.addEventListener.mock.calls.find( + (call) => call[0] === 'error' + ); + expect(errorListenerCall).toBeTruthy(); + + const onError = errorListenerCall![1]; + onError({ + detail: { + severity: 2, + category: 1, + code: 1002, + data: ['', 'Network error'], + }, }); - shakaTech.textTrack = null; + expect(errorHandler).toHaveBeenCalledWith( + expect.objectContaining({ + errorData: expect.objectContaining({ + category: '1', + code: '1002', + message: 'Network error', + }), + fatal: true, + }) + ); }); + }); - it('should handle multiple text tracks correctly', () => { - const mockTracks = [ - { id: '0', language: 'en', label: 'English', active: false }, - { id: '1', language: 'es', label: 'Spanish', active: false }, - { id: '2', language: 'fr', label: 'French', active: false }, - ]; - mockShakaPlayer.getTextTracks.mockReturnValue(mockTracks); - - shakaTech.textTrack = '1|Spanish|es'; - - expect(mockShakaPlayer.selectTextTrack).toHaveBeenCalledWith(mockTracks[1]); + describe('isLive', () => { + it('should delegate to shakaPlayer.isLive()', () => { + mockShakaPlayer.isLive.mockReturnValue(true); + expect(tech.isLive).toBe(true); + expect(mockShakaPlayer.isLive).toHaveBeenCalled(); }); }); - describe('textTrack getter', () => { - it('should return null when no text track is active', () => { - mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: false }, - ]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(false); - - expect(shakaTech.textTrack).toBeNull(); + describe('duration', () => { + it('should return seekRange end - start', () => { + mockShakaPlayer.seekRange.mockReturnValue({ start: 10, end: 110 }); + expect(tech.duration).toBe(100); }); - it('should return track ID when a text track is active and visible', () => { - mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: true }, - ]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(true); - - expect(shakaTech.textTrack).toBe('0|English|en'); // Returns compound ID + it('should return 0 if seekRange is zero-length', () => { + mockShakaPlayer.seekRange.mockReturnValue({ start: 0, end: 0 }); + expect(tech.duration).toBe(0); }); + }); - it('should return null when track is active but not visible', () => { - mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: true }, + describe('audioTrack', () => { + it('should return active track language', () => { + mockShakaPlayer.getVariantTracks.mockReturnValue([ + { active: true, language: 'en' }, + { active: false, language: 'sv' }, ]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(false); - - expect(shakaTech.textTrack).toBeNull(); + expect(tech.audioTrack).toBe('en'); }); - }); - - describe('textTracks getter', () => { - it('should return an empty array when no text tracks are available', () => { - mockShakaPlayer.getTextTracks.mockReturnValue([]); - expect(shakaTech.textTracks).toEqual([]); + it('should set audio language', () => { + tech.audioTrack = 'sv'; + expect(mockShakaPlayer.selectAudioLanguage).toHaveBeenCalledWith('sv'); }); + }); - it('should return formatted text tracks', () => { - mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: false }, - { id: '1', language: 'es', label: 'Spanish', active: false }, + describe('audioTracks', () => { + it('should return mapped audio languages', () => { + mockShakaPlayer.getAudioLanguages.mockReturnValue(['en', 'sv']); + mockShakaPlayer.getVariantTracks.mockReturnValue([ + { active: true, language: 'en' }, ]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(false); - - const tracks = shakaTech.textTracks; + const tracks = tech.audioTracks; expect(tracks).toHaveLength(2); expect(tracks[0]).toEqual({ - id: '0', - label: 'English', + id: 'en', language: 'en', - enabled: false, + label: 'en', + enabled: true, }); expect(tracks[1]).toEqual({ - id: '1', - label: 'Spanish', - language: 'es', + id: 'sv', + language: 'sv', + label: 'sv', enabled: false, }); }); + }); - it('should mark the active track as enabled', () => { + describe('textTrack', () => { + it('should return null when no text track visible', () => { mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: true }, - { id: '1', language: 'es', label: 'Spanish', active: false }, + { active: true, id: '1', label: 'English', language: 'en' }, + ]); + mockShakaPlayer.isTextTrackVisible.mockReturnValue(false); + expect(tech.textTrack).toBeNull(); + }); + + it('should return track id when visible', () => { + mockShakaPlayer.getTextTracks.mockReturnValue([ + { active: true, id: '1', label: 'English', language: 'en' }, ]); mockShakaPlayer.isTextTrackVisible.mockReturnValue(true); + expect(tech.textTrack).toBe('1|English|en'); + }); - const tracks = shakaTech.textTracks; + it('should set text track visibility and select track', () => { + mockShakaPlayer.getTextTracks.mockReturnValue([ + { id: '1', label: 'English', language: 'en' }, + { id: '2', label: 'Swedish', language: 'sv' }, + ]); + tech.textTrack = '2|Swedish|sv'; + expect(mockShakaPlayer.setTextTrackVisibility).toHaveBeenCalledWith(true); + expect(mockShakaPlayer.selectTextTrack).toHaveBeenCalledWith( + expect.objectContaining({ id: '2', label: 'Swedish', language: 'sv' }) + ); + }); - expect(tracks[0].enabled).toBe(true); - expect(tracks[1].enabled).toBe(false); + it('should hide text tracks when set to null/falsy', () => { + tech.textTrack = null; + expect(mockShakaPlayer.setTextTrackVisibility).toHaveBeenCalledWith(false); }); + }); - it('should filter duplicate tracks with same language and label', () => { + describe('textTracks', () => { + it('should return deduplicated text tracks', () => { mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: false }, - { id: '1', language: 'en', label: 'English', active: false }, - { id: '2', language: 'es', label: 'Spanish', active: false }, + { id: '1', label: 'English', language: 'en', active: false }, + { id: '2', label: 'English', language: 'en', active: false }, // duplicate + { id: '3', label: 'Swedish', language: 'sv', active: false }, ]); mockShakaPlayer.isTextTrackVisible.mockReturnValue(false); - const tracks = shakaTech.textTracks; - + const tracks = tech.textTracks; expect(tracks).toHaveLength(2); - expect(tracks.filter(t => t.language === 'en')).toHaveLength(1); + expect(tracks.map((t) => t.language)).toEqual(['en', 'sv']); }); }); - describe('Event propagation', () => { - it('CRITICAL: should emit TEXT_TRACK_CHANGE event with correct track data', (done) => { - const mockTrack = { id: '0', language: 'en', label: 'English', active: true }; - mockShakaPlayer.getTextTracks.mockReturnValue([mockTrack]); - mockShakaPlayer.isTextTrackVisible.mockReturnValue(true); - - shakaTech.on(PlayerEvent.TEXT_TRACK_CHANGE, (track) => { - // Verify the emitted track data is correct - expect(track).toBeDefined(); - expect(track.id).toBe('0'); // ITrack.id is the simple ID, not compound - expect(track.language).toBe('en'); - expect(track.label).toBe('English'); - expect(track.enabled).toBe(true); - done(); + describe('currentLevel', () => { + it('should return current active variant track', () => { + mockShakaPlayer.getVariantTracks.mockReturnValue([ + { id: 1, width: 1920, height: 1080, bandwidth: 5000000, active: true }, + { id: 2, width: 1280, height: 720, bandwidth: 3000000, active: false }, + ]); + const level = tech.currentLevel; + expect(level).toEqual({ + id: 1, + width: 1920, + height: 1080, + bitrate: 5000000, }); + }); - shakaTech.textTrack = '0|English|en'; + it('should enable ABR when set to null', () => { + tech.currentLevel = null; + expect(mockShakaPlayer.configure).toHaveBeenCalledWith( + expect.objectContaining({ abr: { enabled: true } }) + ); }); - it('should handle rapid text track changes without errors', () => { - const mockTracks = [ - { id: '0', language: 'en', label: 'English', active: false }, - { id: '1', language: 'es', label: 'Spanish', active: false }, - ]; - mockShakaPlayer.getTextTracks.mockReturnValue(mockTracks); - - // Rapidly change tracks - expect(() => { - for (let i = 0; i < 10; i++) { - shakaTech.textTrack = String(i % 2); - shakaTech.textTrack = null; - } - }).not.toThrow(); + it('should disable ABR and select specific track', () => { + const level = { id: 2, width: 1280, height: 720, bitrate: 3000000 }; + mockShakaPlayer.getVariantTracks.mockReturnValue([ + { id: 1, width: 1920, height: 1080, bandwidth: 5000000, active: true }, + { id: 2, width: 1280, height: 720, bandwidth: 3000000, active: false }, + ]); + tech.currentLevel = level; + expect(mockShakaPlayer.configure).toHaveBeenCalledWith( + expect.objectContaining({ abr: { enabled: false } }) + ); + expect(mockShakaPlayer.selectVariantTrack).toHaveBeenCalled(); }); }); - describe('Edge cases', () => { - it('should handle setting text track when track does not exist', () => { - mockShakaPlayer.getTextTracks.mockReturnValue([ - { id: '0', language: 'en', label: 'English', active: false }, + describe('getVideoLevels()', () => { + it('should return mapped variant tracks', () => { + mockShakaPlayer.getVariantTracks.mockReturnValue([ + { id: 1, width: 1920, height: 1080, bandwidth: 5000000 }, + { id: 2, width: 1280, height: 720, bandwidth: 3000000 }, ]); + const levels = tech.getVideoLevels(); + expect(levels).toEqual([ + { id: 1, width: 1920, height: 1080, bitrate: 5000000 }, + { id: 2, width: 1280, height: 720, bitrate: 3000000 }, + ]); + }); + }); - // Try to set a track ID that doesn't exist - expect(() => { - shakaTech.textTrack = '99'; - }).not.toThrow(); - - // Should NOT call selectTextTrack when track is not found - expect(mockShakaPlayer.selectTextTrack).not.toHaveBeenCalled(); - - // But should still call onTextTrackChange and set visibility - expect(mockShakaPlayer.setTextTrackVisibility).toHaveBeenCalledWith(true); + describe('destroy()', () => { + it('should destroy shaka player', () => { + tech.destroy(); + expect(mockShakaPlayer.destroy).toHaveBeenCalled(); }); + }); - it('should handle undefined text track value', () => { - expect(() => { - shakaTech.textTrack = undefined as any; - }).not.toThrow(); + describe('bitrate change', () => { + it('should emit BITRATE_CHANGE on adaptation', () => { + const handler = jest.fn(); + tech.on(PlayerEvent.BITRATE_CHANGE, handler); - expect(mockShakaPlayer.setTextTrackVisibility).toHaveBeenCalledWith(false); + mockShakaPlayer.getVariantTracks.mockReturnValue([ + { active: true, type: 'variant', bandwidth: 5000000, width: 1920, height: 1080 }, + ]); + + // Find the 'adaptation' listener + const adaptationCall = mockShakaPlayer.addEventListener.mock.calls.find( + (call) => call[0] === 'adaptation' + ); + expect(adaptationCall).toBeTruthy(); + adaptationCall![1](); + + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + bitrate: 5000000, + width: 1920, + height: 1080, + }) + ); }); }); }); diff --git a/packages/core/src/tech/ShakaTech.ts b/packages/core/src/tech/ShakaTech.ts index f06df61..2d44c7b 100644 --- a/packages/core/src/tech/ShakaTech.ts +++ b/packages/core/src/tech/ShakaTech.ts @@ -3,6 +3,7 @@ import shaka from 'shaka-player'; import { IWebPlayerOptions } from '../WebPlayer'; import BaseTech, { IVideoLevel, ITrack, getTextTrackId } from './BaseTech'; import { PlayerEvent } from '../util/constants'; +import { formatShakaError } from '../util/errors'; export default class DashPlayer extends BaseTech { public name = "ShakaTech"; @@ -40,18 +41,7 @@ export default class DashPlayer extends BaseTech { } private handleLoadError(error: any): void { - const errorDetails = error || {}; - const severity = errorDetails.severity ?? 2; - const fatal = severity > 1; - - const errorData = { - category: errorDetails.category?.toString() ?? 'unknown', - code: errorDetails.code?.toString() ?? 'unknown', - message: errorDetails.data?.[1]?.toString() ?? 'Load failed', - data: errorDetails.data ?? [] - }; - - this.emit(PlayerEvent.ERROR, { errorData, fatal }); + this.emit(PlayerEvent.ERROR, formatShakaError(error)); } protected onBitrateChange() { @@ -73,16 +63,7 @@ export default class DashPlayer extends BaseTech { protected onError(data) { const errorDetails = data?.detail; - console.log(errorDetails); - const fatal = errorDetails.severity > 1 ? true : false; - - let errorData = { - category: errorDetails.category.toString(), - code: errorDetails.code.toString(), - message: errorDetails.data[1].toString(), - data: errorDetails.data - }; - this.emit(PlayerEvent.ERROR, { errorData, fatal }); + this.emit(PlayerEvent.ERROR, formatShakaError(errorDetails)); } get isLive() { diff --git a/packages/core/src/tech/WHEPTech.ts b/packages/core/src/tech/WHEPTech.ts index 8d741cf..713fa2b 100644 --- a/packages/core/src/tech/WHEPTech.ts +++ b/packages/core/src/tech/WHEPTech.ts @@ -31,6 +31,9 @@ export default class WHEPTech extends BaseTech { destroy() { this.player.destroy(); + this.video.srcObject = null; + this.video.removeAttribute("src"); + this.video.load(); super.destroy(); } } diff --git a/packages/core/src/tech/WHPPTech.ts b/packages/core/src/tech/WHPPTech.ts index 5c66fd1..3ca1f94 100644 --- a/packages/core/src/tech/WHPPTech.ts +++ b/packages/core/src/tech/WHPPTech.ts @@ -31,6 +31,9 @@ export default class WHPPTech extends BaseTech { destroy() { this.player.destroy(); + this.video.srcObject = null; + this.video.removeAttribute("src"); + this.video.load(); super.destroy(); } } diff --git a/packages/core/src/tech/WebRTCTech.ts b/packages/core/src/tech/WebRTCTech.ts index 0a4f828..6b4e92b 100644 --- a/packages/core/src/tech/WebRTCTech.ts +++ b/packages/core/src/tech/WebRTCTech.ts @@ -32,6 +32,9 @@ export default class WebRTCTech extends BaseTech { destroy() { this.player.destroy(); + this.video.srcObject = null; + this.video.removeAttribute("src"); + this.video.load(); super.destroy(); } } diff --git a/packages/core/src/util/errors.test.ts b/packages/core/src/util/errors.test.ts new file mode 100644 index 0000000..19d0894 --- /dev/null +++ b/packages/core/src/util/errors.test.ts @@ -0,0 +1,215 @@ +import { + formatPlayerError, + formatShakaError, + formatHlsError, + formatDashError, + IPlayerError, +} from './errors'; + +describe('errors utility', () => { + describe('formatPlayerError()', () => { + it('should create error with all fields', () => { + const error = formatPlayerError({ + category: 'NETWORK', + code: 404, + message: 'Not Found', + data: { url: 'http://example.com' }, + fatal: true, + }); + + expect(error).toEqual({ + errorData: { + category: 'NETWORK', + code: '404', + message: 'Not Found', + data: { url: 'http://example.com' }, + }, + fatal: true, + }); + }); + + it('should use defaults for missing fields', () => { + const error = formatPlayerError({}); + + expect(error).toEqual({ + errorData: { + category: 'unknown', + code: '-1', + message: '', + data: null, + }, + fatal: false, + }); + }); + + it('should convert numeric code to string', () => { + const error = formatPlayerError({ code: 503 }); + expect(error.errorData.code).toBe('503'); + }); + + it('should convert numeric category to string', () => { + const error = formatPlayerError({ category: '3' }); + expect(error.errorData.category).toBe('3'); + }); + }); + + describe('formatShakaError()', () => { + it('should format a Shaka error with all fields', () => { + const error = formatShakaError({ + severity: 2, + category: 1, + code: 1001, + data: ['', 'Failed to fetch manifest'], + }); + + expect(error).toEqual({ + errorData: { + category: '1', + code: '1001', + message: 'Failed to fetch manifest', + data: ['', 'Failed to fetch manifest'], + }, + fatal: true, + }); + }); + + it('should set fatal=false for severity <= 1', () => { + const error = formatShakaError({ severity: 1, category: 2, code: 2001, data: [] }); + expect(error.fatal).toBe(false); + }); + + it('should handle null error', () => { + const error = formatShakaError(null); + expect(error.errorData.category).toBe('unknown'); + expect(error.errorData.code).toBe('-1'); + expect(error.errorData.message).toBe('Shaka error'); + expect(error.fatal).toBe(true); // default severity is 2 + }); + + it('should handle empty error object', () => { + const error = formatShakaError({}); + expect(error.errorData.category).toBe('unknown'); + expect(error.errorData.code).toBe('-1'); + expect(error.fatal).toBe(true); + }); + }); + + describe('formatHlsError()', () => { + it('should format an HLS error with HTTP response', () => { + const error = formatHlsError({ + type: 'networkError', + details: 'manifestLoadError', + fatal: true, + response: { code: 404, text: 'Not Found' }, + }); + + expect(error).toEqual({ + errorData: { + category: 'networkError', + code: '404', + message: 'Not Found', + data: expect.any(Object), + }, + fatal: true, + }); + }); + + it('should format parsing error with reason', () => { + const error = formatHlsError({ + type: 'networkError', + details: 'manifestParsingError', + fatal: true, + reason: 'Malformed M3U8', + }); + + expect(error.errorData.message).toBe('Malformed M3U8'); + }); + + it('should fall back to details as message', () => { + const error = formatHlsError({ + type: 'mediaError', + details: 'bufferStalledError', + fatal: false, + }); + + expect(error.errorData.message).toBe('bufferStalledError'); + expect(error.fatal).toBe(false); + }); + + it('should handle null data', () => { + const error = formatHlsError(null); + expect(error.errorData.category).toBe('unknown'); + expect(error.errorData.code).toBe('-1'); + expect(error.fatal).toBe(false); + }); + + it('should set fatal from data.fatal', () => { + const error = formatHlsError({ fatal: true, type: 'networkError' }); + expect(error.fatal).toBe(true); + }); + }); + + describe('formatDashError()', () => { + it('should format a dash.js error event', () => { + const error = formatDashError({ + error: { code: 25, message: 'Download error' }, + }); + + expect(error).toEqual({ + errorData: { + category: 'MEDIA', + code: '25', + message: 'Download error', + data: expect.any(Object), + }, + fatal: true, + }); + }); + + it('should handle direct error object', () => { + const error = formatDashError({ + code: 10, + message: 'Manifest parse error', + }); + + expect(error.errorData.code).toBe('10'); + expect(error.errorData.message).toBe('Manifest parse error'); + }); + + it('should handle null event', () => { + const error = formatDashError(null); + expect(error.errorData.category).toBe('MEDIA'); + expect(error.errorData.message).toBe('DASH playback error'); + expect(error.fatal).toBe(true); + }); + + it('should always be fatal', () => { + const error = formatDashError({ error: { code: 1 } }); + expect(error.fatal).toBe(true); + }); + }); + + describe('consistent error shape', () => { + it('all formatters should produce the same interface shape', () => { + const errors: IPlayerError[] = [ + formatShakaError({ severity: 2, category: 1, code: 1001, data: ['', 'test'] }), + formatHlsError({ type: 'networkError', details: 'test', fatal: true }), + formatDashError({ error: { code: 1, message: 'test' } }), + formatPlayerError({ category: 'test', code: '1', message: 'test', fatal: true }), + ]; + + for (const error of errors) { + expect(error).toHaveProperty('errorData'); + expect(error).toHaveProperty('fatal'); + expect(error.errorData).toHaveProperty('category'); + expect(error.errorData).toHaveProperty('code'); + expect(error.errorData).toHaveProperty('message'); + expect(error.errorData).toHaveProperty('data'); + expect(typeof error.errorData.category).toBe('string'); + expect(typeof error.errorData.code).toBe('string'); + expect(typeof error.errorData.message).toBe('string'); + expect(typeof error.fatal).toBe('boolean'); + } + }); + }); +}); diff --git a/packages/core/src/util/errors.ts b/packages/core/src/util/errors.ts new file mode 100644 index 0000000..ca40679 --- /dev/null +++ b/packages/core/src/util/errors.ts @@ -0,0 +1,115 @@ +/** + * Shared error formatting utility for all tech adapters. + * Provides a consistent error shape across ShakaTech, HlsJsTech, and DashJsTech. + * + * Standard error shape emitted via PlayerEvent.ERROR: + * { + * errorData: { category: string, code: string, message: string, data: any }, + * fatal: boolean + * } + */ + +export interface IPlayerErrorData { + /** Error category (e.g., 'NETWORK', 'MEDIA', 'MANIFEST', 'unknown') */ + category: string; + /** Error code as string (e.g., HTTP status code, library error code, or '-1' for unknown) */ + code: string; + /** Human-readable error message */ + message: string; + /** Raw error data from the underlying library */ + data: any; +} + +export interface IPlayerError { + errorData: IPlayerErrorData; + fatal: boolean; +} + +/** + * Creates a standardized player error object from raw error data. + * All tech adapters should use this to emit PlayerEvent.ERROR. + */ +export function formatPlayerError(opts: { + category?: string; + code?: string | number; + message?: string; + data?: any; + fatal?: boolean; +}): IPlayerError { + return { + errorData: { + category: opts.category?.toString() ?? 'unknown', + code: opts.code?.toString() ?? '-1', + message: opts.message?.toString() ?? '', + data: opts.data ?? null, + }, + fatal: opts.fatal ?? false, + }; +} + +/** + * Format a Shaka Player error into the standard error shape. + * Shaka errors have: severity, category, code, data[] + */ +export function formatShakaError(error: any): IPlayerError { + const errorDetails = error || {}; + const severity = errorDetails.severity ?? 2; + const fatal = severity > 1; + + return formatPlayerError({ + category: errorDetails.category?.toString(), + code: errorDetails.code?.toString(), + message: errorDetails.data?.[1]?.toString() ?? 'Shaka error', + data: errorDetails.data ?? [], + fatal, + }); +} + +/** + * Format an HLS.js error into the standard error shape. + * HLS.js errors have: type (category), details, fatal, response? { code, text } + */ +export function formatHlsError(data: any): IPlayerError { + const errorData: IPlayerErrorData = { + category: data?.type?.toString() ?? 'unknown', + code: '-1', + message: '', + data: data ?? null, + }; + + const details = data?.details; + // HTTP errors with response codes + if (data?.response?.code !== undefined) { + errorData.code = `${data.response.code}`; + errorData.message = data.response?.text ?? ''; + } else if (data?.reason) { + // Parsing errors with reason + errorData.message = data.reason; + } + + // If we still don't have a message, use the details string + if (!errorData.message && details) { + errorData.message = details.toString(); + } + + return { + errorData, + fatal: data?.fatal ?? false, + }; +} + +/** + * Format a dash.js error into the standard error shape. + * dash.js errors have: error.code, error.message, event type + */ +export function formatDashError(event: any): IPlayerError { + const error = event?.error || event; + + return formatPlayerError({ + category: 'MEDIA', + code: error?.code?.toString(), + message: error?.message?.toString() ?? 'DASH playback error', + data: event ?? null, + fatal: true, + }); +}