diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..fe67601 --- /dev/null +++ b/.env.template @@ -0,0 +1,43 @@ +# ── Required ──────────────────────────────────────────────────────────────── +# Full URL of the BigBlueButton server API (must end with /bigbluebutton/) +BBB_URL="https://your.bbb.server/bigbluebutton/" + +# Shared secret of the BigBlueButton server +BBB_SECRET="yoursupersecretkey" + +# ── Optional ──────────────────────────────────────────────────────────────── +# Direct URL to the plugin manifest (skips the server-side availability check). +# Use this when the plugin is not deployed to the default BBB plugins path. +# Example: PICK_RANDOM_USER_PLUGIN_URL="https://your.bbb.server/plugins/pick-random-user-plugin/dist/manifest.json" +PICK_RANDOM_USER_PLUGIN_URL="" + +# Name of the local Docker container running BigBlueButton (only needed when +# deploying the built plugin into a local container with the copy script). +LOCAL_CONTAINER_NAME="bbb-local-container" + +# Multiply all Playwright timeouts by this factor. +# Defaults to 2 in CI and 1 locally when not set. +TIMEOUT_MULTIPLIER=1 + +# Set to "true" to enable CI mode (1 worker, retries, blob reporter). +CI="false" + +# ── S3-compatible storage (publish-plugin-to-s3.sh) ───────────────────────── +# Access key ID for your S3-compatible bucket. +S3_ACCESS_KEY="your-access-key" + +# Secret access key. +S3_SECRET_KEY="your-secret-key" + +# Full endpoint URL of your S3-compatible provider. +# Examples: +# Digital Ocean Spaces – https://nyc3.digitaloceanspaces.com +# AWS S3 – https://s3.amazonaws.com +# MinIO – https://your.minio.host +S3_ENDPOINT_URL="https://nyc3.digitaloceanspaces.com" + +# Name of the bucket. +S3_BUCKET="your-bucket-name" + +# Path prefix inside the bucket. +S3_PATH="path/to/plugin/dist" diff --git a/.github/actions/e2e-test/action.yml b/.github/actions/e2e-test/action.yml new file mode 100644 index 0000000..9e17895 --- /dev/null +++ b/.github/actions/e2e-test/action.yml @@ -0,0 +1,92 @@ +name: Build, publish & run E2E tests +description: > + Installs dependencies, builds the plugin, publishes the dist to S3, + and runs the full Playwright E2E suite against a live BBB server. + +inputs: + s3_access_key: + description: 'S3 access key ID' + required: true + s3_secret_key: + description: 'S3 secret access key' + required: true + s3_endpoint_url: + description: 'S3 endpoint URL' + required: true + s3_bucket: + description: 'S3 bucket name' + required: true + s3_path: + description: 'Path prefix inside the S3 bucket (computed by the caller)' + required: true + bbb_url: + description: 'BigBlueButton server API URL (must end with /bigbluebutton/)' + required: true + bbb_secret: + description: 'BigBlueButton server shared secret' + required: true + +runs: + using: composite + steps: + # ── Step 1: Build ────────────────────────────────────────────────────────── + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + shell: bash + run: npm ci + + - name: Build plugin + shell: bash + run: npm run build-bundle + + # ── Step 2: Publish to S3 ────────────────────────────────────────────────── + + - name: Publish plugin to S3 + shell: bash + env: + S3_ACCESS_KEY: ${{ inputs.s3_access_key }} + S3_SECRET_KEY: ${{ inputs.s3_secret_key }} + S3_ENDPOINT_URL: ${{ inputs.s3_endpoint_url }} + S3_BUCKET: ${{ inputs.s3_bucket }} + S3_PATH: ${{ inputs.s3_path }} + run: bash scripts/publish-plugin-to-s3.sh + + # ── Step 3: Run tests ────────────────────────────────────────────────────── + + - name: Install Playwright browsers + shell: bash + run: npx playwright install chromium --with-deps + + - name: Run E2E tests + shell: bash + env: + CI: 'true' + BBB_URL: ${{ inputs.bbb_url }} + BBB_SECRET: ${{ inputs.bbb_secret }} + PICK_RANDOM_USER_PLUGIN_URL: ${{ inputs.s3_endpoint_url }}/${{ inputs.s3_bucket }}/${{ inputs.s3_path }}/manifest.json + TIMEOUT_MULTIPLIER: '2' + run: npm run test-chromium-ci + + # ── Artifacts ────────────────────────────────────────────────────────────── + + - name: Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Upload blob report + if: always() + uses: actions/upload-artifact@v4 + with: + name: blob-report + path: blob-report/ + retention-days: 10 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..7943afb --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,47 @@ +name: E2E tests - run on PR + +on: + workflow_dispatch: + inputs: + pr_number: + description: 'Pull request number to test' + required: true + type: string + +permissions: + contents: read + pull-requests: read + +jobs: + e2e-tests: + name: 'Build, publish & test PR #${{ github.event.inputs.pr_number }}' + runs-on: ubuntu-22.04 + + steps: + - name: Resolve PR metadata + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + PR_JSON=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.inputs.pr_number }}) + echo "PR_HEAD_REPO=$(echo "$PR_JSON" | jq -r '.head.repo.full_name')" >> $GITHUB_ENV + echo "PR_HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref')" >> $GITHUB_ENV + echo "S3_PATH=plugins/ci/pick-random-user-plugin/pr-${{ github.event.inputs.pr_number }}/dist" >> $GITHUB_ENV + + - name: Checkout PR code + uses: actions/checkout@v4 + with: + repository: ${{ env.PR_HEAD_REPO }} + ref: ${{ env.PR_HEAD_REF }} + fetch-depth: 1 + + - name: Build, publish & run E2E tests + uses: ./.github/actions/e2e-test + with: + s3_access_key: ${{ secrets.S3_ACCESS_KEY }} + s3_secret_key: ${{ secrets.S3_SECRET_KEY }} + s3_endpoint_url: ${{ secrets.S3_ENDPOINT_URL }} + s3_bucket: ${{ secrets.S3_BUCKET }} + s3_path: ${{ env.S3_PATH }} + bbb_url: ${{ secrets.BBB_URL }} + bbb_secret: ${{ secrets.BBB_SECRET }} diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index 09ffeff..3bb10cd 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -9,8 +9,37 @@ on: default: 'patch' jobs: + e2e-tests: + name: E2E tests (pre-tag) + runs-on: ubuntu-22.04 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Compute S3 path + run: echo "S3_PATH=plugins/ci/pick-random-user-plugin/release-staging/dist" >> $GITHUB_ENV + + - name: Build, publish & run E2E tests + uses: ./.github/actions/e2e-test + with: + s3_access_key: ${{ secrets.S3_ACCESS_KEY }} + s3_secret_key: ${{ secrets.S3_SECRET_KEY }} + s3_endpoint_url: ${{ secrets.S3_ENDPOINT_URL }} + s3_bucket: ${{ secrets.S3_BUCKET }} + s3_path: ${{ env.S3_PATH }} + bbb_url: ${{ secrets.BBB_URL }} + bbb_secret: ${{ secrets.BBB_SECRET }} + bump-version: + needs: e2e-tests runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout repository @@ -28,7 +57,7 @@ jobs: user_email="github-actions@github.com" fi echo "user_email=$user_email" >> $GITHUB_ENV - + - name: Configure Git with the triggering user's info run: | git config user.name "${{ github.actor }}" diff --git a/.gitignore b/.gitignore index ea80a77..0e33e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ debian/**/copyright debian/**/changelog.gz debian/**/md5sums debian/**/var/www/* +memory +playwright-report +test-results +.env diff --git a/package-lock.json b/package-lock.json index 806f3c2..d4ec91d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,16 +27,21 @@ "@babel/core": "^7.21.8", "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", + "@playwright/test": "^1.51.1", "@types/node": "^20.4.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-modal": "^3.16.0", + "@types/sha.js": "^2.4.4", "@types/styled-components": "^5.1.26", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", + "axios": "^1.8.4", "babel-loader": "^9.1.2", "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.7.4", + "dotenv": "^16.4.7", "eslint": "^8.45.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -48,13 +53,15 @@ "lint-staged": "11.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "sha.js": "^2.4.11", "style-loader": "^3.3.3", "ts-loader": "^9.4.3", "typescript": "^5.1.6", "watch": "^1.0.2", "webpack": "^5.95.0", "webpack-cli": "^5.1.1", - "webpack-dev-server": "^4.15.1" + "webpack-dev-server": "^4.15.1", + "xml2js": "^0.6.2" } }, "node_modules/@ampproject/remapping": { @@ -1840,9 +1847,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -1866,11 +1873,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2014,9 +2020,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -2024,11 +2030,10 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2091,11 +2096,10 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2162,6 +2166,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2312,12 +2331,31 @@ "@types/node": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.21", @@ -2529,6 +2567,15 @@ "@types/send": "*" } }, + "node_modules/@types/sha.js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", + "integrity": "sha512-Qukd+D6S2Hm0wLVt2Vh+/eWBIoUt+wF8jWjBsG4F8EFQRwKtYvtXCPcNl2OEUQ1R+eTr3xuSaBYUyM3WD1x/Qw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -2561,6 +2608,15 @@ "@types/node": "*" } }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -2806,163 +2862,148 @@ "license": "ISC" }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "license": "MIT" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -3065,15 +3106,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -3090,11 +3129,10 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3102,14 +3140,16 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/acorn-jsx": { @@ -3137,11 +3177,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3172,11 +3211,10 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3195,16 +3233,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3490,6 +3518,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3515,6 +3549,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3614,6 +3659,17 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -3649,24 +3705,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, - "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -3683,6 +3738,26 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3690,6 +3765,15 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -3702,9 +3786,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" @@ -3735,9 +3819,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -3752,12 +3836,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3770,8 +3854,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/bytes": { "version": "3.1.2", @@ -3783,16 +3866,41 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "license": "MIT", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3821,9 +3929,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001674", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz", - "integrity": "sha512-jOsKlZVRnzfhLojb+Ykb+gyUSp9Xb57So+fAiFlLzzTKpqg8xxSav0e40c8/4F/v9N8QSvrRRaLeVzQbLqomYw==", + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "funding": [ { "type": "opencollective", @@ -3837,8 +3945,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -3990,6 +4097,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -4520,6 +4639,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4603,6 +4731,31 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4611,10 +4764,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.49", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz", - "integrity": "sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==", - "license": "ISC" + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -4634,14 +4786,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -4746,13 +4897,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -4813,18 +4960,15 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true, - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -4833,15 +4977,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4879,7 +5023,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -5088,9 +5231,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5121,11 +5264,10 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5174,9 +5316,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5184,11 +5326,10 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5243,9 +5384,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5266,11 +5407,10 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5355,9 +5495,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5381,11 +5521,10 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5538,39 +5677,39 @@ "license": "BSD-3-Clause" }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -5813,16 +5952,15 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "license": "ISC" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -5830,7 +5968,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -5841,12 +5978,33 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "license": "MIT", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, "dependencies": { - "is-callable": "^1.1.3" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/forwarded": { @@ -5944,16 +6102,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5969,6 +6131,18 @@ "dev": true, "license": "ISC" }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -6039,13 +6213,12 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" + "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -6053,11 +6226,10 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6113,12 +6285,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6204,6 +6375,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6213,10 +6385,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -6421,7 +6592,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -7068,13 +7238,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -7182,7 +7351,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7197,7 +7365,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7215,11 +7382,10 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7455,13 +7621,16 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loaders.css": { @@ -7487,10 +7656,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -7615,6 +7783,14 @@ "lz-string": "bin/bin.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7838,20 +8014,18 @@ "license": "MIT" }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "license": "MIT" + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -7886,10 +8060,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "license": "MIT", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -8284,9 +8457,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true }, "node_modules/path-type": { @@ -8306,10 +8479,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "engines": { "node": ">=8.6" }, @@ -8421,6 +8593,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -8661,6 +8877,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8672,13 +8897,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "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": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -8729,17 +8953,45 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, - "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -9264,8 +9516,16 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.23.2", @@ -9277,11 +9537,10 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9289,7 +9548,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -9297,11 +9556,10 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9573,6 +9831,26 @@ "dev": true, "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -9635,15 +9913,65 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -9721,7 +10049,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -9732,7 +10059,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -10102,24 +10428,26 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -10131,17 +10459,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -10165,31 +10491,11 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/text-table": { "version": "0.2.0", @@ -10218,6 +10524,20 @@ "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==", "license": "MIT" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10373,15 +10693,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -10554,9 +10873,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -10571,10 +10890,9 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -10672,11 +10990,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -10696,35 +11013,36 @@ } }, "node_modules/webpack": { - "version": "5.95.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", - "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "version": "5.106.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz", + "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -10919,11 +11237,10 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -10952,25 +11269,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -11074,15 +11372,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "license": "MIT", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -11156,6 +11455,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -11163,11 +11484,10 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, - "license": "ISC", "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index 4ce1f46..9cd40ae 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "build:watch": "rm -rf dist && tsc -w --module CommonJS", "lint": "eslint ./src/*", "lint:fix": "npm run lint -- --fix", - "lint:watch": "watch 'yarn lint'" + "lint:watch": "watch 'yarn lint'", + "publish-plugin:dev": "bash scripts/publish-plugin-to-container.sh", + "test": "npx playwright test", + "test-chromium-ci": "export CI='true' && npx playwright test --project=chromium" }, "browserslist": { "production": [ @@ -40,6 +43,13 @@ ] }, "devDependencies": { + "@types/xml2js": "^0.4.14", + "@playwright/test": "^1.51.1", + "@types/sha.js": "^2.4.4", + "sha.js": "^2.4.11", + "dotenv": "^16.4.7", + "axios": "^1.8.4", + "xml2js": "^0.6.2", "@babel/core": "^7.21.8", "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..49bf569 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from '@playwright/test'; +import { CI, ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from './tests/core/constants'; +import { server } from './tests/core/parameters'; + +export default defineConfig({ + testDir: process.cwd(), + workers: CI ? 1 : undefined, + retries: CI ? 1 : 0, + fullyParallel: true, + forbidOnly: CI, + reporter: CI + ? [['list'], ['blob']] + : [['list'], ['html', { open: 'never' }]], + use: { + baseURL: server, + headless: true, + trace: 'on', + screenshot: 'on', + video: CI ? 'retain-on-failure' : 'on', + actionTimeout: ELEMENT_WAIT_LONGER_TIME, + viewport: { width: 1280, height: 720 }, + launchOptions: { + slowMo: 0, + }, + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + launchOptions: { + args: [ + '--no-sandbox', + '--ignore-certificate-errors', + '--use-fake-ui-for-media-stream', + '--use-fake-device-for-media-stream', + '--allow-file-access-from-files', + ], + }, + }, + }, + ], + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.05, + }, + toMatchSnapshot: { + maxDiffPixelRatio: 0.05, + }, + timeout: ELEMENT_WAIT_TIME, + }, +}); diff --git a/scripts/publish-plugin-to-container.sh b/scripts/publish-plugin-to-container.sh new file mode 100755 index 0000000..4ad18f2 --- /dev/null +++ b/scripts/publish-plugin-to-container.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Copies the built plugin (dist/) into the running BBB Docker container. +# Usage: ./scripts/publish-plugin-to-container.sh [containerName] +# +# containerName defaults to $LOCAL_CONTAINER_NAME from .env when omitted. + +set -e + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PLUGIN_ROOT="$SCRIPT_DIR/.." + +# Load .env from the plugin root +ENV_FILE="$PLUGIN_ROOT/.env" +if [ -f "$ENV_FILE" ]; then + set -o allexport + source "$ENV_FILE" + set +o allexport +fi + +PLUGIN_NAME="pick-random-user-plugin" +CONTAINER_NAME=${1:-$LOCAL_CONTAINER_NAME} + +if [ -z "$CONTAINER_NAME" ]; then + echo "Container name is not provided. Pass it as an argument or set LOCAL_CONTAINER_NAME in .env." + exit 1 +fi + +PLUGINS_FOLDER_CONTAINER_PATH="/var/www/bigbluebutton-default/assets/plugins" +PLUGIN_CONTAINER_PATH="$PLUGINS_FOLDER_CONTAINER_PATH/$PLUGIN_NAME" + +# Verify the container is running +if ! docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then + echo "Container '$CONTAINER_NAME' is not running. Exiting." + exit 1 +fi + +# Verify the dist/ folder exists locally +if [ ! -d "$PLUGIN_ROOT/dist" ]; then + echo "dist/ folder not found. Run 'npm run build-bundle' first." + exit 1 +fi + +# Remove stale files in the container and recreate the target directory +if docker exec "$CONTAINER_NAME" [ -d "$PLUGIN_CONTAINER_PATH" ]; then + docker exec "$CONTAINER_NAME" rm -rf "$PLUGIN_CONTAINER_PATH" +fi + +echo "Creating container path: $PLUGIN_CONTAINER_PATH ..." +docker exec "$CONTAINER_NAME" mkdir -p "$PLUGIN_CONTAINER_PATH" + +echo "Copying dist/ to $CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist ..." +docker cp "$PLUGIN_ROOT/dist/." "$CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist" + +echo "Done. Plugin deployed to $CONTAINER_NAME at $PLUGIN_CONTAINER_PATH/dist" diff --git a/scripts/publish-plugin-to-s3.sh b/scripts/publish-plugin-to-s3.sh new file mode 100755 index 0000000..aab27d5 --- /dev/null +++ b/scripts/publish-plugin-to-s3.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Uploads the built plugin (dist/) to any S3-compatible bucket and makes all +# files publicly accessible. +# Usage: ./scripts/publish-plugin-to-s3.sh +# +# Required .env variables: +# S3_ACCESS_KEY – Access key ID +# S3_SECRET_KEY – Secret access key +# S3_ENDPOINT_URL – Full endpoint URL (e.g. https://nyc3.digitaloceanspaces.com) +# S3_BUCKET – Bucket name +# S3_PATH – (optional) prefix inside the bucket; defaults to plugins/pick-random-user-plugin/dist + +set -e + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PLUGIN_ROOT="$SCRIPT_DIR/.." + +# Load .env from the plugin root +ENV_FILE="$PLUGIN_ROOT/.env" +if [ -f "$ENV_FILE" ]; then + set -o allexport + source "$ENV_FILE" + set +o allexport +fi + +PLUGIN_NAME="pick-random-user-plugin" +S3_PATH="${S3_PATH:-plugins/$PLUGIN_NAME/dist}" + +# Validate required variables +for var in S3_ACCESS_KEY S3_SECRET_KEY S3_ENDPOINT_URL S3_BUCKET; do + if [ -z "${!var}" ]; then + echo "Error: $var is not set. Define it in .env or export it before running this script." + exit 1 + fi +done + +# Verify the dist/ folder exists locally +if [ ! -d "$PLUGIN_ROOT/dist" ]; then + echo "dist/ folder not found. Run 'npm run build-bundle' first." + exit 1 +fi + +# Verify the AWS CLI is available (used for its S3-compatible interface) +if ! command -v aws &>/dev/null; then + echo "Error: 'aws' CLI not found. Install it with: pip install aws-cli" + exit 1 +fi + +echo "Uploading dist/ to s3://$S3_BUCKET/$S3_PATH ..." + +# aws s3 cp --recursive is used instead of sync so that --acl public-read is +# applied on every run. sync skips unchanged files and never re-applies ACLs, +# which leaves existing objects inaccessible when the ACL wasn't set initially. +AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \ +AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \ +aws s3 cp "$PLUGIN_ROOT/dist/" "s3://$S3_BUCKET/$S3_PATH/" \ + --recursive \ + --endpoint-url "$S3_ENDPOINT_URL" \ + --acl public-read + +echo "" +echo "Done. Plugin is publicly available at:" +echo " $S3_ENDPOINT_URL/$S3_BUCKET/$S3_PATH/" diff --git a/src/components/extensible-areas/action-button-dropdown/component.tsx b/src/components/extensible-areas/action-button-dropdown/component.tsx index 6263e0a..0627a12 100644 --- a/src/components/extensible-areas/action-button-dropdown/component.tsx +++ b/src/components/extensible-areas/action-button-dropdown/component.tsx @@ -35,6 +35,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R new ActionButtonDropdownOption({ label: intl.formatMessage(intlMessages.pickUserLabel), icon: 'user', + dataTest: 'actionDropdownButtonPlugin', tooltip: '', allowed: true, onClick: () => { @@ -49,6 +50,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R label: intl.formatMessage(intlMessages.viewLastPickedUserLabel), icon: 'user', tooltip: '', + dataTest: 'displayLastRandomlyPickedUser', allowed: true, onClick: () => { setShowModal(true); diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index b34be28..9d91194 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -85,6 +85,7 @@ export function PickUserModal(props: PickUserModalProps) { type="button" onClick={handleCloseModal} aria-label="Close button" + data-test="pickRandomUserModalCloseButton" > - {title} + {title} { (pickedUserWithEntryId) ? ( <> @@ -97,12 +97,14 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { {pickedUserWithEntryId?.pickedUser?.name.slice(0, 2)} )} - {pickedUserWithEntryId?.pickedUser?.name} + {pickedUserWithEntryId?.pickedUser?.name} ) : null } {!canClose && remainingSeconds > 0 && !currentUser?.presenter && ( - + {intl.formatMessage( remainingSeconds === 1 ? intlMessages.modalCloseDelayMessageSingular @@ -113,14 +115,17 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { )} { (currentUser?.presenter) ? ( - + {intl.formatMessage(intlMessages.backButtonLabel)} ) : null } {!canClose && remainingSeconds > 0 && currentUser?.presenter && ( - + )} diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 18b9adf..b771eab 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -211,7 +211,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { {intl.formatMessage(intlMessages.availableTitle)} - + {`${users?.length} ${userRoleLabel}: `} {makeHorizontalListOfNames(users)} @@ -223,6 +223,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { deletionFunction([RESET_DATA_CHANNEL]); }} @@ -231,7 +232,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { - + { makeVerticalListOfNames(dataChannelPickedUsers) } @@ -242,6 +243,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { users?.length > 0 ? ( { handlePickRandomUser(); }} @@ -253,7 +255,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { } ) : ( -

