diff --git a/.github/workflows/visual_regression.yml b/.github/workflows/visual_regression.yml new file mode 100644 index 000000000..e0c6eb76c --- /dev/null +++ b/.github/workflows/visual_regression.yml @@ -0,0 +1,41 @@ +name: Visual Regression + +on: + pull_request: + push: + branches: + - staging + - master + +jobs: + visual_regression: + name: visual_regression + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./client + env: + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} + # No backend runs in CI; visual-tests/mocks/api.ts answers these API calls. + VITE_DEV_BASE_URL: http://localhost:5000 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: client/package-lock.json + - name: Install dependencies + run: npm ci + - name: Install Chromium + run: npx playwright install --with-deps chromium + - name: Run visual regression test + run: | + if [ -n "$PERCY_TOKEN" ]; then + npm run test:visual + else + echo "PERCY_TOKEN is not configured; running the browser test without uploading snapshots." + npm run test:visual:local + fi diff --git a/.gitignore b/.gitignore index 9f765769b..6e93d9a92 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ node_modules/ # Build / Dist dist build +playwright-report/ +test-results/ # macOS Metafiles .DS_Store diff --git a/client/package-lock.json b/client/package-lock.json index 35471e22b..ed2eae7b4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -93,6 +93,9 @@ "zod": "3.25.76" }, "devDependencies": { + "@percy/cli": "1.32.6", + "@percy/playwright": "1.1.2", + "@playwright/test": "1.61.1", "@tailwindcss/forms": "0.5.11", "@tailwindcss/postcss": "4.2.4", "@tailwindcss/vite": "4.2.4", @@ -7417,6 +7420,39 @@ "node": ">=6" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@headlessui/react": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.10.tgz", @@ -7522,6 +7558,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@lezer/common": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", @@ -7656,6 +7703,44 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@parcel/watcher": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", @@ -7944,6 +8029,374 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@percy/cli": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.32.6.tgz", + "integrity": "sha512-OKkL0Q9u1zkCrhQpBfVdIef8qj88n1O9fB4GQHyrz7Z61tAvGsBY9iwOluU5HfrJR7TiOqcFbWvsRa1E3+AHIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-app": "1.32.6", + "@percy/cli-build": "1.32.6", + "@percy/cli-command": "1.32.6", + "@percy/cli-config": "1.32.6", + "@percy/cli-doctor": "1.32.6", + "@percy/cli-exec": "1.32.6", + "@percy/cli-snapshot": "1.32.6", + "@percy/cli-upload": "1.32.6", + "@percy/client": "1.32.6", + "@percy/logger": "1.32.6" + }, + "bin": { + "percy": "bin/run.cjs" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-app": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.32.6.tgz", + "integrity": "sha512-t7+ti+JiLUPNAWKJitYKPaf4wHrJzmR+bfd6M1Uh41oQN67kYvRiweVWYuK7vST/mANr+36Re9VhXKoy+hiitg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6", + "@percy/cli-exec": "1.32.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-build": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.32.6.tgz", + "integrity": "sha512-xh/FA8IasbYwtmSe4ojWBVpUcgG3BixAPz9rabSzTfJPtG5TJEnWwb/9l4FYB8bTJXeGhVFbLMk9abNQVy4bxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-command": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.32.6.tgz", + "integrity": "sha512-Y8rvlRUJSUcOI7vm5QMF41gNZBGIkuadPnoFIbYVqgFPs8G55tK/NBVfHrCJ4qn1JiUCiUr08alc+brQt319ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.32.6", + "@percy/core": "1.32.6", + "@percy/logger": "1.32.6" + }, + "bin": { + "percy-cli-readme": "bin/readme.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-config": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.32.6.tgz", + "integrity": "sha512-3zlvvgLJ182f3r56hFGLLSmhLaW9VY0FipHI2k9AzZUz2z0KDCBn8zPSm6ESBI/5G2F1kcLjGOTNvrXflVg4yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-doctor": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-doctor/-/cli-doctor-1.32.6.tgz", + "integrity": "sha512-Eecqy8XO6VofxOEAdE6+qYAaN5310huIr2cB+Jvo1Pb/hALkPfr1Kf0fHZMcadwZvxxNGWTGnDjYq/mv1Vg0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6", + "@percy/client": "1.32.6", + "@percy/config": "1.32.6", + "@percy/core": "1.32.6", + "@percy/env": "1.32.6", + "@percy/logger": "1.32.6", + "@percy/monitoring": "1.32.6", + "minimatch": "^9.0.0", + "ws": "^8.17.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-exec": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.32.6.tgz", + "integrity": "sha512-kZO+zaG5tKTpEgrs7eXsEOc/tvYm5O9gq+97BuNbjsFWN5/HnwwCkNNHadjMjVquh4dQ/F/UCeI1ff2TXz1s/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6", + "@percy/logger": "1.32.6", + "cross-spawn": "^7.0.3", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-snapshot": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.32.6.tgz", + "integrity": "sha512-sQvE/tls+Boa/OAT3Ma2BZZ86LxO54bgy9BBZOzZtNgfU245WlfgkMOmKYU023ykuzDRZbDKuGM5pPdIHORFYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6", + "yaml": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-upload": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.32.6.tgz", + "integrity": "sha512-btgyXK+lI1purljvGq41UBkB2xELSoWY0C2SAucsnhy8RJOez/3zwWKYhQ7PUCd5hIUyrJjX7fMR6uwxTrb48w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.32.6", + "fast-glob": "^3.2.11", + "image-size": "~1.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/client": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.32.6.tgz", + "integrity": "sha512-lvHEDmMSc4Y3+MseMZuhor0hzIPgdy4/q3GYi72OaBkWjHXsb603cZQQZD7qKP0gx2Mehowy/wsSubZY+0qDDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.32.6", + "@percy/env": "1.32.6", + "@percy/logger": "1.32.6", + "pac-proxy-agent": "^7.0.2", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/config": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.32.6.tgz", + "integrity": "sha512-7ZxtVy1ZZlqkG07vtftOp/WGsPoCXOJTeUCh9nG0vnQolDqshls2GZYwTaZxdmTTXzUkMcSUp3DxH39Rvo3Q0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/logger": "1.32.6", + "ajv": "^8.6.2", + "cosmiconfig": "^8.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/config/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@percy/core": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.32.6.tgz", + "integrity": "sha512-tMbDU7D4XD1V/vE3E8f5UC7tBG++Mgcsk814knf+RND/6CcPfkVpfNJ1KiMq3nXu2kv+UvwhkUGgLUDZuqEdDQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@grpc/proto-loader": "^0.8.0", + "@percy/client": "1.32.6", + "@percy/config": "1.32.6", + "@percy/dom": "1.32.6", + "@percy/logger": "1.32.6", + "@percy/monitoring": "1.32.6", + "@percy/webdriver-utils": "1.32.6", + "adm-zip": "^0.6.0", + "busboy": "^1.6.0", + "content-disposition": "^0.5.4", + "cross-spawn": "^7.0.3", + "fast-glob": "^3.2.11", + "fast-xml-parser": "^4.4.1", + "micromatch": "^4.0.8", + "mime-types": "^2.1.34", + "pako": "^2.1.0", + "path-to-regexp": "^6.3.0", + "rimraf": "^3.0.2", + "ws": "^8.17.1", + "yaml": "^2.4.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@percy/cli-doctor": "1.32.6" + } + }, + "node_modules/@percy/core/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@percy/dom": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.32.6.tgz", + "integrity": "sha512-ODX3ATJIoor46qQ8h3nyTj/d9BTyXpxhjcCTdkxXFBvO9KYGTE9QeDlyceesco26KkO+TtGVHULLWkEr1R94sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@percy/env": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.32.6.tgz", + "integrity": "sha512-RfjPagc/dVIWlIRfsi/P0bNtPx5jdkmZ6w527bJePr6Cs5Hg2se/vV7S3NSAPtf94EElVsCdzL6C9VzJdztl+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/logger": "1.32.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/logger": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.32.6.tgz", + "integrity": "sha512-9JwYKzIIcAdtOOc+VrV24DB49lZHIZhPR3Ihku3bQAcIcPhvwJwnaWzfkrcJ3r3AwZTNZAqTmtbPgE5xWJPrWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/monitoring": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/monitoring/-/monitoring-1.32.6.tgz", + "integrity": "sha512-4A+wDJ3HFyrwSjX8kz05fs03NfngJI8ydYuejSybWOpuSyZc9YMPJPl3OVK0auGOJSOqt0Fcfk6/1N8qidNy6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.32.6", + "@percy/logger": "1.32.6", + "@percy/sdk-utils": "1.32.6", + "systeminformation": "^5.25.11" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/playwright": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@percy/playwright/-/playwright-1.1.2.tgz", + "integrity": "sha512-IghZHmh1+R3gvneOd//+HbN2xg6e6qgnMy9yJ5V7vWJ49JpkHeU5Esjs1qI9EqFMHKAg0zOorDQ0mJRglQpkyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/sdk-utils": "^1.32.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@percy/cli": ">=1.32.3", + "@playwright/test": ">=1.49.0", + "playwright-core": ">=1" + }, + "peerDependenciesMeta": { + "@percy/cli": { + "optional": true + }, + "@playwright/test": { + "optional": true + } + } + }, + "node_modules/@percy/sdk-utils": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.32.6.tgz", + "integrity": "sha512-dtS4jFz55zQzd0tAtxpt77w4gMeySEjLZEDFHrs3mtmpmnjRlSD0koYl8pj4/X3f375SmXTXG4+xIRgPpSTgZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pac-proxy-agent": "^7.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/webdriver-utils": { + "version": "1.32.6", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.32.6.tgz", + "integrity": "sha512-kUS3wimaBS35BSImbZIsitK4aHDy2hHBpoqYUv+i7SlcFRhIid+0LLRiy0KfPq6DklXEoYgVCgppYu+ymzXhOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.32.6", + "@percy/sdk-utils": "1.32.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -7954,6 +8407,72 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@react-aria/focus": { "version": "3.22.1", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.22.1.tgz", @@ -9073,6 +9592,13 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@transloadit/prettier-bytes": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz", @@ -10516,6 +11042,16 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/adm-zip": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", + "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -10528,6 +11064,23 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -10551,6 +11104,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -10600,6 +11160,19 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10659,6 +11232,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.11.9", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.9.tgz", @@ -10672,6 +11252,16 @@ "node": ">=6.0.0" } }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -10689,6 +11279,29 @@ "license": "MIT", "peer": true }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", @@ -10729,6 +11342,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -11362,6 +11987,21 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -11462,6 +12102,26 @@ "node": ">= 10" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -11521,6 +12181,21 @@ "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-mediaquery": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", @@ -11973,6 +12648,16 @@ "node": ">=12" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -12158,6 +12843,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delaunator": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", @@ -12319,6 +13019,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -12509,6 +13216,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", @@ -12529,6 +13293,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -12569,6 +13343,13 @@ "integrity": "sha512-0DX6gEWwfcDkglS7bdr6oH5jb6TR6BvsVvmOcsUzyl5ESqkze9XImyNZPLoZtaGi1kKGjH/+dNqd8O0SjD7KgQ==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", @@ -12578,6 +13359,69 @@ "node": ">=6.0.0" } }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.7.tgz", + "integrity": "sha512-a6Qh1RMCNbSrU1+sAyAAZH3rTe+OaWJbNZIq0S+ifZciUUOQtlVxBJwoTUE2bYhysmG/RYyI5WJFIKdBahJdrQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -12602,6 +13446,19 @@ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", "license": "MIT" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/filter-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", @@ -12699,6 +13556,13 @@ "integrity": "sha512-RAskVuBmnTcPJXh87oVhYmnNGy/9lvZlLHGui8QFB8yRBuUjzpZoZfZ+hKmDtBDmWNrE2/LRta06W5WmhTzzWQ==", "license": "MIT" }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -12763,6 +13627,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -12781,23 +13655,97 @@ "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 0.4" + "node": "*" } }, "node_modules/globalize": { @@ -13277,6 +14225,30 @@ "entities": "^3.0.1" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -13308,6 +14280,22 @@ "node": ">=0.10.0" } }, + "node_modules/image-size": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/immutable": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", @@ -13341,6 +14329,25 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", @@ -13380,6 +14387,16 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-address": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", + "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -13562,18 +14579,26 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -13622,6 +14647,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -13801,6 +14836,13 @@ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -13838,6 +14880,29 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", @@ -13933,6 +14998,13 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -14293,6 +15365,13 @@ "lodash._basetostring": "~4.12.0" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14315,6 +15394,13 @@ "lodash._baseuniq": "~4.6.0" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -14735,6 +15821,16 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -15305,6 +16401,33 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -15355,6 +16478,22 @@ "mini-svg-data-uri": "cli.js" } }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -15406,6 +16545,16 @@ "node": "^18 || >=20" } }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -15519,6 +16668,16 @@ "node": ">=12.20.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/p-queue": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", @@ -15564,6 +16723,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", @@ -15649,6 +16883,26 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -15699,6 +16953,67 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -15862,6 +17177,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -15939,6 +17278,37 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -16798,6 +18168,16 @@ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -16859,6 +18239,34 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -16910,12 +18318,57 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -17081,6 +18534,29 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -17166,6 +18642,57 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -17238,6 +18765,15 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -17247,6 +18783,21 @@ "node": ">=4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -17261,6 +18812,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -17294,6 +18858,19 @@ } } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/style-it": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/style-it/-/style-it-2.1.4.tgz", @@ -17380,6 +18957,33 @@ "dev": true, "license": "MIT" }, + "node_modules/systeminformation": { + "version": "5.33.6", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.6.tgz", + "integrity": "sha512-hMOQG/eRUzuopuYGGdl8ntkau0nEC7fOaRoTUg1RSr2GTQIk2VNa76DA0+ApajkGfzmcgAupgIP/vt+jtoe5EA==", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tabbable": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", @@ -17524,6 +19128,19 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/topojson-client": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", @@ -18240,6 +19857,22 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", @@ -18329,6 +19962,69 @@ "resolved": "https://registry.npmjs.org/world-countries/-/world-countries-5.1.0.tgz", "integrity": "sha512-CXR6EBvTbArDlDDIWU3gfKb7Qk0ck2WNZ234b/A0vuecPzIfzzxH+O6Ejnvg1sT8XuiZjVlzOH0h08ZtaO7g0w==" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -18346,6 +20042,16 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -18353,6 +20059,51 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/client/package.json b/client/package.json index 7b158efa4..4e4ea3448 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,16 @@ "build": "tsc && vite build", "serve": "vite preview", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:visual": "SKIP_LOCAL_VISUAL_SCREENSHOTS=1 percy exec -- playwright test --project=journey", + "test:visual:local": "SKIP_PERCY_SNAPSHOTS=1 playwright test --project=journey", + "test:visual:headed": "SKIP_PERCY_SNAPSHOTS=1 playwright test --project=journey --headed", + "test:visual:mock": "VISUAL_USE_REAL_BACKEND=0 VITE_DEV_BASE_URL=http://localhost:5000 npm run test:visual:local --", + "test:visual:mock:headed": "VISUAL_USE_REAL_BACKEND=0 VITE_DEV_BASE_URL=http://localhost:5000 npm run test:visual:headed --", + "test:visual:real": "VISUAL_USE_REAL_BACKEND=1 npm run test:visual:local --", + "test:visual:real:headed": "VISUAL_USE_REAL_BACKEND=1 npm run test:visual:headed --", + "test:visual:isolated": "SKIP_PERCY_SNAPSHOTS=1 playwright test --project=isolated", + "test:visual:isolated:headed": "SKIP_PERCY_SNAPSHOTS=1 playwright test --project=isolated --headed" }, "dependencies": { "@ckeditor/ckeditor5-react": "11.2.0", @@ -115,6 +124,9 @@ ] }, "devDependencies": { + "@percy/cli": "1.32.6", + "@percy/playwright": "1.1.2", + "@playwright/test": "1.61.1", "@tailwindcss/forms": "0.5.11", "@tailwindcss/postcss": "4.2.4", "@tailwindcss/vite": "4.2.4", diff --git a/client/playwright.config.ts b/client/playwright.config.ts new file mode 100644 index 000000000..70ab97ac9 --- /dev/null +++ b/client/playwright.config.ts @@ -0,0 +1,67 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { defineConfig } from '@playwright/test'; + +/** Load client/.env for local configuration and real-backend login credentials. */ +function loadClientEnvFile(): void { + // Scripts are run from /client, so cwd is the project root for Playwright. + const envPath = path.join(process.cwd(), '.env'); + if (!fs.existsSync(envPath)) return; + + for (const rawLine of fs.readFileSync(envPath, 'utf8').split('\n')) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + const eq = line.indexOf('='); + if (eq <= 0) continue; + const key = line.slice(0, eq).trim(); + let value = line.slice(eq + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + if (process.env[key] === undefined) { + process.env[key] = value; + } + } +} + +loadClientEnvFile(); + +const headed = process.argv.includes('--headed'); + +export default defineConfig({ + testDir: './visual-tests', + fullyParallel: false, + workers: 1, + // Login + 1st/3rd/5th books with waits + dual-width shots needs more than default 30s. + timeout: 180_000, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? 'github' : 'list', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + ...(headed + ? { launchOptions: { slowMo: 300 } } + : {}), + }, + webServer: { + command: 'npm run start -- --host localhost --port 3000', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, + projects: [ + { + // Default for CI + local headed/local: one end-to-end journey. + name: 'journey', + testMatch: /journey\.visual\.spec\.ts/, + }, + { + // Opt-in pieces (login-only, books-only). Not run unless selected. + name: 'isolated', + testMatch: /isolated\/.*\.visual\.spec\.ts/, + }, + ], +}); diff --git a/client/visual-tests/base.ts b/client/visual-tests/base.ts new file mode 100644 index 000000000..a5072c4c3 --- /dev/null +++ b/client/visual-tests/base.ts @@ -0,0 +1,82 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { Page } from '@playwright/test'; +import percySnapshot from '@percy/playwright'; +import { mockConductorApi } from './mocks/api'; + +/** Desktop + mobile widths used for Percy / local snapshots. */ +export const PERCY_WIDTHS = [375, 1280] as const; + +const LOCAL_SCREENSHOT_DIR = path.join('test-results', 'visual'); + +export function usesRealBackend(): boolean { + return process.env.VISUAL_USE_REAL_BACKEND === '1'; +} + +/** + * Mock API calls by default (CI has no backend). Real mode leaves API requests + * untouched. Both modes block the external support widget for stable snapshots. + */ +export async function stabilizeAppShell(page: Page): Promise { + if (!usesRealBackend()) { + await mockConductorApi(page); + } + await page.route('https://cdn.libretexts.net/**', (route) => route.abort()); +} + +function shouldCaptureLocal(): boolean { + return process.env.SKIP_LOCAL_VISUAL_SCREENSHOTS !== '1'; +} + +function shouldCapturePercy(): boolean { + // Set by `test:visual` / CI. Local `:headed` / `:local` set SKIP_PERCY_SNAPSHOTS=1. + return process.env.SKIP_PERCY_SNAPSHOTS !== '1'; +} + +/** + * One snapshot call for all flows. + * - Percy path (`npm run test:visual` / CI): uploads via Percy, skips local PNGs. + * - Local path (`test:visual:local` / `:headed`): writes PNGs, skips Percy. + */ +export async function captureVisualSnapshot( + page: Page, + name: string, +): Promise { + if (shouldCapturePercy()) { + await percySnapshot(page, name, { + widths: [...PERCY_WIDTHS], + }); + } + + if (shouldCaptureLocal()) { + await captureLocalSnapshots(page, name); + } +} + +/** + * Save PNGs under gitignored `test-results/visual/` so you can preview shots + * locally without Percy or a PR. No-ops when SKIP_LOCAL_VISUAL_SCREENSHOTS=1. + */ +export async function captureLocalSnapshots( + page: Page, + name: string, +): Promise { + if (!shouldCaptureLocal()) { + return; + } + + fs.mkdirSync(LOCAL_SCREENSHOT_DIR, { recursive: true }); + const slug = name + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + + for (const width of PERCY_WIDTHS) { + await page.setViewportSize({ width, height: 900 }); + await page.screenshot({ + path: path.join(LOCAL_SCREENSHOT_DIR, `${slug}-${width}.png`), + fullPage: true, + }); + } +} diff --git a/client/visual-tests/flows/commons/base.ts b/client/visual-tests/flows/commons/base.ts new file mode 100644 index 000000000..305e849e1 --- /dev/null +++ b/client/visual-tests/flows/commons/base.ts @@ -0,0 +1,9 @@ +import type { Page } from '@playwright/test'; +import { books } from './books/base'; + +/** + * Public entry for Commons flows. Outside callers import from here only. + */ +export async function commons(page: Page): Promise { + await books(page); +} diff --git a/client/visual-tests/flows/commons/books/base.ts b/client/visual-tests/flows/commons/books/base.ts new file mode 100644 index 000000000..081a204cc --- /dev/null +++ b/client/visual-tests/flows/commons/books/base.ts @@ -0,0 +1,9 @@ +import type { Page } from '@playwright/test'; +import { openCommonsBooks } from './openCommonsBooks'; + +/** + * Public entry for Commons books flows. Outside callers import from here only. + */ +export async function books(page: Page): Promise { + await openCommonsBooks(page); +} diff --git a/client/visual-tests/flows/commons/books/openCommonsBooks.ts b/client/visual-tests/flows/commons/books/openCommonsBooks.ts new file mode 100644 index 000000000..dab0e3151 --- /dev/null +++ b/client/visual-tests/flows/commons/books/openCommonsBooks.ts @@ -0,0 +1,34 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { captureVisualSnapshot } from '../../../base'; +import { clickOn, goToPage, wait0s, wait5s } from '../../../utils'; + +/** 1-based catalog positions to open (same steps for each). */ +const BOOK_POSITIONS = [1, 3, 5] as const; + +/** + * Commons books flow: for each of the 1st, 3rd, and 5th catalog cards, + * screenshot the catalog, open the book, then screenshot the book page. + */ +export async function openCommonsBooks(page: Page): Promise { + for (const position of BOOK_POSITIONS) { + const index = position - 1; + + await goToPage(page, '/', wait5s); + + const bookLink = page.locator('a[href^="/book/"]').nth(index); + await expect(bookLink).toBeVisible({ timeout: 30_000 }); + + await captureVisualSnapshot( + page, + `Commons catalog before book ${position}`, + ); + + await clickOn(page, 'a[href^="/book/"]', wait0s, wait5s, index); + + await captureVisualSnapshot( + page, + `Commons after opening book ${position}`, + ); + } +} diff --git a/client/visual-tests/flows/home/base.ts b/client/visual-tests/flows/home/base.ts new file mode 100644 index 000000000..c60c7be4d --- /dev/null +++ b/client/visual-tests/flows/home/base.ts @@ -0,0 +1,9 @@ +import type { Page } from '@playwright/test'; +import { visitHome } from './home'; + +/** + * Public entry for the home flow. Outside callers import from here only. + */ +export async function home(page: Page): Promise { + await visitHome(page); +} diff --git a/client/visual-tests/flows/home/home.ts b/client/visual-tests/flows/home/home.ts new file mode 100644 index 000000000..2a7e1d66b --- /dev/null +++ b/client/visual-tests/flows/home/home.ts @@ -0,0 +1,15 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { captureVisualSnapshot } from '../../base'; +import { goToPage, wait5s } from '../../utils'; + +/** + * Conductor home (`/home`). Safe to call after login, or alone if already authed. + */ +export async function visitHome(page: Page): Promise { + await goToPage(page, '/home', wait5s); + + await expect(page).toHaveURL(/\/home/); + + await captureVisualSnapshot(page, 'Conductor home'); +} diff --git a/client/visual-tests/flows/loginWithFallbackAuth.ts b/client/visual-tests/flows/loginWithFallbackAuth.ts new file mode 100644 index 000000000..7d218e5d0 --- /dev/null +++ b/client/visual-tests/flows/loginWithFallbackAuth.ts @@ -0,0 +1,30 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { captureVisualSnapshot, usesRealBackend } from '../base'; +import { clickOn, goToPage, typeIn, wait0s, wait5s } from '../utils'; + +/** + * Isolated fallback-auth login: always starts at `/fallback-auth`. + */ +export async function loginWithFallbackAuth(page: Page): Promise { + const realBackend = usesRealBackend(); + const email = realBackend ? process.env.VISUAL_AUTH_EMAIL : 'visual-tests@example.com'; + const password = realBackend ? process.env.VISUAL_AUTH_PASSWORD : 'visual-tests-password'; + if (!email || !password) { + throw new Error( + 'Real-backend login requires VISUAL_AUTH_EMAIL and VISUAL_AUTH_PASSWORD in your environment or client/.env.', + ); + } + + await goToPage(page, '/fallback-auth'); + await expect(page.getByLabel('Email')).toBeVisible(); + await expect(page.getByLabel('Password')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Login' })).toBeVisible(); + + await captureVisualSnapshot(page, 'Conductor fallback auth'); + + await typeIn(page, '#email', email); + await typeIn(page, '#password', password); + await clickOn(page, 'button[type="submit"]', wait0s, wait5s); + await expect(page).toHaveURL(/\/home/); +} diff --git a/client/visual-tests/isolated/books/open-first-book.visual.spec.ts b/client/visual-tests/isolated/books/open-first-book.visual.spec.ts new file mode 100644 index 000000000..aafc0a585 --- /dev/null +++ b/client/visual-tests/isolated/books/open-first-book.visual.spec.ts @@ -0,0 +1,18 @@ +import { test } from '@playwright/test'; +import { stabilizeAppShell } from '../../base'; +import { books } from '../../flows/commons/books/base'; + +/** + * Independent books scenario: opens the 1st, 3rd, and 5th catalog books. + * Run with: npm run test:visual:isolated:headed + */ +test('open commons books from catalog', async ({ page }) => { + await stabilizeAppShell(page); + + try { + await books(page); + } catch (error) { + console.error('open commons books visual test failed:', error); + throw error; + } +}); diff --git a/client/visual-tests/isolated/login/fallback-auth.visual.spec.ts b/client/visual-tests/isolated/login/fallback-auth.visual.spec.ts new file mode 100644 index 000000000..3e88f8ee1 --- /dev/null +++ b/client/visual-tests/isolated/login/fallback-auth.visual.spec.ts @@ -0,0 +1,18 @@ +import { test } from '@playwright/test'; +import { stabilizeAppShell } from '../../base'; +import { loginWithFallbackAuth } from '../../flows/loginWithFallbackAuth'; + +/** + * Independent login scenario: always starts at `/fallback-auth`. + * Run with: npm run test:visual:isolated:headed + */ +test('fallback auth page', async ({ page }) => { + await stabilizeAppShell(page); + + try { + await loginWithFallbackAuth(page); + } catch (error) { + console.error('fallback auth visual test failed:', error); + throw error; + } +}); diff --git a/client/visual-tests/journey.visual.spec.ts b/client/visual-tests/journey.visual.spec.ts new file mode 100644 index 000000000..e6639c46f --- /dev/null +++ b/client/visual-tests/journey.visual.spec.ts @@ -0,0 +1,32 @@ +import { test } from '@playwright/test'; +import { stabilizeAppShell } from './base'; +import { loginWithFallbackAuth } from './flows/loginWithFallbackAuth'; +import { home } from './flows/home/base'; +import { commons } from './flows/commons/base'; + +/** + * Default visual journey (what CI / `npm run test:visual*` runs). + * One browser, one flow, multiple Percy/local screenshots along the way: + * 1) fallback-auth login + * 2) Conductor home + * 3) Commons (books) + * + * Isolated login/books specs live under `isolated/` and only run with + * `--project=isolated`. + */ +test.describe.configure({ mode: 'serial' }); + +test('conductor visual journey: login then open commons books', async ({ + page, +}) => { + await stabilizeAppShell(page); + + try { + await loginWithFallbackAuth(page); + // await home(page); + await commons(page); + } catch (error) { + console.error('conductor visual journey failed:', error); + throw error; + } +}); diff --git a/client/visual-tests/mocks/api.ts b/client/visual-tests/mocks/api.ts new file mode 100644 index 000000000..4a198722a --- /dev/null +++ b/client/visual-tests/mocks/api.ts @@ -0,0 +1,172 @@ +import type { Page, Route } from '@playwright/test'; +import books from './fixtures/books.json'; +import catalog from './fixtures/catalog.json'; +import filters from './fixtures/filters.json'; +import libregrid from './fixtures/libregrid.json'; + +/** + * Fake Conductor API for visual tests. CI has no backend or database, so every + * `/api/v1/*` request is answered here with fixed data. That keeps screenshots + * identical between runs (no live catalog order, covers, or user data). + * + * Fixtures were trimmed from a real local session; book thumbnails are replaced + * with a flat placeholder so Percy never diffs on remote images. + */ + +// Must match COOKIE_NAMES in src/components/util/AuthHelper.ts (APP_ENV "production" prefix). +const AUTH_COOKIES = ['conductor_access_v2', 'conductor_signed_v2']; + +const ORG = { + orgID: 'libretexts', + name: 'LibreTexts', + shortName: 'LibreTexts', + abbreviation: 'LT', + largeLogo: '', + mediumLogo: '', + smallLogo: '', + aboutLink: '', + commonsHeader: '', + commonsMessage: '', + videoLengthLimit: 0, + defaultProjectLead: '', + addToLibreGridList: false, +}; + +const USER = { + uuid: '00000000-0000-4000-8000-000000000000', + firstName: 'Visual', + lastName: 'Tester', + email: 'visual-tests@example.com', + avatar: '', + authType: 'traditional', + roles: [{ org: 'libretexts', role: 'superadmin' }], + verifiedInstructor: false, +}; + +type Json = Record; +type Handler = (match: RegExpMatchArray, route: Route, page: Page) => Json | Promise; + +const bookMap = books as Record; + +function bookDetail(bookID: string): Json | undefined { + if (bookMap[bookID]) return bookMap[bookID]; + const summary = catalog.books.find((b) => b.bookID === bookID); + return summary && { ...summary, readerResources: [], isbns: [] }; +} + +/** Routes are matched against the path after `/api/v1`, first match wins. */ +const ROUTES: Array<[method: string, path: RegExp, handler: Handler]> = [ + ['GET', /^\/org$/, () => ({ err: false, org: ORG })], + // Constant build ID so the "new version available" toast never appears. + ['GET', /^\/build$/, () => ({ version: 'visual-test', ref: 'visual-test' })], + [ + 'GET', + /^\/config$/, + () => ({ + err: false, + data: { + env: 'development', + main_commons_url: '', + instructor_verification_url: '', + central_identity_base_url: '', + }, + }), + ], + [ + 'POST', + /^\/auth\/fallback-auth$/, + async (_m, _route, page) => { + // The real server sets these cookies; the client only checks they exist. + await page.context().addCookies( + AUTH_COOKIES.map((name) => ({ + name, + value: 'visual-test', + url: new URL(page.url()).origin, + })), + ); + return { err: false }; + }, + ], + ['GET', /^\/user\/basicinfo$/, () => ({ err: false, user: USER })], + ['GET', /^\/announcements\/system$/, () => ({ err: false, sysAnnouncement: null })], + ['GET', /^\/announcements\/all$/, () => ({ err: false, announcements: [] })], + ['GET', /^\/projects\/recent$/, () => ({ err: false, projects: [] })], + ['GET', /^\/projects\/pinned$/, () => ({ err: false, pinned: [] })], + + // Commons catalog + ['GET', /^\/commons\/catalog$/, () => catalog], + ['GET', /^\/commons\/filters$/, () => filters], + ['GET', /^\/orgs\/libregrid$/, () => libregrid], + ['GET', /^\/central-identity\/licenses$/, () => ({ err: false, licenses: [] })], + ['GET', /^\/projects\/public$/, () => ({ err: false, projects: [], totalCount: 0 })], + ['GET', /^\/projects\/files\/public$/, () => ({ err: false, files: [], totalCount: 0 })], + ['GET', /^\/search\/authors$/, () => ({ err: false, numResults: 0, results: [] })], + ['GET', /^\/search\/minirepos$/, () => ({ err: false, numResults: 0, results: [] })], + + // Commons book page + [ + 'GET', + /^\/commons\/book\/([^/]+)$/, + ([, bookID]) => { + const book = bookDetail(bookID); + return book ? { err: false, book } : { err: true, errMsg: 'Book not found.' }; + }, + ], + [ + 'GET', + /^\/commons\/book\/([^/]+)\/toc$/, + ([, bookID]) => { + const book = bookDetail(bookID); + const url = (book?.links as Json | undefined)?.online ?? ''; + return { + err: false, + toc: { + id: bookID, + title: book?.title ?? '', + url, + children: ['Front Matter', 'Chapter 1', 'Chapter 2', 'Back Matter'].map((title, i) => ({ + id: `${bookID}-${i}`, + title, + url, + children: [], + })), + }, + }; + }, + ], + [ + 'GET', + /^\/commons\/book\/([^/]+)\/licensereport$/, + () => ({ err: false, found: false, msg: "Couldn't find a Content Licensing Report for that resource." }), + ], + [ + 'GET', + /^\/project\/[^/]+\/files\/content\/?$/, + () => ({ err: false, files: [], path: [{ fileID: '', name: '' }] }), + ], +]; + +/** + * Answer every Conductor API call from fixtures. Unmocked endpoints return a + * 404 and are logged, so a new API call on a snapshotted page is easy to spot. + */ +export async function mockConductorApi(page: Page): Promise { + await page.route(/\/api\/v1\//, async (route) => { + const request = route.request(); + const url = new URL(request.url()); + const path = url.pathname.replace(/^.*?\/api\/v1/, ''); + + for (const [method, pattern, handler] of ROUTES) { + const match = request.method() === method && path.match(pattern); + if (match) { + return route.fulfill({ json: await handler(match, route, page) }); + } + } + + console.warn(`[visual mocks] unmocked API call: ${request.method()} ${path}`); + return route.fulfill({ + status: 404, + json: { err: true, errMsg: `No visual-test mock for ${request.method()} ${path}` }, + }); + }); +} diff --git a/client/visual-tests/mocks/fixtures/books.json b/client/visual-tests/mocks/fixtures/books.json new file mode 100644 index 000000000..c2fa7aa2c --- /dev/null +++ b/client/visual-tests/mocks/fixtures/books.json @@ -0,0 +1,126 @@ +{ + "socialsci-372438": { + "bookID": "socialsci-372438", + "affiliation": "Chabot College", + "author": "Jessica Gallucci", + "course": "Chabot College", + "lastUpdated": "Sun, 19 Jul 2026 01:18:10 GMT", + "library": "socialsci", + "libraryTags": [ + "article:topic-category", + "authorname:jscglc", + "coverpage:yes", + "license:ccby", + "lulu@Introduction to California Government and Politics (saved June 2026 version) by Steven Reti@Jessica Gallucci@Chabot College@Introduction to California Government and Politics (saved June 2026) by Steven Reti" + ], + "license": "ccby", + "links": { + "online": "https://socialsci.libretexts.org/Courses/Chabot_College/Introduction_to_California_Government_and_Politics_(saved_June_2026_version)_by_Steven_Reti", + "pdf": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/pdf", + "buy": "https://commons.libretexts.org/store/product/socialsci-372438", + "zip": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/pages", + "files": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/thincc" + }, + "location": "campus", + "program": "", + "readerResources": [], + "subject": "", + "summary": "", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "thumbnailIsAnimated": false, + "title": "Introduction to California Government and Politics (saved June 2026 version) by Steven Reti", + "coverID": "372438", + "hasReaderResources": false, + "allowAnonPR": false, + "hasPeerReviews": false, + "hasAdaptCourse": false, + "isbns": [] + }, + "phys-24413": { + "bookID": "phys-24413", + "title": "Classical Mechanics", + "author": "Peter Dourmashkin", + "affiliation": "Massachusetts Institute of Technology", + "library": "phys", + "subject": "Classical Mechanics", + "location": "central", + "course": "", + "program": "mitocw", + "license": "ccbyncsa", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://phys.libretexts.org/Bookshelves/Classical_Mechanics/Classical_Mechanics_(Dourmashkin)", + "pdf": "https://downloads.libretexts.org/api/v1/download/phys-24413/pdf", + "buy": "https://commons.libretexts.org/store/product/phys-24413", + "zip": "https://downloads.libretexts.org/api/v1/download/phys-24413/pages", + "files": "https://downloads.libretexts.org/api/v1/download/phys-24413/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/phys-24413/thincc" + }, + "summary": "The principles of mechanics successfully described many phenomena encountered in the world. Conservation laws involving energy, momentum and angular momentum provided a second parallel approach to solving many of the same problems. This text investigate both approaches: Force and conservation laws to develop a conceptual understanding of the core concepts, a familiarity with the experimental verification of our theoretical laws, and an ability to predict the motions of bodies.", + "lastUpdated": "Tue, 02 Jul 2024 15:28:21 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:pdourmashkin", + "coverpage:yes", + "license:ccbyncsa", + "licenseversion:40", + "lulu@Classical Mechanics@Peter Dourmashkin@Massachusetts Institute of Technology@Classical Mechanics", + "program:mitocw", + "showtoc:no", + "source@https://ocw.mit.edu/courses/8-01sc-classical-mechanics-fall-2016/" + ], + "rating": 4, + "thumbnailIsAnimated": false, + "coverID": "24413", + "projectID": "iwRt6mh65R", + "hasReaderResources": false, + "allowAnonPR": true, + "hasPeerReviews": false, + "hasAdaptCourse": false, + "isbns": [] + }, + "human-385563": { + "bookID": "human-385563", + "affiliation": "Plymouth State University", + "author": "George Matthews", + "course": "", + "lastUpdated": "Sat, 07 Mar 2026 04:41:12 GMT", + "library": "human", + "libraryTags": [ + "article:topic-category", + "authorname:gmtth", + "coverpage:yes", + "cssprint:default", + "license:ccby", + "licenseversion:40", + "lulu@Introduction to Philosophy- Ethics@George Matthews@Plymouth State University@Introduction to Philosophy- Ethics", + "printoptions:no-header-title", + "program:oeri", + "showtoc:yes" + ], + "license": "ccby", + "links": { + "online": "https://human.libretexts.org/Bookshelves/Philosophy/Ethics/Introduction_to_Philosophy%3A_Ethics_(Matthews_and_Hendricks)", + "pdf": "https://downloads.libretexts.org/api/v1/download/human-385563/pdf", + "buy": "https://commons.libretexts.org/store/product/human-385563", + "zip": "https://downloads.libretexts.org/api/v1/download/human-385563/pages", + "files": "https://downloads.libretexts.org/api/v1/download/human-385563/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/human-385563/thincc" + }, + "location": "central", + "program": "oeri", + "readerResources": [], + "subject": "Philosophy", + "summary": "", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "title": "Introduction to Philosophy- Ethics", + "thumbnailIsAnimated": false, + "coverID": "385563", + "hasReaderResources": false, + "allowAnonPR": false, + "hasPeerReviews": false, + "hasAdaptCourse": false, + "isbns": [] + } +} diff --git a/client/visual-tests/mocks/fixtures/catalog.json b/client/visual-tests/mocks/fixtures/catalog.json new file mode 100644 index 000000000..35d9a86a0 --- /dev/null +++ b/client/visual-tests/mocks/fixtures/catalog.json @@ -0,0 +1,286 @@ +{ + "err": false, + "numTotal": 8, + "books": [ + { + "bookID": "socialsci-372438", + "title": "Introduction to California Government and Politics (saved June 2026 version) by Steven Reti", + "author": "Jessica Gallucci", + "affiliation": "Chabot College", + "library": "socialsci", + "subject": "", + "location": "campus", + "course": "Chabot College", + "program": "", + "license": "ccby", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://socialsci.libretexts.org/Courses/Chabot_College/Introduction_to_California_Government_and_Politics_(saved_June_2026_version)_by_Steven_Reti", + "pdf": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/pdf", + "buy": "https://commons.libretexts.org/store/product/socialsci-372438", + "zip": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/pages", + "files": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/socialsci-372438/thincc" + }, + "summary": "", + "lastUpdated": "Sun, 19 Jul 2026 01:18:10 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:jscglc", + "coverpage:yes", + "license:ccby", + "lulu@Introduction to California Government and Politics (saved June 2026 version) by Steven Reti@Jessica Gallucci@Chabot College@Introduction to California Government and Politics (saved June 2026) by Steven Reti" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "chem-330276", + "title": "CHM 320: Advanced Inorganic Chemistry textbook", + "author": "", + "affiliation": "", + "library": "chem", + "subject": "", + "location": "campus", + "course": "Kutztown University of Pennsylvania", + "program": "", + "license": "", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://chem.libretexts.org/Courses/Kutztown_University_of_Pennsylvania/CHM_320%3A_Advanced_Inorganic_Chemistry_textbook", + "pdf": "https://downloads.libretexts.org/api/v1/download/chem-330276/pdf", + "buy": "https://commons.libretexts.org/store/product/chem-330276", + "zip": "https://downloads.libretexts.org/api/v1/download/chem-330276/pages", + "files": "https://downloads.libretexts.org/api/v1/download/chem-330276/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/chem-330276/thincc" + }, + "summary": "", + "lastUpdated": "Tue, 03 Aug 2021 19:33:01 GMT", + "libraryTags": [ + "article:topic-category", + "coverpage:yes" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "phys-24413", + "title": "Classical Mechanics", + "author": "Peter Dourmashkin", + "affiliation": "Massachusetts Institute of Technology", + "library": "phys", + "subject": "Classical Mechanics", + "location": "central", + "course": "", + "program": "mitocw", + "license": "ccbyncsa", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://phys.libretexts.org/Bookshelves/Classical_Mechanics/Classical_Mechanics_(Dourmashkin)", + "pdf": "https://downloads.libretexts.org/api/v1/download/phys-24413/pdf", + "buy": "https://commons.libretexts.org/store/product/phys-24413", + "zip": "https://downloads.libretexts.org/api/v1/download/phys-24413/pages", + "files": "https://downloads.libretexts.org/api/v1/download/phys-24413/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/phys-24413/thincc" + }, + "summary": "The principles of mechanics successfully described many phenomena encountered in the world. Conservation laws involving energy, momentum and angular momentum provided a second parallel approach to solving many of the same problems. This text investigate both approaches: Force and conservation laws to develop a conceptual understanding of the core concepts, a familiarity with the experimental verification of our theoretical laws, and an ability to predict the motions of bodies.", + "lastUpdated": "Tue, 02 Jul 2024 15:28:21 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:pdourmashkin", + "coverpage:yes", + "license:ccbyncsa", + "licenseversion:40", + "lulu@Classical Mechanics@Peter Dourmashkin@Massachusetts Institute of Technology@Classical Mechanics", + "program:mitocw", + "showtoc:no", + "source@https://ocw.mit.edu/courses/8-01sc-classical-mechanics-fall-2016/" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "human-230520", + "title": "Para vivir con salud", + "author": "Kathryn Joy McKnight and Jill S. Kuhnheim", + "affiliation": "University of New Mexico", + "library": "human", + "subject": "Languages", + "location": "central", + "course": "", + "program": "", + "license": "ccbync", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://human.libretexts.org/Bookshelves/Languages/Spanish/Spanish_for_the_Professions_Bookshelf/Para_vivir_con_salud_-_leyendo_la_salud_y_la_literatura_(McKnight_y_Kuhnheim)", + "pdf": "https://downloads.libretexts.org/api/v1/download/human-230520/pdf", + "buy": "https://commons.libretexts.org/store/product/human-230520", + "zip": "https://downloads.libretexts.org/api/v1/download/human-230520/pages", + "files": "https://downloads.libretexts.org/api/v1/download/human-230520/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/human-230520/thincc" + }, + "summary": "Para vivir offers an introduction to reading different literary and cultural texts from the Spanish-speaking world with a thematic focus on health. It can be used as an alternative to the standard Introduction to Hispanic Literature course texts, as it also teaches techniques of close reading. It incorporates authors from seventeen counties, has an almost even representation of male and female authors and diverse communities in the Hispanic world.", + "lastUpdated": "Sat, 28 Jun 2025 22:11:48 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:mcknight-kuhnheim", + "coverpage:yes", + "license:ccbync", + "licenseversion:40", + "lulu@Para vivir con salud@Kathryn Joy McKnight and Jill S. Kuhnheim@University of New Mexico@Para vivir con salud", + "showtoc:no", + "source@https://opentext.ku.edu/paravivir" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "human-385563", + "title": "Introduction to Philosophy- Ethics", + "author": "George Matthews", + "affiliation": "Plymouth State University", + "library": "human", + "subject": "Philosophy", + "location": "central", + "course": "", + "program": "oeri", + "license": "ccby", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://human.libretexts.org/Bookshelves/Philosophy/Ethics/Introduction_to_Philosophy%3A_Ethics_(Matthews_and_Hendricks)", + "pdf": "https://downloads.libretexts.org/api/v1/download/human-385563/pdf", + "buy": "https://commons.libretexts.org/store/product/human-385563", + "zip": "https://downloads.libretexts.org/api/v1/download/human-385563/pages", + "files": "https://downloads.libretexts.org/api/v1/download/human-385563/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/human-385563/thincc" + }, + "summary": "", + "lastUpdated": "Sat, 07 Mar 2026 04:41:12 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:gmtth", + "coverpage:yes", + "cssprint:default", + "license:ccby", + "licenseversion:40", + "lulu@Introduction to Philosophy- Ethics@George Matthews@Plymouth State University@Introduction to Philosophy- Ethics", + "printoptions:no-header-title", + "program:oeri", + "showtoc:yes" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "socialsci-366888", + "title": "Digital Media Literacy in the Age of Artificial Intelligence", + "author": "Jae Sik Ha", + "affiliation": "University of Illinois Springfield", + "library": "socialsci", + "subject": "", + "location": "campus", + "course": "University of Illinois Springfield", + "program": "carli", + "license": "ccby", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://socialsci.libretexts.org/Courses/University_of_Illinois_Springfield/Digital_Media_Literacy_in_the_Age_of_Artificial_Intelligence%3A_Navigating_Truth_Power_and_Misinformation_v2", + "pdf": "https://downloads.libretexts.org/api/v1/download/socialsci-366888/pdf", + "buy": "https://commons.libretexts.org/store/product/socialsci-366888", + "zip": "https://downloads.libretexts.org/api/v1/download/socialsci-366888/pages", + "files": "https://downloads.libretexts.org/api/v1/download/socialsci-366888/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/socialsci-366888/thincc" + }, + "summary": "", + "lastUpdated": "Fri, 05 Jun 2026 21:28:16 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:jskh", + "coverpage:yes", + "license:ccby", + "licenseversion:40", + "lulu@Digital Media Literacy in the Age of Artificial Intelligence@Jae Sik Ha@University of Illinois Springfield@Digital Media Literacy in the Age of Artificial Intelligence", + "program:carli" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "geo-15555", + "title": "Engaging Adults in Science and Energy", + "author": "Pennsylvania State University", + "affiliation": "Pennsylvania State University", + "library": "geo", + "subject": "Geology", + "location": "central", + "course": "", + "program": "", + "license": "ccbyncsa", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://geo.libretexts.org/Bookshelves/Geology/Engaging_Adults_in_Science_and_Energy", + "pdf": "https://downloads.libretexts.org/api/v1/download/geo-15555/pdf", + "buy": "https://commons.libretexts.org/store/product/geo-15555", + "zip": "https://downloads.libretexts.org/api/v1/download/geo-15555/pages", + "files": "https://downloads.libretexts.org/api/v1/download/geo-15555/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/geo-15555/thincc" + }, + "summary": "This text provided ten weekly educational classes along with field experiences to adults living in rural Pennsylvania where a majority of natural gas exploration and development was occurring producing large-scale change within their communities. The program curriculum focused on energy and energy systems, the scientific process, social and economic impacts, and communication skills to enhance citizens' abilities to engage in civil dialogue around contentious topics.", + "lastUpdated": "Sun, 06 Nov 2022 00:09:22 GMT", + "libraryTags": [ + "article:topic-category", + "authorname:marcellusmatters", + "coverpage:yes", + "license:ccbyncsa", + "licenseversion:40", + "lulu@Engaging Adults in Science and Energy@Pennsylvania State University@@Engaging Adults in Science and Energy", + "showtoc:no", + "source@https://www.e-education.psu.edu/marcellus" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + }, + { + "bookID": "chem-476102", + "title": "CHEM 4110: Advanced Inorganic Chemistry", + "author": "author", + "affiliation": "Catherine McCusker", + "library": "chem", + "subject": "", + "location": "campus", + "course": "East Tennessee State University", + "program": "", + "license": "ccbyncsa", + "thumbnail": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22400%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23d6dde5%22%2F%3E%3C%2Fsvg%3E", + "links": { + "online": "https://chem.libretexts.org/Courses/East_Tennessee_State_University/CHEM_4110%3A_Advanced_Inorganic_Chemistry", + "pdf": "https://downloads.libretexts.org/api/v1/download/chem-476102/pdf", + "buy": "https://commons.libretexts.org/store/product/chem-476102", + "zip": "https://downloads.libretexts.org/api/v1/download/chem-476102/pages", + "files": "https://downloads.libretexts.org/api/v1/download/chem-476102/publication", + "lms": "https://downloads.libretexts.org/api/v1/download/chem-476102/thincc" + }, + "summary": "", + "lastUpdated": "Wed, 22 Jan 2025 20:44:10 GMT", + "libraryTags": [ + "article:topic-category", + "author@Catherine McCusker, East Tennessee State University", + "authorname:cmccusker", + "coverpage:yes", + "inorganic", + "license:ccbyncsa", + "lulu@CHEM 4110: Advanced Inorganic Chemistry@author@Catherine McCusker@East Tennessee State University@CHEM 4110: Advanced Inorganic Chemistry" + ], + "thumbnailIsAnimated": false, + "publicAssets": 0, + "instructorAssets": 0 + } + ] +} diff --git a/client/visual-tests/mocks/fixtures/filters.json b/client/visual-tests/mocks/fixtures/filters.json new file mode 100644 index 000000000..460070f64 --- /dev/null +++ b/client/visual-tests/mocks/fixtures/filters.json @@ -0,0 +1,165 @@ +{ + "authors": [ + "Aaron Lee", + "Aaron Mattox", + "Abbey K. Elder, Stefanie Buck, Jeff Gallant, Marco Seiferle-Valencia, and Apurva Ashok", + "Abdullah Oguz", + "Adam Bledsoe", + "Adam Dastrup", + "Adam MacDougall", + "Adam Rex Pope", + "Adaptive Apparel Design", + "Addie Clark", + "Adrianne Kotecki", + "Adrienne Richards", + "Adrienne Watt", + "Aeschylus", + "A. Goodman", + "Aimee Samara Krouskop, Ben Cushing, Kimberly Puttman, and Avery Temple", + "AJ Barse and Justina Brown", + "Ajna Rivera", + "Alain Olavarrieta", + "Alaka Pradhan", + "Alan Keys", + "Alan Polansky", + "Al Doerr & Ken Levasseur", + "Alessandro Baldi", + "Alex A. G. Taub" + ], + "subjects": [ + "Abstract and Geometric Algebra", + "Accounting", + "Advanced Statistics", + "Aerospace Engineering", + "Agriculture and Horticulture", + "Algebra", + "Allied Health", + "Analysis", + "Analytical Chemistry", + "Anatomy and Physiology", + "Anthropology", + "Applied Mathematics", + "Applied Statistics", + "Arithmetic and Basic Math", + "Art", + "Artificial Intelligence", + "Arts Audio Visual Technology and Communications", + "Astronomy Cosmology", + "Basic Science", + "Biochemistry", + "Biological Chemistry", + "Biological Engineering", + "Biotechnology", + "Botany", + "Business" + ], + "affiliations": [ + "Academic Senate of California Community Colleges Open Education Resources Initiative", + "Achieving the Dream", + "Adler University", + "African Virtual University", + "Al Akhawayn University", + "Albuquerque Public Schools & George Mason University", + "Alfred State College of Technology SUNY", + "Allan Hancock College", + "Alma College", + "Alqua", + "American Meteorological Society", + "American Public University", + "American River College", + "American University", + "American University of Beirut", + "Angelina College", + "Angelo State University", + "Anoka-Ramsey Community College", + "Antelope Valley College", + "Arapahoe Community College", + "Archabbey of St. Meinrad in Indiana", + "Arizona State University", + "Arkansas Northeastern College", + "Arkansas State University-Beebe", + "Arkansas State University- Beebe" + ], + "courses": [ + "Achieving the Dream", + "Adler University", + "Al Akhawayn University", + "Allan Hancock College", + "Alma College", + "American Meteorological Society", + "American Public University", + "American River College", + "Angelina College", + "Angelo State University", + "Anoka-Ramsey Community College", + "Antelope Valley College", + "Arapahoe Community College", + "Arkansas Northeastern College", + "Arkansas State University-Beebe", + "Arkansas Tech University", + "Athabasca University", + "Aurora University", + "Austin Peay State University", + "Baker College", + "Bakersfield College", + "Barry University", + "Barstow Community College", + "Barton Community College", + "Bellarmine University" + ], + "programs": [ + "athabasca", + "bcc", + "cali", + "carli", + "ck12", + "clc-innovators", + "ecampusontario", + "echo", + "evc", + "galileo", + "hidden", + "ilc", + "lumen", + "mitocw", + "oeri", + "openoregon", + "openrn", + "openstax", + "openstaxcnx", + "opensuny", + "opsuny", + "pdxopen", + "rotel", + "virginiatech", + "wac" + ], + "cids": [ + "ACCT 110", + "ACCT 120", + "ADS 110 X", + "ADS 120 X", + "ADS 130 X", + "ADS 140 X", + "ADS 150 X", + "ADS 160 X", + "ADS 170 X", + "ADS 180 X", + "ADS 190 X", + "ADS 195 X", + "ADS 200 X", + "ADS 210 X", + "AG-AB 104", + "AG-AB 108 108", + "AG-AB 112", + "AG-AB 124", + "AG-AB 128", + "AG-AS 104", + "AG-AS 108 L", + "AG-AS 112 L", + "AG-AS 116 L", + "AG-AS 120 L", + "AG-AS 124 L" + ], + "err": false +} diff --git a/client/visual-tests/mocks/fixtures/libregrid.json b/client/visual-tests/mocks/fixtures/libregrid.json new file mode 100644 index 000000000..95b4224d5 --- /dev/null +++ b/client/visual-tests/mocks/fixtures/libregrid.json @@ -0,0 +1,60 @@ +{ + "orgs": [ + { + "orgID": "oeri", + "name": "ASCCC Open Educational Resources Initiative", + "domain": "https://oeri.commons.libretexts.org" + }, + { + "name": "California State University, Sacramento", + "orgID": "csusacramento", + "domain": "https://csus.commons.libretexts.org" + }, + { + "orgID": "chabot", + "name": "Chabot College", + "domain": "https://chabot.commons.libretexts.org" + }, + { + "orgID": "canyons", + "name": "College of the Canyons", + "domain": "https://canyons.commons.libretexts.org" + }, + { + "domain": "https://deanza.commons.libretexts.org", + "name": "De Anza College", + "orgID": "deanza" + }, + { + "orgID": "hacc", + "name": "HACC", + "domain": "https://hacc.commons.libretexts.org" + }, + { + "orgID": "highline", + "name": "Highline College", + "domain": "https://highline.commons.libretexts.org" + }, + { + "orgID": "kstate", + "name": "Kansas State University", + "domain": "https://k-state.commons.libretexts.org" + }, + { + "orgID": "pgcc", + "name": "Prince George's Community College", + "domain": "https://pgcc.commons.libretexts.org" + }, + { + "orgID": "reedley", + "name": "Reedley College", + "domain": "https://reedley.commons.libretexts.org" + }, + { + "orgID": "ucdavis", + "name": "University of California, Davis", + "domain": "https://ucdavis.commons.libretexts.org" + } + ], + "err": false +} diff --git a/client/visual-tests/utils/actions.ts b/client/visual-tests/utils/actions.ts new file mode 100644 index 000000000..e153b0124 --- /dev/null +++ b/client/visual-tests/utils/actions.ts @@ -0,0 +1,43 @@ +import type { Page } from '@playwright/test'; +import { wait0s, type WaitFn } from './waits'; + +/** + * Optional wait before click, click a match by 0-based index (default first), + * then optional wait after. Avoids strict-mode failures on multi-match selectors. + * + * @example + * await clickOn(page, 'button:has-text("Login")'); + * await clickOn(page, 'a[href^="/book/"]', wait0s, wait5s); + * await clickOn(page, 'a[href^="/book/"]', wait0s, wait5s, 2); // 3rd book + */ +export async function clickOn( + page: Page, + selector: string, + beforeActionWait: WaitFn = wait0s, + afterActionWait: WaitFn = wait0s, + index = 0, +): Promise { + await beforeActionWait(page); + await page.locator(selector).nth(index).click(); + await afterActionWait(page); +} + +/** + * Optional wait first, then type into an input matched by selector. + * Clears existing value first. + * + * @example + * await typeIn(page, '#email', 'admin@example.com'); + * await typeIn(page, 'input[name="password"]', 'secret', wait5s); + */ +export async function typeIn( + page: Page, + selector: string, + text: string, + beforeActionWait: WaitFn = wait0s, +): Promise { + await beforeActionWait(page); + const field = page.locator(selector).first(); + await field.click(); + await field.fill(text); +} diff --git a/client/visual-tests/utils/index.ts b/client/visual-tests/utils/index.ts new file mode 100644 index 000000000..b78ec803f --- /dev/null +++ b/client/visual-tests/utils/index.ts @@ -0,0 +1,11 @@ +export { goToPage } from './navigation'; +export { clickOn, typeIn } from './actions'; +export { + WAITS, + wait0s, + wait5s, + wait10s, + wait30s, + wait45s, + type WaitFn, +} from './waits'; diff --git a/client/visual-tests/utils/navigation.ts b/client/visual-tests/utils/navigation.ts new file mode 100644 index 000000000..2114bed23 --- /dev/null +++ b/client/visual-tests/utils/navigation.ts @@ -0,0 +1,19 @@ +import type { Page } from '@playwright/test'; +import { wait0s, type WaitFn } from './waits'; + +/** + * Optional wait first, then navigate to a path (relative to Playwright baseURL) + * or absolute URL, then wait for the document to load. + * + * @example + * await goToPage(page, '/fallback-auth'); + * await goToPage(page, '/fallback-auth', wait5s); + */ +export async function goToPage( + page: Page, + pathOrUrl: string, + beforeActionWait: WaitFn = wait0s, +): Promise { + await beforeActionWait(page); + await page.goto(pathOrUrl, { waitUntil: 'load' }); +} diff --git a/client/visual-tests/utils/waits.ts b/client/visual-tests/utils/waits.ts new file mode 100644 index 000000000..6d3d5aaa1 --- /dev/null +++ b/client/visual-tests/utils/waits.ts @@ -0,0 +1,32 @@ +import type { Page } from '@playwright/test'; + +/** Fixed pause helpers — call by name so wait intent stays obvious in specs. */ +export async function wait0s(_page?: Page): Promise { + // no-op placeholder for a consistent API with the other waits +} + +export async function wait5s(page: Page): Promise { + await page.waitForTimeout(5_000); +} + +export async function wait10s(page: Page): Promise { + await page.waitForTimeout(10_000); +} + +export async function wait30s(page: Page): Promise { + await page.waitForTimeout(30_000); +} + +export async function wait45s(page: Page): Promise { + await page.waitForTimeout(45_000); +} + +export type WaitFn = (page: Page) => Promise; + +export const WAITS = { + none: wait0s, + '5s': wait5s, + '10s': wait10s, + '30s': wait30s, + '45s': wait45s, +} as const; diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md new file mode 100644 index 000000000..25e711ab1 --- /dev/null +++ b/docs/visual-regression-testing.md @@ -0,0 +1,142 @@ +# Visual regression testing + +Playwright + Percy. Shared helpers live in `client/visual-tests/base.ts` and +`client/visual-tests/utils/`. Reusable flows live in `client/visual-tests/flows/`. + +## What CI runs (default) + +`npm run test:visual` runs **one** journey (`--project=journey`): + +1. Login at `/fallback-auth` (screenshot) +2. Go to Commons `/`, then for the 1st, 3rd, and 5th books: screenshot catalog, + open the book, screenshot again + +That is a single browser / single test with multiple Percy snapshots in one build — +not separate login/books tests. + +## Mocked API (no backend needed) + +The visual tests check how pages **look**, not whether the backend works. CI has +no server or database, so `client/visual-tests/mocks/api.ts` answers every +`/api/v1/*` request with fixed data from `client/visual-tests/mocks/fixtures/`. +Local runs use the same mocks by default; the explicit `:real` commands below +use your running backend instead. + +`VITE_DEV_BASE_URL=http://localhost:5000` gives the frontend an address for +constructing API URLs. Playwright intercepts those requests before they reach a +server; nothing needs to listen on port 5000. The GitHub workflow already sets +this value directly, so **do not add it as a GitHub secret**. Only `PERCY_TOKEN` +is needed as a secret for Percy uploads. + +- Login is faked: the mocked `POST /auth/fallback-auth` sets the auth cookies the + client checks for. Any email/password works; no credentials are needed. +- The catalog always returns the same books in the same order, with a flat + placeholder instead of remote cover images, so Percy only diffs on UI changes. +- An endpoint without a mock returns 404 and logs + `[visual mocks] unmocked API call: ...`. When you snapshot a new page, add a + route for each call it logs. + +## Isolated specs (opt-in only) + +Login-only and books-only specs live under `visual-tests/isolated/` and run only when +you ask for them: + +```sh +npm run test:visual:isolated +npm run test:visual:isolated:headed +``` + +## One-time Percy setup + +1. Open the existing Percy Web project (or create one). +2. Copy its write-only project token from the Percy project settings. +3. In GitHub, open **Settings → Secrets and variables → Actions** and create + repository secrets: + - `PERCY_TOKEN` — Percy write token +4. Install the Percy GitHub app for this repository and link it to the Percy + project. +5. Run the Visual Regression workflow on `staging` once to establish its baseline. + Pushes to both `staging` and `master` refresh their respective baselines after + a pull request has been reviewed and merged. + +The token must never be committed. Pull requests from forks do not +receive secrets; their workflow still verifies that the browser scenario loads, +but it does not upload snapshots to Percy. + +## Run locally + +Best way to preview what the journey captures (visible browser): + +```sh +cd client +npx playwright install chromium +npm run test:visual:mock:headed +``` + +Screenshots are written to **`client/test-results/visual/`** (gitignored). Open that +folder after the run to review PNGs at 375 and 1280 widths. No Percy upload. + +The `:mock` commands explicitly set the same API address as CI and run the same +mocked journey. Playwright starts the frontend at `http://localhost:3000` (or +reuses an existing frontend there). No backend, database, or login credentials +are needed. The older `:local` and `:headed` commands also use mocks; they do not +test your real local backend or database. Local PNG rendering can differ from +Percy's rendering, but the journey and API fixtures are shared. + +Other local commands: + +```sh +# Same journey, headless (still writes local PNGs) +npm run test:visual:mock + +# Optional isolated pieces only +npm run test:visual:isolated:headed + +# Percy upload (same journey CI uses; no local PNGs) +PERCY_TOKEN=... npm run test:visual +``` + +Flows call one helper (`captureVisualSnapshot`): Percy on `npm run test:visual` / +CI, local PNGs on `:local` / `:headed`. Never both. + +No backend or login credentials are needed (see **Mocked API** above). +`PERCY_TOKEN` is a GitHub Actions secret (or set in the shell for local Percy runs) — +it is not stored in `client/.env`. + +## Run locally against your real backend and database + +Start your local backend and its database as usual. In `client/.env`, set: + +```dotenv +VITE_DEV_BASE_URL=http://localhost:5000 +VISUAL_AUTH_EMAIL=your-local-test-account@example.com +VISUAL_AUTH_PASSWORD=your-local-test-account-password +``` + +Use the backend's actual URL if it runs on a different port. These must be valid +fallback-auth credentials for an account in the database your backend uses. +Keep credentials in your uncommitted `.env`; do not prefix them with `VITE_`. + +From `client`, run either: + +```sh +# Real API and login, headless browser +npm run test:visual:real + +# Real API and login, visible browser +npm run test:visual:real:headed +``` + +These commands set `VISUAL_USE_REAL_BACKEND=1`, skip API mocks and fake cookies, +and read `VISUAL_AUTH_EMAIL` / `VISUAL_AUTH_PASSWORD` from your environment or +`client/.env`. They start or reuse the frontend on port 3000, but do not start +the backend or database. A reused frontend must already use the correct API URL; +restart it if you changed its configuration. + +The journey logs in and opens the 1st, 3rd, and 5th Commons books, so your database +must provide at least five visible catalog books. Screenshots are saved to +`client/test-results/visual/` with no Percy upload. Live data can change between +runs. The external support widget remains blocked in both modes. + +Use `:mock` / `:mock:headed` to return to the fixed data used by CI. Avoid setting +`VISUAL_USE_REAL_BACKEND` in `.env`; the commands select the mode for you.