+

{intl.formatMessage(intlMessages.noUsersWarning, { 0: userRoleLabel })}

) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..193358e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,108 @@ +# Pick Random User Plugin – Automated Tests + +End-to-end tests for the **Pick Random User Plugin** written with [Playwright](https://playwright.dev/). +They reuse the shared test infrastructure from the +[BigBlueButton HTML Plugin SDK](https://github.com/bigbluebutton/bigbluebutton-html-plugin-sdk). + +--- + +## Test scenarios + +### Structural (`tests/structural/test.spec.ts`) + +Verify that the plugin renders the correct elements with the correct labels and +initial state. + +### Behavioural – single user (`tests/behavioral/single-user.spec.ts`) + +Verify the pick-and-track workflow using only the moderator/presenter. + +### Behavioural – multi-user (`tests/behavioral/multi-user.spec.ts`) + +Verify real-time data-channel synchronization between the presenter and an +attendee in the same meeting. + +--- + +## How to run the tests + +### 1 – Install dependencies + +From the **plugin root** (`plugin-pick-random-user/`): + +```bash +npm install +npx playwright install --with-deps chromium +``` + +The SDK test utilities are resolved via relative paths to the sibling +`bigbluebutton-html-plugin-sdk/` directory: + +```bash +cd ../bigbluebutton-html-plugin-sdk && npm install && cd - +``` + +### 2 – Configure environment variables + +```bash +cp .env.template .env +# edit .env and set BBB_URL and BBB_SECRET +``` + +| Variable | Required | Description | +|----------|----------|-------------| +| `BBB_URL` | **yes** | Full API URL, e.g. `https://bbb.example.com/bigbluebutton/` | +| `BBB_SECRET` | **yes** | Shared secret of the BBB server | +| `PICK_RANDOM_USER_PLUGIN_URL` | no | Direct URL to `manifest.json`; auto-detected from server otherwise | +| `LOCAL_CONTAINER_NAME` | no | Docker container name for the local deployment script | +| `TIMEOUT_MULTIPLIER` | no | Multiply all timeouts (default 1 locally, 2 in CI) | +| `CI` | no | `"true"` enables CI reporter and single-worker mode | + +### 3 – Build and deploy the plugin + +To build the plugin use the command: + +```bash +npm run build-bundle +``` + +Then you need to deploy the plugin serve it so that the target BBB server can access it. You can deploy it to a S3-like environment or to your own server that you are targeting the test to. For the second option, we have a command to do that already: + +```bash +npm run publish-plugin:dev +``` + +--- + +## Running the tests + +```bash +# All suites +npm test + +# Only structural tests +npm test -- tests/structural + +# Only behavioural tests +npm test -- tests/behavioral + +# Only multi-user behavioural tests +npm test -- tests/behavioral/multi-user + +# A single test by name +npm test -- -g "should show the same picked user name" + +# View the HTML report after a run +npx playwright show-report +``` + +--- + +## Test output + +| Artifact | Location | +|----------|----------| +| HTML report | `playwright-report/index.html` | +| Traces | Attached to every test in the HTML report | +| Screenshots | Captured for every test | +| Video | Every test locally; failure-only in CI | diff --git a/tests/behavioral/fixtures.ts b/tests/behavioral/fixtures.ts new file mode 100644 index 0000000..d646a30 --- /dev/null +++ b/tests/behavioral/fixtures.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { test as base } from '@playwright/test'; +import { Plugin } from '../core/plugin'; +import { SessionPage } from '../core/sessionPage'; +import { + encodeCustomParams, + getJoinURL, + generateSettingsData, +} from '../core/helpers'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; + +export interface MultiUserTestFixtures { + multiUserTest: { + modPage: SessionPage; + attendeePage: SessionPage; + }; +} + +export type MultiUserTestConfig = { + envVarName: string; + getPluginUrl?: () => string | undefined; +}; + +/** + * Creates a Playwright test instance whose `multiUserTest` fixture spins up + * two independent browser contexts: one moderator (also presenter) and one + * attendee, both joined into the same BBB meeting with the plugin active. + */ +export function createMultiUserTest(config: MultiUserTestConfig) { + let pluginUrl: string | undefined = process.env[config.envVarName]; + + const test = base.extend({ + multiUserTest: async ({ browser }, use) => { + const customUrl = config.getPluginUrl?.() || pluginUrl; + if (!customUrl) { + throw new Error( + `Plugin URL is not set. Set ${config.envVarName} or ensure beforeAll ran successfully.`, + ); + } + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: customUrl }])}`, + ); + + // ── Moderator / presenter ──────────────────────────────────────────── + const modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Plugin({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + const { modPage } = sample; + + // ── Attendee (viewer) ──────────────────────────────────────────────── + // Joins the same meeting already created by the moderator. + const attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + const attendeePage = new SessionPage({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + + await use({ modPage, attendeePage }); + + await modContext.close(); + await attendeeContext.close(); + }, + }); + + return { + test, + setPluginUrl: (url: string) => { pluginUrl = url; }, + getPluginUrl: () => pluginUrl, + }; +} diff --git a/tests/behavioral/helpers.ts b/tests/behavioral/helpers.ts new file mode 100644 index 0000000..657df3b --- /dev/null +++ b/tests/behavioral/helpers.ts @@ -0,0 +1,63 @@ +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as Page } from '../core/sessionPage'; + +export async function openModal(modPage: Page): Promise { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'action button should appear'); + await modPage.page.click(e.pickRandomUserActionButton); +} + +export async function goBackToPresenterView(modPage: Page): Promise { + await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.hasElement( + e.includeModeratorsCheckbox, + 'presenter view should be restored after clicking back', + ELEMENT_WAIT_TIME, + ); +} + +export async function moderatorCleanupAfterTest(modPage: Page): Promise { + if (await modPage.page.locator(e.pickRandomUserPickedUserViewTitle).isVisible()) { + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.page.locator(e.pickRandomUserModalCloseButton).waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + } + + const modCloseBtn = modPage.page.locator(e.pickRandomUserModalCloseButton); + if (await modCloseBtn.isVisible()) { + // Uncheck any filter checkboxes that were left enabled. + const includePickedUsers = modPage.page.locator(e.includePickedUsersCheckbox); + if (await includePickedUsers.isChecked()) await includePickedUsers.click(); + const includeModerators = modPage.page.locator(e.includeModeratorsCheckbox); + if (await includeModerators.isChecked()) await includeModerators.click(); + const includePresenter = modPage.page.locator(e.includePresenterCheckbox); + if (await includePresenter.isChecked()) await includePresenter.click(); + + // Clear the previously-picked history. + const clearBtn = modPage.page.locator(e.pickRandomUserClearAllButton); + if (await clearBtn.isVisible()) { + await modPage.page.click(e.pickRandomUserClearAllButton); + } + + await modCloseBtn.click(); + return; + } + + // Dismiss the mod's actions dropdown if it was left open. + if (await modPage.page.locator(e.pickRandomUserActionButton).isVisible()) { + await modPage.page.keyboard.press('Escape'); + } +} + +export async function attendeeCleanupAfterTest(attendeePage: Page): Promise { + const attendeeCloseBtn = attendeePage.page.locator(e.pickRandomUserModalCloseButton); + if (await attendeeCloseBtn.isVisible()) { + await attendeeCloseBtn.click(); + } + // Dismiss the attendee's actions dropdown if it was left open. + if (await attendeePage.page.locator(e.pickRandomUserActionButton).isVisible()) { + await attendeePage.page.keyboard.press('Escape'); + } +} diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts new file mode 100644 index 0000000..8923896 --- /dev/null +++ b/tests/behavioral/multi-user.spec.ts @@ -0,0 +1,350 @@ +/** + * Behavioural tests – multi-user (moderator/presenter + attendee/viewer). + */ +import { test, BrowserContext } from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME, ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as Page } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { + encodeCustomParams, + getJoinURL, + generateSettingsData, +} from '../core/helpers'; +import { + attendeeCleanupAfterTest, + goBackToPresenterView, + moderatorCleanupAfterTest, + openModal, +} from './helpers'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; + +/** + * Wait for the attendee's page to finish loading the whiteboard. + * This ensures the attendee is fully in the meeting before the presenter picks. + */ +async function waitForAttendeeMeeting(attendeePage: Page): Promise { + await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); +} + +/** + * Reset state after each test for both pages: + */ +async function cleanupAfterTest(modPage: Page, attendeePage: Page): Promise { + await attendeeCleanupAfterTest(attendeePage); + await moderatorCleanupAfterTest(modPage); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── +test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { + test.describe.configure({ mode: 'serial' }); + + let modPage: Page; + let attendeePage: Page; + let modContext: BrowserContext; + let attendeeContext: BrowserContext; + + test.beforeAll(async ({ browser, request }, testInfo) => { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + + // ── Moderator / presenter ────────────────────────────────────────────── + modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Plugin({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + modPage = sample.modPage; + + // ── Attendee (viewer) ────────────────────────────────────────────────── + attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + attendeePage = new Page({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + }); + + test.afterAll(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + + test.afterEach(async () => { + if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); + }); + + test('should show the same picked user name on both the presenter page and the attendee page', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + + // With default filters the attendee is the only eligible viewer. + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should be visible: there is 1 eligible viewer (the attendee)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Pick the user. + await modPage.page.click(e.pickRandomUserPickButton); + + // Presenter page: transition to picked-user view. + await modPage.hasElement( + e.pickRandomUserPickedUserName, + 'presenter page should show the picked user name', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Attendee page: modal opens automatically. + await attendeePage.hasElement( + e.pickRandomUserPickedUserName, + 'attendee page should show the same picked user name (data channel sync)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Both pages must display the exact same name text. + const pickedNamePresenter = await modPage.getLocator( + e.pickRandomUserPickedUserName, + ).textContent(); + const pickedNameAttendee = await attendeePage + .getLocator(e.pickRandomUserPickedUserName).textContent(); + + test.expect(pickedNamePresenter, 'picked user name on presenter page should not be empty').toBeTruthy(); + test.expect( + pickedNameAttendee, + 'picked user name shown to attendee must match the name shown to the presenter', + ).toBe(pickedNamePresenter); + }); + + test('should open the attendee modal automatically without any action on the attendee side', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + + // Pick – the attendee has not interacted with their page at all. + await modPage.page.click(e.pickRandomUserPickButton); + + // The attendee's modal should open driven purely by the data-channel push. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open automatically via data channel after the presenter picks', + ELEMENT_WAIT_LONGER_TIME, + ); + }); + + test('should show "You have been randomly picked" to the picked attendee and "Randomly picked user" to the presenter', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + // Attendee is the picked user → sees "You have been randomly picked". + await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee modal should open', ELEMENT_WAIT_LONGER_TIME); + await attendeePage.hasText( + e.pickRandomUserPickedUserViewTitle, + 'You have been randomly picked', + 'picked attendee should see the "You have been randomly picked" title', + ); + + // Presenter sees "Randomly picked user" (different user was picked). + await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter modal should transition to picked-user view', ELEMENT_WAIT_LONGER_TIME); + await modPage.hasText( + e.pickRandomUserPickedUserViewTitle, + 'Randomly picked user', + 'presenter should see the "Randomly picked user" title (not their own name)', + ); + }); + + test('should show a countdown message only to the attendee and a countdown bar only to the presenter', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter view should open', ELEMENT_WAIT_LONGER_TIME); + await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee view should open', ELEMENT_WAIT_LONGER_TIME); + + const attendeeCountdown = attendeePage.getLocator(e.pickRandomUserCountDownMessage); + await test.expect( + attendeeCountdown, + 'attendee should see the countdown message during the prevent-close delay', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + + const presenterCountdown = modPage.getLocator(e.pickRandomUserCountDownMessage); + await test.expect( + presenterCountdown, + 'presenter should NOT see the viewer-side countdown message', + ).toBeHidden({ timeout: ELEMENT_WAIT_TIME }); + + const presenterProgressBar = modPage.getLocator(e.pickRandomUserCountDownProgressBar); + await test.expect( + presenterProgressBar, + 'presenter should see the progress countdown bar', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + }); + + test('should not close the picked attendee modal when clicking the overlay during the countdown lock period', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + + // Pick the attendee – the countdown lock starts on the attendee side immediately. + await modPage.page.click(e.pickRandomUserPickButton); + + // Wait for the attendee's modal to open via data-channel sync. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open after being picked', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Click the modal overlay at the top-left corner (well outside the centred modal + // content) while the countdown is still active (shouldCloseOnOverlayClick === false). + await attendeePage.page.locator(e.pickRandomUserModalOverlay).click({ + position: { x: 5, y: 5 }, + force: true, + }); + + // The modal must still be open – the overlay click should have been swallowed. + await test.expect( + attendeePage.getLocator(e.pickRandomUserPickedUserViewTitle), + 'modal should remain open after clicking the overlay during the countdown lock period', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + }); + + test('should keep the previously-picked viewer in the available pool and re-pick the same user when "include already picked users" is enabled and the presenter navigates back', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + + // Enable "Include already picked users" so the viewer stays eligible after being picked. + await modPage.page.click(e.includePickedUsersCheckbox); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should be visible once the attendee is an eligible viewer', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Pick the attendee (the only eligible viewer with default filters). + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter should transition to the picked-user view', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Capture the name of the first picked user for later comparison. + const firstPickedName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect(firstPickedName, 'first picked user name should not be empty').toBeTruthy(); + + // Return to the presenter view via the back button. + await goBackToPresenterView(modPage); + + // ── Assertion 1: attendee appears in the "Previously picked" list ──────── + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + const pickedListCount = await pickedList.count(); + test.expect( + pickedListCount, + 'previously-picked list must contain at least one entry – the attendee was picked', + ).toBeGreaterThanOrEqual(1); + + // ── Assertion 2: picked user is still in the available-for-selection pool ─ + // With includePickedUsers=true the attendee is not removed from the eligible pool. + await modPage.hasText( + e.pickRandomUserAvailableContent, + '1 viewer', + 'available section should still list the attendee as eligible (includePickedUsers is ON)', + ); + await modPage.hasText( + e.pickRandomUserPickButton, + 'Pick again', + 'pick button should read "Pick again" because a user has already been picked this session', + ); + + // ── Assertion 3: re-picking selects the same (and only) eligible user ───── + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter should transition to picked-user view after re-picking', + ELEMENT_WAIT_LONGER_TIME, + ); + const secondPickedName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect( + secondPickedName, + 'the same user must be picked again - they are the only eligible viewer in the pool', + ).toBe(firstPickedName); + }); + + test('should inject "Display last randomly picked user" into the attendee actions dropdown after a pick', async (): Promise => { + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + // Wait for the data channel to sync on the attendee side. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open (confirms data channel received the pick)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Close the attendee modal so the actions dropdown is accessible. + await attendeePage.page.click(e.pickRandomUserModalCloseButton); + await attendeePage.wasRemoved( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should close after clicking the close button', + ELEMENT_WAIT_TIME, + ); + + // Open the attendee's actions dropdown. + await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await attendeePage.page.click(e.actions); + + // The plugin injects "Display last randomly picked user" for attendees. + await attendeePage.hasElement( + e.displayLastRandomlyPickedUser, + 'attendee actions dropdown should contain the plugin option', + ); + await attendeePage.hasText( + e.displayLastRandomlyPickedUser, + 'Display last randomly picked user', + 'plugin option should read "Display last randomly picked user" for the attendee', + ); + }); +}); diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts new file mode 100644 index 0000000..b2ff80c --- /dev/null +++ b/tests/behavioral/single-user.spec.ts @@ -0,0 +1,205 @@ +/** + * Behavioural tests – single user (moderator / presenter only). + */ +import { test, BrowserContext } from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as ModPage } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { encodeCustomParams } from '../core/helpers'; +import { goBackToPresenterView, openModal, moderatorCleanupAfterTest } from './helpers'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; + +/** Enable both inclusion filters so the single moderator/presenter is eligible. */ +async function enableInclusionFilters(modPage: ModPage): Promise { + await modPage.page.click(e.includeModeratorsCheckbox); + await modPage.page.click(e.includePresenterCheckbox); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should appear after enabling both inclusion filters', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +/** Enable all three filters (include picked users too, so "Pick again" is reachable). */ +async function enableAllFilters(modPage: ModPage): Promise { + await modPage.page.click(e.includeModeratorsCheckbox); + await modPage.page.click(e.includePresenterCheckbox); + await modPage.page.click(e.includePickedUsersCheckbox); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should appear after enabling all filters', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +/** Pick a user and wait for the picked-user view to appear. */ +async function pickUser(modPage: ModPage): Promise { + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'picked-user view should appear after clicking pick', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +async function cleanupAfterTest(modPage: ModPage) { + await moderatorCleanupAfterTest(modPage); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── +test.describe('Pick Random User Plugin - Behavioural (single user)', () => { + test.describe.configure({ mode: 'serial' }); + + let modPage: ModPage; + let sharedContext: BrowserContext; + + test.beforeAll(async ({ browser, request }, testInfo) => { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + sharedContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const page = await sharedContext.newPage(); + const plugin = new Plugin({ browser, context: sharedContext }); + await plugin.initModPage(page, { createParameter }); + modPage = plugin.modPage; + }); + + test.afterAll(async () => { + await sharedContext?.close(); + }); + + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + + test('should show "Pick again" button (not "Pick user") after navigating back from picked-user view', async (): Promise => { + // With "Include already picked users" ON the presenter stays in the pool + // after being picked, so the pick button remains visible on return. + await openModal(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); + + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should still be visible (includePickedUsers is enabled)', + ); + await modPage.hasText( + e.pickRandomUserPickButton, + 'Pick again', + 'button label should read "Pick again" after a user has already been picked', + ); + }); + + test('should list the picked user in the "Previously picked" section after picking', async (): Promise => { + await openModal(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); + + // The "Previously picked"
    should contain at least one
  • entry. + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + const count = await pickedList.count(); + test.expect(count, 'previously-picked list should have at least one entry after picking').toBeGreaterThanOrEqual(1); + }); + + test('should empty the "Previously picked" list when "Clear All" is clicked', async (): Promise => { + await openModal(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); + + // Confirm there is at least one entry before clearing. + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + + // Click Clear All. + await modPage.page.click(e.pickRandomUserClearAllButton); + + // The list should now be empty. + await modPage.wasRemoved( + `${e.pickRandomUserPreviouslyPickedList} li`, + '"Previously picked" list should be empty after clicking "Clear All"', + ELEMENT_WAIT_LONGER_TIME, + ); + }); + + test('should drop available count to 0 and show "no users" warning after picking with "Include already picked users" unchecked', async (): Promise => { + // With includePickedUsers=false (default), a picked user is immediately + // removed from the eligible pool after being selected. + await openModal(modPage); + await enableInclusionFilters(modPage); // does NOT enable includePickedUsers + + // Verify 1 user is available before picking. + await modPage.hasText( + e.pickRandomUserAvailableContent, + '1 user', + '"1 user" should be displayed before picking', + ); + + await pickUser(modPage); + await goBackToPresenterView(modPage); + + // After picking, the presenter is now in the picked list and excluded. + await modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'no-users warning should appear: the only user was already picked', + ); + await modPage.wasRemoved( + e.pickRandomUserPickButton, + 'pick button should be hidden when there are 0 eligible users', + ELEMENT_WAIT_TIME, + ); + }); + + test('should restore the pick button after "Clear All" resets the picked-user history', async (): Promise => { + // Scenario: pick once (user excluded) → warning shown → Clear All → user eligible again. + await openModal(modPage); + await enableInclusionFilters(modPage); // includePickedUsers stays OFF + await pickUser(modPage); + await goBackToPresenterView(modPage); + + // No-users warning is shown (picked user excluded). + await modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'no-users warning should be visible before Clear All', + ); + + // Clear the pick history – the user is no longer in the "picked" list. + await modPage.page.click(e.pickRandomUserClearAllButton); + + // The user is now eligible again, so the pick button should reappear. + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should reappear after clearing the pick history', + ELEMENT_WAIT_LONGER_TIME, + ); + await modPage.wasRemoved( + e.pickRandomUserNoUsersWarning, + 'no-users warning should disappear once the pick button is available again', + ELEMENT_WAIT_TIME, + ); + }); +}); diff --git a/tests/core/constants.ts b/tests/core/constants.ts new file mode 100644 index 0000000..5df6056 --- /dev/null +++ b/tests/core/constants.ts @@ -0,0 +1,14 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); + +export const CI = process.env.CI === 'true'; +const TIMEOUT_MULTIPLIER = Number(process.env.TIMEOUT_MULTIPLIER); +const MULTIPLIER = CI ? TIMEOUT_MULTIPLIER || 2 : TIMEOUT_MULTIPLIER || 1; + +// GLOBAL TESTS VARS +export const ELEMENT_WAIT_TIME: number = 5000 * MULTIPLIER; +export const ELEMENT_WAIT_LONGER_TIME: number = 10000 * MULTIPLIER; +export const ELEMENT_WAIT_EXTRA_LONG_TIME: number = 15000 * MULTIPLIER; +export const LOOP_INTERVAL: number = 1200; diff --git a/tests/core/coreElements.ts b/tests/core/coreElements.ts new file mode 100644 index 0000000..6d07619 --- /dev/null +++ b/tests/core/coreElements.ts @@ -0,0 +1,7 @@ +export const coreElements = { + audioModal: 'div[data-test="audioModal"]', + closeModal: 'button[data-test="closeModal"]', + errorMessageLabel: 'span[id="error-message"]', + whiteboard: 'div[data-testid="canvas"]', + actions: 'button[data-test="actionsButton"]', +}; diff --git a/tests/core/fixtures/README.md b/tests/core/fixtures/README.md new file mode 100644 index 0000000..759b7c0 --- /dev/null +++ b/tests/core/fixtures/README.md @@ -0,0 +1,47 @@ +# Sample Test Utilities + +This directory contains shared utilities for sample plugin tests. + +## Usage + +### 1. Using the shared fixture (`sampleFixture.ts`) + +The `createSampleTest` function creates a Playwright test instance with a `sampleTest` fixture that automatically sets up the plugin for testing. + +```typescript +import { expect } from '@playwright/test'; +import { createSampleTest } from '../../../tests/core/sampleFixture'; +import { checkPluginAvailability } from '../../../tests/core/sampleBeforeAll'; + +const { test, setPluginUrl, getPluginUrl } = createSampleTest({ + envVarName: 'YOUR_PLUGIN_URL_ENV_VAR', // e.g., 'ACTIONS_BAR_URL' + getPluginUrl: () => process.env.YOUR_PLUGIN_URL_ENV_VAR, +}); +``` + +### 2. Using the shared beforeAll hook (`sampleBeforeAll.ts`) + +The `checkPluginAvailability` function creates a `beforeAll` hook that automatically checks the plugin availability, fetches the plugin manifest and sets up the plugin URL. + +```typescript +test.describe.parallel('Your Plugin Tests', () => { + test.beforeAll(checkPluginAvailability({ + pluginName: 'sample-your-plugin-name', // Must match the folder name in /plugins/ + envVarName: 'YOUR_PLUGIN_URL_ENV_VAR', + setPluginUrl, + getPluginUrl, + })); + + test('your test', async ({ sampleTest }) => { + // Use sampleTest fixture here + await sampleTest.modPage.hasElement('your-element'); + }); +}); +``` + +## Environment Variable Handling + +The utilities support two ways to provide plugin URLs: + +1. **Custom URL via environment variable**: Set `YOUR_PLUGIN_URL_ENV_VAR` to a custom plugin URL +2. **Auto-generated URL**: If no custom URL is provided, the `beforeAll` hook will construct the URL using the server configuration and plugin name diff --git a/tests/core/fixtures/pluginBeforeAll.ts b/tests/core/fixtures/pluginBeforeAll.ts new file mode 100644 index 0000000..7bb929f --- /dev/null +++ b/tests/core/fixtures/pluginBeforeAll.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +/* eslint-disable import/no-extraneous-dependencies */ +import { TestInfo, APIRequestContext } from '@playwright/test'; +import { secret, server } from '../parameters'; + +export type SampleBeforeAllConfig = { + pluginName: string; + envVarName: string; + setPluginUrl: (url: string) => void; + getPluginUrl?: () => string | undefined; +}; + +export function checkPluginAvailability(config: SampleBeforeAllConfig) { + return async ({ request }: { request: APIRequestContext }, testInfo: TestInfo): Promise => { + // Check if custom URL is already provided via environment variable + const customUrl = config.getPluginUrl?.(); + if (customUrl) { + config.setPluginUrl(customUrl); + return; + } + + const BBB_URL_PATTERN = /^https:\/\/[^/]+\/bigbluebutton\/$/; + if (!secret) throw new Error('BBB_SECRET environment variable is not set'); + if (!server) throw new Error('BBB_URL environment variable is not set'); + if (!BBB_URL_PATTERN.test(server)) throw new Error('BBB_URL must follow the pattern "https://DOMAIN_NAME/bigbluebutton/"'); + + const serverDomain = new URL(server).origin; + const manifestUrlPath = `/plugins/${config.pluginName}/dist/manifest.json`; + const pluginUrl = `${serverDomain}${manifestUrlPath}`; + + const response = await request.get(pluginUrl); + if (!response.ok()) { + const msg = `Failed to fetch plugin manifest for ${pluginUrl} plugin. returned status ${response.status()}`; + console.error(msg); + testInfo.skip( + true, + msg, + ); + return; + } + + try { + await response.json(); + config.setPluginUrl(pluginUrl); + } catch (error) { + const msg = `Invalid JSON response from plugin manifest for ${testInfo.title} plugin`; + console.error(msg, error); + testInfo.skip(true, msg); + } + }; +} diff --git a/tests/core/helpers.ts b/tests/core/helpers.ts new file mode 100644 index 0000000..71ec5aa --- /dev/null +++ b/tests/core/helpers.ts @@ -0,0 +1,195 @@ +import { expect, Page } from '@playwright/test'; +import sha, { Algorithm } from 'sha.js'; +import xml2js from 'xml2js'; +import axios from 'axios'; +import { + attendeePW, fullName, moderatorPW, secret, server, +} from './parameters'; +import { InitParameters } from './sessionPage'; + +declare global { + interface Window { + meetingClientSettings: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public: any; + }; + } +} + +interface getJoinURLParams { + meetingID: string; + isModerator: boolean; + joinParameter?: string; + skipSessionDetailsModal?: boolean; +} + +export interface SessionSettings { + reactionsButton: boolean; + sharedNotesEnabled: boolean; + directLeaveButton: boolean; + // Audio + autoJoinAudioModal: boolean; + listenOnlyMode: boolean; + forceListenOnly: boolean; + skipEchoTest: boolean; + skipEchoTestOnJoin: boolean; + skipEchoTestIfPreviousDevice: boolean; + speechRecognitionEnabled: boolean; + // Chat + chatEnabled: boolean; + publicChatOptionsEnabled: boolean; + maxMessageLength: number; + emojiPickerEnabled: boolean; + autoConvertEmojiEnabled: boolean; + // Polling + pollEnabled: boolean; + pollChatMessage: boolean; + // Presentation + originalPresentationDownloadable: boolean; + presentationWithAnnotationsDownloadable: boolean; + externalVideoPlayer: boolean; + hidePresentationOnJoin: boolean; + // Screensharing + screensharingEnabled: boolean; + // Timeouts + listenOnlyCallTimeout: number; + videoPreviewTimeout: number; + // Webcam + webcamSharingEnabled: boolean; + skipVideoPreview: boolean; + skipVideoPreviewOnFirstJoin: boolean; + skipVideoPreviewIfPreviousDevice: boolean; + // Emoji + emojiRain: boolean; +} + +function getChecksum(text: string) { + let algorithm: Algorithm = (process.env.CHECKSUM || '').toLowerCase() as Algorithm; + if (!['sha1', 'sha256', 'sha512'].includes(algorithm)) { + switch (secret?.length) { + case 128: + algorithm = 'sha512'; + break; + case 64: + algorithm = 'sha256'; + break; + case 40: + default: + algorithm = 'sha1'; + } + } + return sha(algorithm).update(text).digest('hex'); +} + +function getRandomInt(min: number, max: number) { + const adjustedMin = Math.ceil(min); + const adjustedMax = Math.floor(max); + return Math.floor(Math.random() * (adjustedMax - adjustedMin)) + adjustedMin; +} + +function createMeetingUrl(params: InitParameters, createParameter: string | undefined) { + const meetingID = `random-${getRandomInt(1000000, 10000000).toString()}`; + const mp = params.moderatorPW; + const ap = params.attendeePW; + const baseQuery = `name=${meetingID}&meetingID=${meetingID}&attendeePW=${ap}&moderatorPW=${mp}` + + `&allowStartStopRecording=true&autoStartRecording=false&welcome=${params.welcome}`; + const query = createParameter !== undefined ? `${baseQuery}&${createParameter}` : baseQuery; + const apiCall = `create${query}${params.secret}`; + const checksum = getChecksum(apiCall); + const url = `${params.server}/api/create?${query}&checksum=${checksum}`; + return url; +} + +function createMeetingPromise(params: InitParameters, createParameter: string | undefined) { + const url = createMeetingUrl(params, createParameter); + return axios.get(url, { adapter: 'http' }); +} + +export async function createMeeting( + params: InitParameters, + createParameter: string | undefined, +): Promise { + const promise = createMeetingPromise(params, createParameter); + const response = await promise; + expect(response.status).toEqual(200); + const xmlResponse = await xml2js.parseStringPromise(response.data); + return xmlResponse.response.meetingID[0]; +} + +export function getJoinURL({ + meetingID, isModerator, joinParameter, skipSessionDetailsModal, +}: getJoinURLParams) { + const pw = isModerator ? moderatorPW : attendeePW; + const shouldSkipSessionDetailsModal = skipSessionDetailsModal ? '&userdata-bbb_show_session_details_on_join=false' : ''; // default value in settings.yml is true + const baseQuery = `fullName=${fullName}&meetingID=${meetingID}&password=${pw}${shouldSkipSessionDetailsModal}`; + const query = joinParameter ? `${baseQuery}&${joinParameter}` : baseQuery; + const apiCall = `join${query}${secret}`; + const checksum = getChecksum(apiCall); + return `${server}/api/join?${query}&checksum=${checksum}`; +} + +export function encodeCustomParams(param: string): string { + try { + const splitted = param.split('='); + if (splitted.length > 2) { + const aux = splitted.shift(); + splitted[1] = splitted.join('='); + splitted[0] = aux || ''; + } + splitted[1] = encodeURIComponent(splitted[1]).replace(/%20/g, '+'); + return splitted.join('='); + } catch (err) { + throw new Error(`Error encoding custom params: ${err}`); + } +} + +export async function generateSettingsData(page: Page): Promise { + try { + const settingsData = await page.evaluate(() => window.meetingClientSettings.public); + + const settings = { + reactionsButton: settingsData.app?.reactionsButton?.enabled, + sharedNotesEnabled: settingsData.notes?.enabled, + directLeaveButton: settingsData.app?.defaultSettings?.application?.directLeaveButton, + // Audio + autoJoinAudioModal: settingsData.app?.autoJoin, + listenOnlyMode: settingsData.app?.listenOnlyMode, + forceListenOnly: settingsData.app?.forceListenOnly, + skipEchoTest: settingsData.app?.skipCheck, + skipEchoTestOnJoin: settingsData.app?.skipCheckOnJoin, + skipEchoTestIfPreviousDevice: settingsData.app?.skipEchoTestIfPreviousDevice, + speechRecognitionEnabled: settingsData.app?.audioCaptions?.enabled, + // Chat + chatEnabled: settingsData.chat?.enabled, + publicChatOptionsEnabled: settingsData.chat?.enableSaveAndCopyPublicChat, + maxMessageLength: settingsData.chat?.max_message_length, + emojiPickerEnabled: settingsData.chat?.emojiPicker?.enable, + autoConvertEmojiEnabled: settingsData.chat?.autoConvertEmoji, + // Polling + pollEnabled: settingsData.poll?.enabled, + pollChatMessage: settingsData.poll?.chatMessage, + // Presentation + originalPresentationDownloadable: settingsData.presentation?.allowDownloadOriginal, + presentationWithAnnotationsDownloadable: + settingsData.presentation?.allowDownloadWithAnnotations, + externalVideoPlayer: settingsData.externalVideoPlayer?.enabled, + hidePresentationOnJoin: settingsData.layout?.hidePresentationOnJoin, + // Screensharing + screensharingEnabled: settingsData.kurento?.enableScreensharing, + // Timeouts + listenOnlyCallTimeout: parseInt(settingsData.media?.listenOnlyCallTimeout, 10), + videoPreviewTimeout: parseInt(settingsData.kurento?.gUMTimeout, 10), + // Webcam + webcamSharingEnabled: settingsData.kurento?.enableVideo, + skipVideoPreview: settingsData.kurento?.skipVideoPreview, + skipVideoPreviewOnFirstJoin: settingsData.kurento?.skipVideoPreviewOnFirstJoin, + skipVideoPreviewIfPreviousDevice: settingsData.kurento?.skipVideoPreviewIfPreviousDevice, + // Emoji + emojiRain: settingsData.app?.emojiRain?.enabled, + }; + + return settings; + } catch (err) { + throw new Error(`Error generating settings data: ${err}`); + } +} diff --git a/tests/core/parameters.ts b/tests/core/parameters.ts new file mode 100644 index 0000000..7029805 --- /dev/null +++ b/tests/core/parameters.ts @@ -0,0 +1,6 @@ +export const server: string | undefined = process.env.BBB_URL; +export const secret: string | undefined = process.env.BBB_SECRET; +export const welcome: string = '%3Cbr%3EWelcome+to+%3Cb%3E%25%25CONFNAME%25%25%3C%2Fb%3E%21'; +export const fullName: string = 'User1'; +export const moderatorPW: string = 'mp'; +export const attendeePW: string = 'ap'; diff --git a/tests/core/plugin.ts b/tests/core/plugin.ts new file mode 100644 index 0000000..6685b06 --- /dev/null +++ b/tests/core/plugin.ts @@ -0,0 +1,30 @@ +import { Page, Browser, BrowserContext } from '@playwright/test'; +import { InitOptions, SessionPage } from './sessionPage'; + +interface PluginProps { + browser: Browser; + context: BrowserContext; +} + +export class Plugin { + readonly browser: Browser; + + readonly context: BrowserContext; + + modPage!: SessionPage; + + constructor({ browser, context }: PluginProps) { + this.browser = browser; + this.context = context; + } + + async initModPage(page: Page, { + fullName = 'Moderator', + shouldCloseAudioModal = true, + ...restOptions + }: InitOptions = {}) { + const options = { fullName, ...restOptions }; + this.modPage = new SessionPage({ browser: this.browser, page }); + await this.modPage.init({ isModerator: true, shouldCloseAudioModal, initOptions: options }); + } +} diff --git a/tests/core/sessionPage.ts b/tests/core/sessionPage.ts new file mode 100644 index 0000000..555e127 --- /dev/null +++ b/tests/core/sessionPage.ts @@ -0,0 +1,135 @@ +import { + expect, Page, Browser, Locator, +} from '@playwright/test'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME, ELEMENT_WAIT_TIME } from './constants'; +import * as parameters from './parameters'; +import { + createMeeting, generateSettingsData, getJoinURL, SessionSettings, +} from './helpers'; +import { coreElements as e } from './coreElements'; + +interface PageProps { + browser: Browser; + page: Page; +} + +export interface InitOptions { + fullName?: string; + createParameter?: string; + joinParameter?: string; + shouldCloseAudioModal?: boolean; + skipSessionDetailsModal?: boolean; + shouldCheckAllInitialSteps?: boolean; +} + +interface InitFunctionParameters { + isModerator: boolean; + shouldCloseAudioModal: boolean; + initOptions: InitOptions; +} + +export interface InitParameters { + server: string | undefined; + secret: string | undefined; + welcome: string; + fullName: string; + moderatorPW: string; + attendeePW: string; +} + +export class SessionPage { + readonly page: Page; + + readonly browser: Browser; + + settings: SessionSettings | undefined; + + initParameters: InitParameters; + + username: string; + + meetingId: string; + + constructor({ browser, page }: PageProps) { + this.browser = browser; + this.page = page; + this.username = ''; + this.meetingId = ''; + this.initParameters = { ...parameters }; + } + + async init({ isModerator, shouldCloseAudioModal, initOptions = {} }: InitFunctionParameters) { + const { + fullName, + createParameter, + joinParameter, + skipSessionDetailsModal = true, + shouldCheckAllInitialSteps = true, + } = initOptions; + + if (!isModerator) this.initParameters.moderatorPW = ''; + this.username = fullName || this.initParameters.fullName; + + this.meetingId = await createMeeting(this.initParameters, createParameter); + const joinUrl = getJoinURL({ + meetingID: this.meetingId, isModerator, joinParameter, skipSessionDetailsModal, + }); + const response = await this.page.goto(joinUrl); + await expect(response?.ok()).toBeTruthy(); + const hasErrorLabel = await this.checkElement(e.errorMessageLabel); + await expect(hasErrorLabel, 'should pass the authentication and the layout element should be displayed').toBeFalsy(); + if (shouldCheckAllInitialSteps) { + await this.page.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + this.settings = await generateSettingsData(this.page); + const autoJoinAudioModal = this.settings?.autoJoinAudioModal; + if (shouldCloseAudioModal && autoJoinAudioModal) await this.closeAudioModal(); + } + // overwrite for font used in CI + await this.page.addStyleTag({ + content: ` + body { + font-family: 'Liberation Sans', Arial, sans-serif; + }`, + }); + } + + async hasElement(selector: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector); + await expect(locator, description).toBeVisible({ timeout }); + } + + async wasRemoved(selector: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector); + await expect(locator, description).toBeHidden({ timeout }); + } + + async checkElement(selector: string, index = 0): Promise { + // eslint-disable-next-line @typescript-eslint/no-shadow + return this.page.evaluate(([selector, index]) => { + if (typeof selector !== 'string') throw new Error('Selector must be a string'); + const element = document.querySelectorAll(selector); + if (element.length > 0) { + return element[index as number] !== undefined; + } + return false; + }, [selector, index]); + } + + getLocator(selector: string): Locator { + return this.page.locator(selector); + } + + async waitForPluginLogger() { + return this.page.waitForEvent('console', (msg) => msg.text().includes('PluginLogger')); + } + + async hasText(selector: string, text: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector).first(); + await expect(locator, description).toContainText(text, { timeout }); + } + + async closeAudioModal() { + await this.hasElement(e.audioModal, 'should display the audio modal', ELEMENT_WAIT_EXTRA_LONG_TIME); + await this.page.click(e.closeModal); + } +} diff --git a/tests/elements.ts b/tests/elements.ts new file mode 100644 index 0000000..1943b1f --- /dev/null +++ b/tests/elements.ts @@ -0,0 +1,38 @@ +import { coreElements } from './core/coreElements'; + +export const elements = { + ...coreElements, + + // Action button dropdown items injected by the plugin + pickRandomUserActionButton: 'li[data-test="actionDropdownButtonPlugin"]', + displayLastRandomlyPickedUser: 'li[data-test="displayLastRandomlyPickedUser"]', + + // Modal close button + pickRandomUserModalCloseButton: '[data-test="pickRandomUserModalCloseButton"]', + + // Presenter view – filter checkboxes (identified by their HTML id attributes) + includeModeratorsCheckbox: '#includeModerators', + includePresenterCheckbox: '#includePresenter', + includePickedUsersCheckbox: '#includePickedUsers', + + // Presenter view – available users section + pickRandomUserAvailableContent: '[data-test="pickRandomUserAvailableContent"]', + + // Presenter view – pick button and "no users" warning + pickRandomUserPickButton: '[data-test="pickRandomUserPickButton"]', + pickRandomUserNoUsersWarning: '[data-test="pickRandomUserNoUsersWarning"]', + + // Presenter view – previously picked section + pickRandomUserClearAllButton: '[data-test="pickRandomUserClearAllButton"]', + pickRandomUserPreviouslyPickedList: '[data-test="pickRandomUserPreviouslyPickedList"]', + + // Picked-user view + pickRandomUserPickedUserViewTitle: '[data-test="pickRandomUserPickedUserViewTitle"]', + pickRandomUserPickedUserName: '[data-test="pickRandomUserPickedUserName"]', + pickRandomUserBackButton: '[data-test="pickRandomUserBackButton"]', + pickRandomUserCountDownMessage: 'div[data-test="countDownMessage"]', + pickRandomUserCountDownProgressBar: 'div[data-test="countDownProgressBar"]', + + // Modal overlay (ReactModal renders this as a full-screen backdrop) + pickRandomUserModalOverlay: '.modalOverlay', +}; diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts new file mode 100644 index 0000000..e8c55b4 --- /dev/null +++ b/tests/structural/test.spec.ts @@ -0,0 +1,153 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { test, BrowserContext } from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as ModPage } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { encodeCustomParams } from '../core/helpers'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; + +/** Helper: open the plugin modal from the actions dropdown. */ +async function openPickRandomUserModal(modPage: ModPage) { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'action button should be visible'); + await modPage.page.click(e.pickRandomUserActionButton); +} + +/** + * Reset state after each test: close the presenter modal if open, + * or dismiss the actions dropdown if it was left open without opening the modal. + */ +async function cleanupAfterTest(modPage: ModPage): Promise { + // If on the picked-user view, go back to the presenter view first. + if (await modPage.page.locator(e.pickRandomUserPickedUserViewTitle).isVisible()) { + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.page.locator(e.pickRandomUserModalCloseButton).waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + } + + // If the modal is open, clear the picked-user history and close it. + const closeBtn = modPage.page.locator(e.pickRandomUserModalCloseButton); + if (await closeBtn.isVisible()) { + const clearBtn = modPage.page.locator(e.pickRandomUserClearAllButton); + if (await clearBtn.isVisible()) { + await modPage.page.click(e.pickRandomUserClearAllButton); + } + await closeBtn.click(); + return; + } + + // If only the actions dropdown is open (modal was never opened), dismiss it. + if (await modPage.page.locator(e.pickRandomUserActionButton).isVisible()) { + await modPage.page.keyboard.press('Escape'); + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +test.describe('Pick Random User Plugin - Structural', () => { + test.describe.configure({ mode: 'serial' }); + + let modPage: ModPage; + let sharedContext: BrowserContext; + + test.beforeAll(async ({ browser, request }, testInfo) => { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + sharedContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const page = await sharedContext.newPage(); + const plugin = new Plugin({ browser, context: sharedContext }); + await plugin.initModPage(page, { createParameter }); + modPage = plugin.modPage; + }); + + test.afterAll(async () => { + await sharedContext?.close(); + }); + + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + + test('should show "Pick random user" label in the actions dropdown for a presenter', async (): Promise => { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'should display the plugin action-button item'); + await modPage.hasText( + e.pickRandomUserActionButton, + 'Pick random user', + 'should display the correct label "Pick random user"', + ); + }); + + test('should open the presenter modal when clicking the action-button option', async (): Promise => { + await openPickRandomUserModal(modPage); + await modPage.hasElement( + e.includeModeratorsCheckbox, + 'should show the modal presenter view with the "Include moderators" checkbox', + ); + await modPage.hasElement( + e.pickRandomUserAvailableContent, + 'should display the "Available for selection" section', + ); + }); + + test('should display all three filter checkboxes in the presenter view', async (): Promise => { + await openPickRandomUserModal(modPage); + await modPage.hasElement(e.includeModeratorsCheckbox, 'should display the "Include moderators" checkbox'); + await modPage.hasElement(e.includePresenterCheckbox, 'should display the "Include presenter" checkbox'); + await modPage.hasElement(e.includePickedUsersCheckbox, 'should display the "Include already picked user" checkbox'); + }); + + test('should have all three filter checkboxes unchecked by default', async (): Promise => { + await openPickRandomUserModal(modPage); + await test.expect( + modPage.getLocator(e.includeModeratorsCheckbox), + '"Include moderators" should be unchecked by default', + ).not.toBeChecked(); + await test.expect( + modPage.getLocator(e.includePresenterCheckbox), + '"Include presenter" should be unchecked by default', + ).not.toBeChecked(); + await test.expect( + modPage.getLocator(e.includePickedUsersCheckbox), + '"Include already picked user" should be unchecked by default', + ).not.toBeChecked(); + }); + + test('should show "no users" warning and hide the pick button with default filters (only presenter in meeting)', async (): Promise => { + // Default: includeModerators=false, includePresenter=false → + // the single moderator/presenter user is excluded by both rules → 0 eligible. + await openPickRandomUserModal(modPage); + await modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'should show the "No {0} available" warning when 0 users are eligible', + ); + await modPage.wasRemoved( + e.pickRandomUserPickButton, + 'should NOT render the pick button when there are no eligible users', + ELEMENT_WAIT_TIME, + ); + }); +});