diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 45030acf..ea118360 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -5,7 +5,28 @@ on: types: [ published ] jobs: + test: + runs-on: ubuntu-latest + name: Run Tests Before Publishing + + steps: + - name: 📥 Checkout Code + uses: actions/checkout@v4 + + - name: 🔧 Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: 📦 Install Dependencies + run: npm ci + + - name: ✅ Run Tests + run: npm run test + publish-prime: + needs: test runs-on: ubuntu-latest defaults: run: @@ -30,6 +51,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} publish-plugin: + needs: test runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cdd190dd..acf8ae12 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -10,6 +10,29 @@ on: - cron: '0 0 * * *' # Daily at midnight jobs: + javascript-tests: + runs-on: ubuntu-latest + name: JavaScript Tests + + steps: + - name: 📥 Checkout Code + uses: actions/checkout@v4 + + - name: 🔧 Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: 📦 Install Dependencies + run: npm ci + + - name: ✅ Run Tests + run: npm run test + + - name: 📊 Run Coverage + run: npm run test:coverage + static-analysis: runs-on: ubuntu-latest strategy: diff --git a/.gitignore b/.gitignore index 90a2f7ee..e2ab1022 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ /vendor/ /node_modules/ +/coverage/ + /.cache/php-cs-fixer/ /.cache/phplint/ /.cache/phpstan/ diff --git a/package-lock.json b/package-lock.json index 2ce31c7d..aa398b27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,8 @@ "@types/node": "^22.19.11", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "@vitest/coverage-v8": "^3.0.7", + "@vitest/ui": "^3.0.7", "all-contributors-cli": "^6.26.1", "autoprefixer": "^10.4.24", "browserslist": "^4.28.1", @@ -42,6 +44,7 @@ "eslint-plugin-babel": "^5.3.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", + "jsdom": "^26.0.0", "postcss": "^8.5.6", "postcss-discard-comments": "^7.0.5", "punycode": "^2.3.1", @@ -56,13 +59,28 @@ "sass": "^1.97.3", "ts-node": "^10.9.2", "tslib": "^2.8.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^3.0.7" }, "engines": { "node": ">=16.0.0", "npm": ">=8.0.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@antfu/eslint-config": { "version": "2.27.3", "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-2.27.3.tgz", @@ -415,6 +433,27 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1942,6 +1981,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@clack/core": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz", @@ -2005,6 +2054,121 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", @@ -2054,6 +2218,448 @@ "node": ">=16" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-plugin-eslint-comments": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.4.1.tgz", @@ -2395,6 +3001,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3103,6 +3719,13 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-babel": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", @@ -3887,6 +4510,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/doctrine": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", @@ -4722,6 +5363,40 @@ "win32" ] }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/eslint-plugin": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.4.2.tgz", @@ -4799,6 +5474,137 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/ui/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", @@ -5377,6 +6183,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -5657,6 +6502,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -5702,22 +6557,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/cacache/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -5859,6 +6698,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5916,6 +6772,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -6448,6 +7314,20 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6455,6 +7335,57 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -6537,6 +7468,23 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6924,9 +7872,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -6990,6 +7938,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8298,6 +9288,16 @@ "dev": true, "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -8406,6 +9406,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9134,6 +10141,26 @@ "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -9780,6 +10807,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -9949,6 +10983,76 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", @@ -10018,6 +11122,131 @@ "node": ">=12.0.0" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/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/jsdom/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/jsdom/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/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -10299,6 +11528,13 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10319,6 +11555,47 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -11018,6 +12295,16 @@ "dev": true, "license": "MIT" }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11563,6 +12850,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -11980,6 +13274,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12061,6 +13381,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pegjs": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", @@ -13155,22 +14485,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", @@ -14712,6 +16026,13 @@ "dev": true, "license": "MIT" }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -14884,6 +16205,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scslre": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", @@ -15074,6 +16408,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -15176,6 +16517,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -15379,6 +16735,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -15563,6 +16933,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-inject": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", @@ -15674,6 +17064,13 @@ "url": "https://github.com/sponsors/limonte" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/synckit": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", @@ -15788,6 +17185,122 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", + "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -15802,6 +17315,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -15826,6 +17346,56 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -15890,6 +17460,29 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -16444,6 +18037,303 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { "version": "9.4.3", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", @@ -16513,6 +18403,29 @@ "node": ">=10" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -16520,6 +18433,43 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -16643,6 +18593,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -16787,6 +18754,28 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -16797,6 +18786,13 @@ "node": ">=12" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 06f76e4b..63a9cb97 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,16 @@ "build": "cross-env NODE_ENV=production rollup -c", "clean": "rimraf src/*/Prime/Resources/dist", "link": "npm link --workspaces", - "ncu": "ncu -u && npm run ncu --workspaces" + "ncu": "ncu -u && npm run ncu --workspaces", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:ui": "vitest --ui" }, "devDependencies": { "@antfu/eslint-config": "^2.27.3", + "@vitest/coverage-v8": "^3.0.7", + "@vitest/ui": "^3.0.7", "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", "@rollup/plugin-babel": "^6.1.0", @@ -84,6 +90,8 @@ "sass": "^1.97.3", "ts-node": "^10.9.2", "tslib": "^2.8.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^3.0.7", + "jsdom": "^26.0.0" } } diff --git a/src/Prime/Resources/assets/flasher-plugin.ts b/src/Prime/Resources/assets/flasher-plugin.ts index c3a5129f..db952eba 100644 --- a/src/Prime/Resources/assets/flasher-plugin.ts +++ b/src/Prime/Resources/assets/flasher-plugin.ts @@ -104,7 +104,9 @@ export default class FlasherPlugin extends AbstractPlugin { // Apply custom styles Object.entries(options.style).forEach(([key, value]) => { if (value !== undefined && value !== null) { - container.style.setProperty(key, String(value)) + // Convert camelCase to kebab-case for CSS property names + const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase() + container.style.setProperty(cssKey, String(value)) } }) diff --git a/src/Prime/Resources/assets/flasher.ts b/src/Prime/Resources/assets/flasher.ts index b6429310..85eba424 100644 --- a/src/Prime/Resources/assets/flasher.ts +++ b/src/Prime/Resources/assets/flasher.ts @@ -128,7 +128,7 @@ export default class Flasher extends AbstractPlugin { envelope.metadata.plugin = this.resolvePluginAlias(envelope.metadata.plugin) this.addThemeStyles(resolved, envelope.metadata.plugin) envelope.options = this.resolveOptions(envelope.options) - envelope.context = response.context as Context + envelope.context = resolved.context }) return resolved @@ -156,17 +156,32 @@ export default class Flasher extends AbstractPlugin { const functionRegex = /^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/ const arrowFunctionRegex = /^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/ - const match = func.match(functionRegex) || func.match(arrowFunctionRegex) - if (!match) { + const functionMatch = func.match(functionRegex) + const arrowMatch = func.match(arrowFunctionRegex) + + if (!functionMatch && !arrowMatch) { return func } - const args = match[2]?.split(',').map((arg) => arg.trim()) ?? [] - let body = match[3].trim() + let args: string[] + let body: string - // Arrow functions with a single expression can omit the curly braces and the return keyword - if (!body.startsWith('{')) { - body = `{ return ${body}; }` + if (functionMatch) { + // Regular function: body is already complete statements + args = functionMatch[2]?.split(',').map((arg) => arg.trim()).filter(Boolean) ?? [] + body = functionMatch[3].trim() + } else { + // Arrow function: may need to wrap expression body with return + args = arrowMatch![2]?.split(',').map((arg) => arg.trim()).filter(Boolean) ?? [] + body = arrowMatch![3].trim() + + // Arrow functions with a single expression need return added + if (!body.startsWith('{')) { + body = `return ${body};` + } else { + // Remove outer braces for arrow functions with block body + body = body.slice(1, -1).trim() + } } try { diff --git a/src/Prime/Resources/dist/flasher.esm.js b/src/Prime/Resources/dist/flasher.esm.js index dc33683f..9a810d6a 100644 --- a/src/Prime/Resources/dist/flasher.esm.js +++ b/src/Prime/Resources/dist/flasher.esm.js @@ -190,7 +190,8 @@ class FlasherPlugin extends AbstractPlugin { container.dataset.position = options.position; Object.entries(options.style).forEach(([key, value]) => { if (value !== undefined && value !== null) { - container.style.setProperty(key, String(value)); + const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase(); + container.style.setProperty(cssKey, String(value)); } }); document.body.appendChild(container); @@ -413,7 +414,7 @@ class Flasher extends AbstractPlugin { envelope.metadata.plugin = this.resolvePluginAlias(envelope.metadata.plugin); this.addThemeStyles(resolved, envelope.metadata.plugin); envelope.options = this.resolveOptions(envelope.options); - envelope.context = response.context; + envelope.context = resolved.context; }); return resolved; } @@ -428,20 +429,32 @@ class Flasher extends AbstractPlugin { return resolved; } resolveFunction(func) { - var _a, _b; + var _a, _b, _c, _d; if (typeof func !== 'string') { return func; } const functionRegex = /^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/; const arrowFunctionRegex = /^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/; - const match = func.match(functionRegex) || func.match(arrowFunctionRegex); - if (!match) { + const functionMatch = func.match(functionRegex); + const arrowMatch = func.match(arrowFunctionRegex); + if (!functionMatch && !arrowMatch) { return func; } - const args = (_b = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim())) !== null && _b !== void 0 ? _b : []; - let body = match[3].trim(); - if (!body.startsWith('{')) { - body = `{ return ${body}; }`; + let args; + let body; + if (functionMatch) { + args = (_b = (_a = functionMatch[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : []; + body = functionMatch[3].trim(); + } + else { + args = (_d = (_c = arrowMatch[2]) === null || _c === void 0 ? void 0 : _c.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _d !== void 0 ? _d : []; + body = arrowMatch[3].trim(); + if (!body.startsWith('{')) { + body = `return ${body};`; + } + else { + body = body.slice(1, -1).trim(); + } } try { return new Function(...args, body); diff --git a/src/Prime/Resources/dist/flasher.js b/src/Prime/Resources/dist/flasher.js index 6a5ec738..2ed70d64 100644 --- a/src/Prime/Resources/dist/flasher.js +++ b/src/Prime/Resources/dist/flasher.js @@ -196,7 +196,8 @@ container.dataset.position = options.position; Object.entries(options.style).forEach(([key, value]) => { if (value !== undefined && value !== null) { - container.style.setProperty(key, String(value)); + const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase(); + container.style.setProperty(cssKey, String(value)); } }); document.body.appendChild(container); @@ -419,7 +420,7 @@ envelope.metadata.plugin = this.resolvePluginAlias(envelope.metadata.plugin); this.addThemeStyles(resolved, envelope.metadata.plugin); envelope.options = this.resolveOptions(envelope.options); - envelope.context = response.context; + envelope.context = resolved.context; }); return resolved; } @@ -434,20 +435,32 @@ return resolved; } resolveFunction(func) { - var _a, _b; + var _a, _b, _c, _d; if (typeof func !== 'string') { return func; } const functionRegex = /^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/; const arrowFunctionRegex = /^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/; - const match = func.match(functionRegex) || func.match(arrowFunctionRegex); - if (!match) { + const functionMatch = func.match(functionRegex); + const arrowMatch = func.match(arrowFunctionRegex); + if (!functionMatch && !arrowMatch) { return func; } - const args = (_b = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim())) !== null && _b !== void 0 ? _b : []; - let body = match[3].trim(); - if (!body.startsWith('{')) { - body = `{ return ${body}; }`; + let args; + let body; + if (functionMatch) { + args = (_b = (_a = functionMatch[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : []; + body = functionMatch[3].trim(); + } + else { + args = (_d = (_c = arrowMatch[2]) === null || _c === void 0 ? void 0 : _c.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _d !== void 0 ? _d : []; + body = arrowMatch[3].trim(); + if (!body.startsWith('{')) { + body = `return ${body};`; + } + else { + body = body.slice(1, -1).trim(); + } } try { return new Function(...args, body); diff --git a/src/Prime/Resources/dist/flasher.min.js b/src/Prime/Resources/dist/flasher.min.js index d34b9128..62b72f2d 100644 --- a/src/Prime/Resources/dist/flasher.min.js +++ b/src/Prime/Resources/dist/flasher.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{null!=s&&t.style.setProperty(e,String(s))})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n),e.addEventListener("mouseout",(()=>{clearInterval(r),r=window.setInterval(i,n)})),e.addEventListener("mouseover",(()=>clearInterval(r)))}removeNotification(e){e&&(e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");return t.innerHTML=e.trim(),t.content.firstElementChild}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((s=>{s.metadata=s.metadata||{},s.metadata.plugin=this.resolvePluginAlias(s.metadata.plugin),this.addThemeStyles(t,s.metadata.plugin),s.options=this.resolveOptions(s.options),s.context=e.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s;if("string"!=typeof e)return e;const n=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/)||e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!n)return e;const r=null!==(s=null===(t=n[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())))&&void 0!==s?s:[];let o=n[3].trim();o.startsWith("{")||(o=`{ return ${o}; }`);try{return new Function(...r,o)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){return document.querySelector(`${"style"===s?"link":"script"}[src="${e}"]`)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{if(null!=s){const n=e.replace(/([A-Z])/g,"-$1").toLowerCase();t.style.setProperty(n,String(s))}})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n),e.addEventListener("mouseout",(()=>{clearInterval(r),r=window.setInterval(i,n)})),e.addEventListener("mouseover",(()=>clearInterval(r)))}removeNotification(e){e&&(e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");return t.innerHTML=e.trim(),t.content.firstElementChild}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((e=>{e.metadata=e.metadata||{},e.metadata.plugin=this.resolvePluginAlias(e.metadata.plugin),this.addThemeStyles(t,e.metadata.plugin),e.options=this.resolveOptions(e.options),e.context=t.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s,n,r;if("string"!=typeof e)return e;const o=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/),i=e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!o&&!i)return e;let l,a;o?(l=null!==(s=null===(t=o[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==s?s:[],a=o[3].trim()):(l=null!==(r=null===(n=i[2])||void 0===n?void 0:n.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==r?r:[],a=i[3].trim(),a=a.startsWith("{")?a.slice(1,-1).trim():`return ${a};`);try{return new Function(...l,a)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){return document.querySelector(`${"style"===s?"link":"script"}[src="${e}"]`)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); diff --git a/src/Prime/Resources/public/flasher.min.js b/src/Prime/Resources/public/flasher.min.js index d34b9128..62b72f2d 100644 --- a/src/Prime/Resources/public/flasher.min.js +++ b/src/Prime/Resources/public/flasher.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{null!=s&&t.style.setProperty(e,String(s))})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n),e.addEventListener("mouseout",(()=>{clearInterval(r),r=window.setInterval(i,n)})),e.addEventListener("mouseover",(()=>clearInterval(r)))}removeNotification(e){e&&(e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");return t.innerHTML=e.trim(),t.content.firstElementChild}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((s=>{s.metadata=s.metadata||{},s.metadata.plugin=this.resolvePluginAlias(s.metadata.plugin),this.addThemeStyles(t,s.metadata.plugin),s.options=this.resolveOptions(s.options),s.context=e.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s;if("string"!=typeof e)return e;const n=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/)||e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!n)return e;const r=null!==(s=null===(t=n[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())))&&void 0!==s?s:[];let o=n[3].trim();o.startsWith("{")||(o=`{ return ${o}; }`);try{return new Function(...r,o)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){return document.querySelector(`${"style"===s?"link":"script"}[src="${e}"]`)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).flasher=t()}(this,(function(){"use strict";function e(e,t,s,n){return new(s||(s=Promise))((function(r,o){function i(e){try{a(n.next(e))}catch(e){o(e)}}function l(e){try{a(n.throw(e))}catch(e){o(e)}}function a(e){var t;e.done?r(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(i,l)}a((n=n.apply(e,t||[])).next())}))}"function"==typeof SuppressedError&&SuppressedError;class t{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,n){let r,o,i,l={};if("object"==typeof e?(l=Object.assign({},e),r=l.type,o=l.message,i=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),r=e,o=l.message,i=l.title,delete l.message,delete l.title):(r=e,o=t,null==s?(i=void 0,l=n||{}):"string"==typeof s?(i=s,l=n||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(i=l.title,delete l.title):i=void 0,n&&"object"==typeof n&&(l=Object.assign(Object.assign({},l),n)))),!r)throw new Error("Type is required for notifications");if(null==o)throw new Error("Message is required for notifications");null==i&&(i=r.charAt(0).toUpperCase()+r.slice(1));const a={type:r,message:o,title:i,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}class s extends t{constructor(e){if(super(),this.options={timeout:null,timeouts:{success:1e4,info:1e4,error:1e4,warning:1e4},fps:30,position:"top-right",direction:"top",rtl:!1,style:{},escapeHtml:!1},!e)throw new Error("Theme is required");if("function"!=typeof e.render)throw new TypeError("Theme must have a render function");this.theme=e}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t=()=>{e.forEach((e=>{var t,s,n,r;try{const o=null!==(s=null!==(t=this.options.timeout)&&void 0!==t?t:this.options.timeouts[e.type])&&void 0!==s?s:1e4,i=Object.assign(Object.assign(Object.assign({},this.options),e.options),{timeout:this.normalizeTimeout(null!==(n=e.options.timeout)&&void 0!==n?n:o),escapeHtml:null!==(r=e.options.escapeHtml)&&void 0!==r?r:this.options.escapeHtml}),l=this.createContainer(i),a={direction:i.direction,timeout:Number(i.timeout||0),fps:i.fps,rtl:i.rtl,escapeHtml:i.escapeHtml};this.addToContainer(l,e,a)}catch(t){console.error("PHPFlasher: Error rendering envelope",t,e)}}))};"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()}renderOptions(e){e&&(this.options=Object.assign(Object.assign({},this.options),e))}createContainer(e){let t=document.querySelector(`.fl-wrapper[data-position="${e.position}"]`);return t||(t=document.createElement("div"),t.className="fl-wrapper",t.dataset.position=e.position,Object.entries(e.style).forEach((([e,s])=>{if(null!=s){const n=e.replace(/([A-Z])/g,"-$1").toLowerCase();t.style.setProperty(n,String(s))}})),document.body.appendChild(t)),t.dataset.turboTemporary="",t}addToContainer(e,t,s){s.escapeHtml&&(t.title=this.escapeHtml(t.title),t.message=this.escapeHtml(t.message));const n=this.stringToHTML(this.theme.render(t));n.classList.add("fl-container"),s.rtl&&n.classList.add("fl-rtl"),"bottom"===s.direction?e.append(n):e.prepend(n),requestAnimationFrame((()=>n.classList.add("fl-show")));const r=n.querySelector(".fl-close");if(r&&r.addEventListener("click",(e=>{e.stopPropagation(),this.removeNotification(n)})),s.timeout>0)this.addTimer(n,s);else{n.classList.add("fl-sticky");const e=n.querySelector(".fl-progress-bar");if(e){const t=document.createElement("span");t.classList.add("fl-progress","fl-sticky-progress"),t.style.width="100%",e.append(t)}}}normalizeTimeout(e){return!1===e||"number"==typeof e&&e<0||null==e?0:Number(e)||0}addTimer(e,{timeout:t,fps:s}){if(t<=0)return;const n=1e3/s;let r,o=0;const i=()=>{o+=n;const s=e.querySelector(".fl-progress-bar");if(s){let e=s.querySelector(".fl-progress");e||(e=document.createElement("span"),e.classList.add("fl-progress"),s.append(e));const n=100*(1-o/t);e.style.width=`${Math.max(0,n)}%`}o>=t&&(clearInterval(r),this.removeNotification(e))};r=window.setInterval(i,n),e.addEventListener("mouseout",(()=>{clearInterval(r),r=window.setInterval(i,n)})),e.addEventListener("mouseover",(()=>clearInterval(r)))}removeNotification(e){e&&(e.classList.remove("fl-show"),e.ontransitionend=()=>{const t=e.parentElement;e.remove(),t&&!t.hasChildNodes()&&t.remove()})}stringToHTML(e){const t=document.createElement("template");return t.innerHTML=e.trim(),t.content.firstElementChild}escapeHtml(e){if(null==e)return"";const t={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","=":"=","/":"/"};return e.replace(/[&<>"'`=/]/g,(e=>t[e]||e))}}const n=new class extends t{constructor(){super(...arguments),this.defaultPlugin="flasher",this.plugins=new Map,this.themes=new Map,this.loadedAssets=new Set}render(t){return e(this,void 0,void 0,(function*(){const e=this.resolveResponse(t);try{yield this.addAssets([{urls:e.styles,nonce:e.context.csp_style_nonce,type:"style"},{urls:e.scripts,nonce:e.context.csp_script_nonce,type:"script"}]),this.renderOptions(e.options),this.renderEnvelopes(e.envelopes)}catch(e){console.error("PHPFlasher: Error rendering notifications",e)}}))}renderEnvelopes(e){if(!(null==e?void 0:e.length))return;const t={};e.forEach((e=>{const s=this.resolvePluginAlias(e.metadata.plugin);t[s]=t[s]||[],t[s].push(e)})),Object.entries(t).forEach((([e,t])=>{try{this.use(e).renderEnvelopes(t)}catch(t){console.error(`PHPFlasher: Error rendering envelopes for plugin "${e}"`,t)}}))}renderOptions(e){e&&Object.entries(e).forEach((([e,t])=>{try{this.use(e).renderOptions(t)}catch(t){console.error(`PHPFlasher: Error applying options for plugin "${e}"`,t)}}))}addPlugin(e,t){if(!e||!t)throw new Error("Both plugin name and instance are required");this.plugins.set(e,t)}addTheme(e,t){if(!e||!t)throw new Error("Both theme name and definition are required");this.themes.set(e,t)}use(e){const t=this.resolvePluginAlias(e);this.resolvePlugin(t);const s=this.plugins.get(t);if(!s)throw new Error(`Unable to resolve "${t}" plugin, did you forget to register it?`);return s}create(e){return this.use(e)}resolveResponse(e){const t=Object.assign({envelopes:[],options:{},scripts:[],styles:[],context:{}},e);return Object.entries(t.options).forEach((([e,s])=>{t.options[e]=this.resolveOptions(s)})),t.context.csp_style_nonce=t.context.csp_style_nonce||"",t.context.csp_script_nonce=t.context.csp_script_nonce||"",t.envelopes.forEach((e=>{e.metadata=e.metadata||{},e.metadata.plugin=this.resolvePluginAlias(e.metadata.plugin),this.addThemeStyles(t,e.metadata.plugin),e.options=this.resolveOptions(e.options),e.context=t.context})),t}resolveOptions(e){if(!e)return{};const t=Object.assign({},e);return Object.entries(t).forEach((([e,s])=>{t[e]=this.resolveFunction(s)})),t}resolveFunction(e){var t,s,n,r;if("string"!=typeof e)return e;const o=e.match(/^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/),i=e.match(/^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/);if(!o&&!i)return e;let l,a;o?(l=null!==(s=null===(t=o[2])||void 0===t?void 0:t.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==s?s:[],a=o[3].trim()):(l=null!==(r=null===(n=i[2])||void 0===n?void 0:n.split(",").map((e=>e.trim())).filter(Boolean))&&void 0!==r?r:[],a=i[3].trim(),a=a.startsWith("{")?a.slice(1,-1).trim():`return ${a};`);try{return new Function(...l,a)}catch(t){return console.error("PHPFlasher: Error converting string to function:",t),e}}resolvePlugin(e){if(this.plugins.get(e)||!e.includes("theme."))return;const t=e.replace("theme.",""),n=this.themes.get(t);n&&this.addPlugin(e,new s(n))}resolvePluginAlias(e){return"flasher"===(e=e||this.defaultPlugin)?"theme.flasher":e}addAssets(t){return e(this,void 0,void 0,(function*(){try{const e=t.filter((e=>"style"===e.type)),s=[];for(const{urls:t,nonce:n,type:r}of e)if(null==t?void 0:t.length)for(const e of t)e&&!this.loadedAssets.has(e)&&(s.push(this.loadAsset(e,n,r)),this.loadedAssets.add(e));yield Promise.all(s);const n=t.filter((e=>"script"===e.type));for(const{urls:e,nonce:t,type:s}of n)if(null==e?void 0:e.length)for(const n of e)n&&!this.loadedAssets.has(n)&&(yield this.loadAsset(n,t,s),this.loadedAssets.add(n))}catch(e){console.error("PHPFlasher: Error loading assets",e)}}))}loadAsset(e,t,s){return document.querySelector(`${"style"===s?"link":"script"}[src="${e}"]`)?Promise.resolve():new Promise(((n,r)=>{const o=document.createElement("style"===s?"link":"script");"style"===s?(o.rel="stylesheet",o.href=e):(o.type="text/javascript",o.src=e),t&&o.setAttribute("nonce",t),o.onload=()=>n(),o.onerror=()=>r(new Error(`Failed to load ${e}`)),document.head.appendChild(o)}))}addThemeStyles(e,t){if("flasher"!==t&&!t.includes("theme."))return;const s=t.replace("theme.",""),n=this.themes.get(s);if(!(null==n?void 0:n.styles))return;const r=Array.isArray(n.styles)?n.styles:[n.styles];e.styles=Array.from(new Set([...e.styles,...r]))}};return n.addTheme("flasher",{render:e=>{const{type:t,title:s,message:n}=e,r="error"===t||"warning"===t,o=r?"alert":"status",i=r?"assertive":"polite",l=s||t.charAt(0).toUpperCase()+t.slice(1);return`\n
\n
\n
\n
\n ${l}\n ${n}\n
\n \n
\n \n \n \n
`}}),"undefined"!=typeof window&&(window.flasher=n),n})); diff --git a/tests/adapters/noty.test.ts b/tests/adapters/noty.test.ts new file mode 100644 index 00000000..8b48c749 --- /dev/null +++ b/tests/adapters/noty.test.ts @@ -0,0 +1,227 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Envelope } from '@flasher/flasher/types' + +// Use vi.hoisted to define mocks that will be available during vi.mock hoisting +const { mockShow, mockNotyInstance, MockNoty } = vi.hoisted(() => { + const mockShow = vi.fn() + const mockNotyInstance = { + show: mockShow, + layoutDom: { dataset: {} as DOMStringMap }, + } + const MockNoty = Object.assign( + vi.fn().mockImplementation(() => mockNotyInstance), + { overrideDefaults: vi.fn() }, + ) + return { mockShow, mockNotyInstance, MockNoty } +}) + +vi.mock('noty', () => ({ + default: MockNoty, +})) + +// Import after mocks +import NotyPlugin from '@flasher/flasher-noty/noty' + +const createEnvelope = (overrides: Partial = {}): Envelope => ({ + type: 'success', + message: 'Test message', + title: 'Test title', + options: {}, + metadata: { plugin: 'noty' }, + ...overrides, +}) + +describe('NotyPlugin', () => { + let plugin: NotyPlugin + + beforeEach(() => { + vi.clearAllMocks() + // Restore mock implementations after clearing + MockNoty.mockImplementation(() => mockNotyInstance) + mockNotyInstance.layoutDom = { dataset: {} as DOMStringMap } + plugin = new NotyPlugin() + }) + + describe('renderEnvelopes', () => { + it('should do nothing with empty envelopes', () => { + plugin.renderEnvelopes([]) + + expect(MockNoty).not.toHaveBeenCalled() + }) + + it('should do nothing with null/undefined envelopes', () => { + plugin.renderEnvelopes(null as unknown as Envelope[]) + plugin.renderEnvelopes(undefined as unknown as Envelope[]) + + expect(MockNoty).not.toHaveBeenCalled() + }) + + it('should create Noty instance with envelope data', () => { + plugin.renderEnvelopes([createEnvelope({ + type: 'success', + message: 'Hello World', + })]) + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + text: 'Hello World', + type: 'success', + })) + }) + + it('should call show() on Noty instance', () => { + plugin.renderEnvelopes([createEnvelope()]) + + expect(mockShow).toHaveBeenCalled() + }) + + it('should include default timeout option', () => { + plugin.renderEnvelopes([createEnvelope()]) + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + timeout: 10000, + })) + }) + + it('should merge envelope options', () => { + plugin.renderEnvelopes([createEnvelope({ + options: { timeout: 5000, layout: 'topRight' }, + })]) + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + timeout: 5000, + layout: 'topRight', + })) + }) + + it('should render multiple envelopes', () => { + plugin.renderEnvelopes([ + createEnvelope({ message: 'First' }), + createEnvelope({ message: 'Second' }), + ]) + + expect(MockNoty).toHaveBeenCalledTimes(2) + expect(mockShow).toHaveBeenCalledTimes(2) + }) + + it('should set Turbo compatibility on layoutDom', () => { + plugin.renderEnvelopes([createEnvelope()]) + + expect(mockNotyInstance.layoutDom.dataset.turboTemporary).toBe('') + }) + + it('should handle missing layoutDom gracefully', () => { + MockNoty.mockImplementationOnce(() => ({ + show: mockShow, + layoutDom: null, + })) + + // Should not throw + expect(() => plugin.renderEnvelopes([createEnvelope()])).not.toThrow() + }) + + it('should handle errors gracefully', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + MockNoty.mockImplementationOnce(() => { + throw new Error('Noty error') + }) + + plugin.renderEnvelopes([createEnvelope()]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error rendering notification'), + expect.any(Error), + expect.any(Object), + ) + }) + }) + + describe('renderOptions', () => { + it('should do nothing with null/undefined options', () => { + plugin.renderOptions(null as unknown as Record) + plugin.renderOptions(undefined as unknown as Record) + + expect(MockNoty.overrideDefaults).not.toHaveBeenCalled() + }) + + it('should call Noty.overrideDefaults with merged options', () => { + plugin.renderOptions({ timeout: 8000, layout: 'bottomLeft' }) + + expect(MockNoty.overrideDefaults).toHaveBeenCalledWith(expect.objectContaining({ + timeout: 8000, + layout: 'bottomLeft', + })) + }) + + it('should preserve existing default options', () => { + plugin.renderOptions({ layout: 'topLeft' }) + plugin.renderOptions({ theme: 'mint' }) + + // Get the last call arguments + const lastCall = MockNoty.overrideDefaults.mock.calls[MockNoty.overrideDefaults.mock.calls.length - 1][0] + + expect(lastCall).toMatchObject({ + timeout: 10000, // default + layout: 'topLeft', // from first call + theme: 'mint', // from second call + }) + }) + + it('should use options in subsequent renderEnvelopes', () => { + plugin.renderOptions({ animation: { open: 'fadeIn', close: 'fadeOut' } }) + plugin.renderEnvelopes([createEnvelope()]) + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + animation: { open: 'fadeIn', close: 'fadeOut' }, + })) + }) + }) + + describe('convenience methods (inherited from AbstractPlugin)', () => { + it('success() should create success notification', () => { + plugin.success('Success message') + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + type: 'success', + })) + }) + + it('error() should create error notification', () => { + plugin.error('Error message') + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + type: 'error', + })) + }) + + it('info() should create info notification', () => { + plugin.info('Info message') + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + type: 'info', + })) + }) + + it('warning() should create warning notification', () => { + plugin.warning('Warning message') + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + type: 'warning', + })) + }) + }) + + describe('notification types', () => { + it('should support all standard Noty types', () => { + const types = ['alert', 'success', 'error', 'warning', 'info'] + + types.forEach((type) => { + MockNoty.mockClear() + plugin.renderEnvelopes([createEnvelope({ type })]) + + expect(MockNoty).toHaveBeenCalledWith(expect.objectContaining({ + type, + })) + }) + }) + }) +}) diff --git a/tests/adapters/notyf.test.ts b/tests/adapters/notyf.test.ts new file mode 100644 index 00000000..f90f6275 --- /dev/null +++ b/tests/adapters/notyf.test.ts @@ -0,0 +1,218 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Envelope } from '@flasher/flasher/types' + +// Use vi.hoisted to define mocks that will be available during vi.mock hoisting +const { mockOpen, mockNotyfInstance, MockNotyf } = vi.hoisted(() => { + const mockOpen = vi.fn() + const mockNotyfInstance = { + open: mockOpen, + view: { + container: { dataset: {} as DOMStringMap }, + a11yContainer: { dataset: {} as DOMStringMap }, + }, + } + const MockNotyf = vi.fn().mockImplementation(() => mockNotyfInstance) + return { mockOpen, mockNotyfInstance, MockNotyf } +}) + +vi.mock('notyf', () => ({ + Notyf: MockNotyf, +})) + +vi.mock('notyf/notyf.min.css', () => ({})) + +// Import after mocks +import NotyfPlugin from '@flasher/flasher-notyf/notyf' + +const createEnvelope = (overrides: Partial = {}): Envelope => ({ + type: 'success', + message: 'Test message', + title: 'Test title', + options: {}, + metadata: { plugin: 'notyf' }, + ...overrides, +}) + +describe('NotyfPlugin', () => { + let plugin: NotyfPlugin + + beforeEach(() => { + vi.clearAllMocks() + // Restore mock implementations after clearing + MockNotyf.mockImplementation(() => mockNotyfInstance) + mockNotyfInstance.view.container.dataset = {} as DOMStringMap + mockNotyfInstance.view.a11yContainer.dataset = {} as DOMStringMap + plugin = new NotyfPlugin() + }) + + describe('renderEnvelopes', () => { + it('should initialize Notyf on first render', () => { + plugin.renderEnvelopes([createEnvelope()]) + + expect(MockNotyf).toHaveBeenCalled() + }) + + it('should call notyf.open with envelope data', () => { + plugin.renderEnvelopes([createEnvelope({ + type: 'success', + message: 'Hello', + title: 'Title', + })]) + + expect(mockOpen).toHaveBeenCalledWith(expect.objectContaining({ + type: 'success', + message: 'Hello', + title: 'Title', + })) + }) + + it('should merge envelope options', () => { + plugin.renderEnvelopes([createEnvelope({ + options: { duration: 5000, dismissible: true }, + })]) + + expect(mockOpen).toHaveBeenCalledWith(expect.objectContaining({ + duration: 5000, + dismissible: true, + })) + }) + + it('should render multiple envelopes', () => { + plugin.renderEnvelopes([ + createEnvelope({ message: 'First' }), + createEnvelope({ message: 'Second' }), + ]) + + expect(mockOpen).toHaveBeenCalledTimes(2) + }) + + it('should set Turbo compatibility on containers', () => { + plugin.renderEnvelopes([createEnvelope()]) + + expect(mockNotyfInstance.view.container.dataset.turboTemporary).toBe('') + expect(mockNotyfInstance.view.a11yContainer.dataset.turboTemporary).toBe('') + }) + + it('should handle errors gracefully', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + mockOpen.mockImplementationOnce(() => { + throw new Error('Notyf error') + }) + + plugin.renderEnvelopes([createEnvelope()]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error rendering notification'), + expect.any(Error), + expect.any(Object), + ) + }) + }) + + describe('renderOptions', () => { + it('should do nothing with null/undefined options', () => { + plugin.renderOptions(null as unknown as Record) + plugin.renderOptions(undefined as unknown as Record) + // Should not throw + }) + + it('should initialize Notyf with options', () => { + plugin.renderOptions({ duration: 8000, position: { x: 'left', y: 'bottom' } }) + + expect(MockNotyf).toHaveBeenCalledWith(expect.objectContaining({ + duration: 8000, + position: { x: 'left', y: 'bottom' }, + })) + }) + + it('should use default duration if not provided', () => { + plugin.renderOptions({ position: { x: 'center', y: 'top' } }) + + expect(MockNotyf).toHaveBeenCalledWith(expect.objectContaining({ + duration: 10000, + })) + }) + + it('should add info type configuration', () => { + plugin.renderOptions({}) + + const callArgs = MockNotyf.mock.calls[0][0] + const infoType = callArgs.types.find((t: any) => t.type === 'info') + + expect(infoType).toBeDefined() + expect(infoType.className).toBe('notyf__toast--info') + expect(infoType.background).toBe('#5784E5') + }) + + it('should add warning type configuration', () => { + plugin.renderOptions({}) + + const callArgs = MockNotyf.mock.calls[0][0] + const warningType = callArgs.types.find((t: any) => t.type === 'warning') + + expect(warningType).toBeDefined() + expect(warningType.className).toBe('notyf__toast--warning') + expect(warningType.background).toBe('#E3A008') + }) + + it('should not duplicate types if already provided', () => { + plugin.renderOptions({ + types: [ + { type: 'info', background: '#custom' }, + ], + }) + + const callArgs = MockNotyf.mock.calls[0][0] + const infoTypes = callArgs.types.filter((t: any) => t.type === 'info') + + expect(infoTypes).toHaveLength(1) + expect(infoTypes[0].background).toBe('#custom') + }) + }) + + describe('default initialization', () => { + it('should initialize with default options when rendering without prior options', () => { + plugin.renderEnvelopes([createEnvelope()]) + + expect(MockNotyf).toHaveBeenCalledWith(expect.objectContaining({ + duration: 10000, + position: { x: 'right', y: 'top' }, + dismissible: true, + })) + }) + }) + + describe('convenience methods (inherited from AbstractPlugin)', () => { + it('success() should create success notification', () => { + plugin.success('Success message') + + expect(mockOpen).toHaveBeenCalledWith(expect.objectContaining({ + type: 'success', + })) + }) + + it('error() should create error notification', () => { + plugin.error('Error message') + + expect(mockOpen).toHaveBeenCalledWith(expect.objectContaining({ + type: 'error', + })) + }) + + it('info() should create info notification', () => { + plugin.info('Info message') + + expect(mockOpen).toHaveBeenCalledWith(expect.objectContaining({ + type: 'info', + })) + }) + + it('warning() should create warning notification', () => { + plugin.warning('Warning message') + + expect(mockOpen).toHaveBeenCalledWith(expect.objectContaining({ + type: 'warning', + })) + }) + }) +}) diff --git a/tests/adapters/sweetalert.test.ts b/tests/adapters/sweetalert.test.ts new file mode 100644 index 00000000..30eb8761 --- /dev/null +++ b/tests/adapters/sweetalert.test.ts @@ -0,0 +1,271 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Envelope } from '@flasher/flasher/types' + +// Use vi.hoisted to define mocks that will be available during vi.mock hoisting +const { mockFire, mockMixin, mockIsVisible, mockGetPopup, mockClose } = vi.hoisted(() => { + const mockFire = vi.fn().mockResolvedValue({ isConfirmed: true }) + const mockMixin = vi.fn().mockReturnValue({ fire: mockFire }) + const mockIsVisible = vi.fn().mockReturnValue(false) + const mockGetPopup = vi.fn().mockReturnValue({ style: { setProperty: vi.fn() } }) + const mockClose = vi.fn() + return { mockFire, mockMixin, mockIsVisible, mockGetPopup, mockClose } +}) + +vi.mock('sweetalert2', () => ({ + default: { + mixin: mockMixin, + isVisible: mockIsVisible, + getPopup: mockGetPopup, + close: mockClose, + }, +})) + +// Import after mocks +import SweetAlertPlugin from '@flasher/flasher-sweetalert/sweetalert' + +const createEnvelope = (overrides: Partial = {}): Envelope => ({ + type: 'success', + message: 'Test message', + title: 'Test title', + options: {}, + metadata: { plugin: 'sweetalert' }, + ...overrides, +}) + +describe('SweetAlertPlugin', () => { + let plugin: SweetAlertPlugin + + beforeEach(() => { + vi.clearAllMocks() + // Restore mock implementations after clearing + mockFire.mockResolvedValue({ isConfirmed: true }) + mockMixin.mockReturnValue({ fire: mockFire }) + mockIsVisible.mockReturnValue(false) + mockGetPopup.mockReturnValue({ style: { setProperty: vi.fn() } }) + plugin = new SweetAlertPlugin() + }) + + describe('renderEnvelopes', () => { + it('should initialize SweetAlert on first render', async () => { + await plugin.renderEnvelopes([createEnvelope()]) + + expect(mockMixin).toHaveBeenCalled() + }) + + it('should call fire with envelope options', async () => { + await plugin.renderEnvelopes([createEnvelope({ + type: 'success', + message: 'Hello World', + })]) + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + icon: 'success', + text: 'Hello World', + })) + }) + + it('should use envelope.type as icon by default', async () => { + await plugin.renderEnvelopes([createEnvelope({ type: 'error' })]) + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + icon: 'error', + })) + }) + + it('should use envelope.message as text by default', async () => { + await plugin.renderEnvelopes([createEnvelope({ message: 'Custom message' })]) + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + text: 'Custom message', + })) + }) + + it('should allow overriding icon via options', async () => { + await plugin.renderEnvelopes([createEnvelope({ + type: 'success', + options: { icon: 'warning' }, + })]) + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + icon: 'warning', + })) + }) + + it('should allow overriding text via options', async () => { + await plugin.renderEnvelopes([createEnvelope({ + message: 'Original', + options: { text: 'Override' }, + })]) + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + text: 'Override', + })) + }) + + it('should render envelopes sequentially', async () => { + const callOrder: string[] = [] + + mockFire + .mockImplementationOnce(async () => { + callOrder.push('first') + return { isConfirmed: true } + }) + .mockImplementationOnce(async () => { + callOrder.push('second') + return { isConfirmed: true } + }) + + await plugin.renderEnvelopes([ + createEnvelope({ message: 'First' }), + createEnvelope({ message: 'Second' }), + ]) + + expect(callOrder).toEqual(['first', 'second']) + }) + + it('should dispatch promise event after fire', async () => { + const eventHandler = vi.fn() + window.addEventListener('flasher:sweetalert:promise', eventHandler) + + await plugin.renderEnvelopes([createEnvelope()]) + + expect(eventHandler).toHaveBeenCalledWith(expect.objectContaining({ + detail: expect.objectContaining({ + promise: { isConfirmed: true }, + envelope: expect.any(Object), + }), + })) + + window.removeEventListener('flasher:sweetalert:promise', eventHandler) + }) + + it('should handle errors gracefully', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + mockFire.mockRejectedValueOnce(new Error('Swal error')) + + await plugin.renderEnvelopes([createEnvelope()]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error rendering envelope'), + expect.any(Error), + expect.any(Object), + ) + }) + }) + + describe('renderOptions', () => { + it('should create mixin with options', () => { + plugin.renderOptions({ timer: 5000, showConfirmButton: false }) + + expect(mockMixin).toHaveBeenCalledWith(expect.objectContaining({ + timer: 5000, + showConfirmButton: false, + })) + }) + + it('should use default timer if not provided', () => { + plugin.renderOptions({}) + + expect(mockMixin).toHaveBeenCalledWith(expect.objectContaining({ + timer: 10000, + })) + }) + + it('should use default timerProgressBar if not provided', () => { + plugin.renderOptions({}) + + expect(mockMixin).toHaveBeenCalledWith(expect.objectContaining({ + timerProgressBar: true, + })) + }) + + it('should setup Turbo compatibility', () => { + const addEventListenerSpy = vi.spyOn(document, 'addEventListener') + + plugin.renderOptions({}) + + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'turbo:before-cache', + expect.any(Function), + ) + }) + + it('should handle errors gracefully', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + mockMixin.mockImplementationOnce(() => { + throw new Error('Mixin error') + }) + + plugin.renderOptions({ timer: 5000 }) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error applying options'), + expect.any(Error), + ) + }) + }) + + describe('Turbo compatibility', () => { + it('should close visible Swal on turbo:before-cache', () => { + mockIsVisible.mockReturnValueOnce(true) + + plugin.renderOptions({}) + + // Simulate turbo:before-cache event + document.dispatchEvent(new Event('turbo:before-cache')) + + expect(mockClose).toHaveBeenCalled() + }) + + it('should set animation duration to 0ms before closing', () => { + mockIsVisible.mockReturnValueOnce(true) + const mockSetProperty = vi.fn() + mockGetPopup.mockReturnValueOnce({ style: { setProperty: mockSetProperty } }) + + plugin.renderOptions({}) + + document.dispatchEvent(new Event('turbo:before-cache')) + + expect(mockSetProperty).toHaveBeenCalledWith('animation-duration', '0ms') + }) + + it('should not close if Swal is not visible', () => { + mockIsVisible.mockReturnValueOnce(false) + + plugin.renderOptions({}) + + document.dispatchEvent(new Event('turbo:before-cache')) + + expect(mockClose).not.toHaveBeenCalled() + }) + }) + + describe('default initialization', () => { + it('should initialize with default options when rendering without prior options', async () => { + await plugin.renderEnvelopes([createEnvelope()]) + + expect(mockMixin).toHaveBeenCalledWith(expect.objectContaining({ + timer: 10000, + timerProgressBar: true, + })) + }) + }) + + describe('convenience methods (inherited from AbstractPlugin)', () => { + it('success() should create success notification', async () => { + await plugin.success('Success message') + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + icon: 'success', + })) + }) + + it('error() should create error notification', async () => { + await plugin.error('Error message') + + expect(mockFire).toHaveBeenCalledWith(expect.objectContaining({ + icon: 'error', + })) + }) + }) +}) diff --git a/tests/adapters/toastr.test.ts b/tests/adapters/toastr.test.ts new file mode 100644 index 00000000..7bf34828 --- /dev/null +++ b/tests/adapters/toastr.test.ts @@ -0,0 +1,221 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Envelope } from '@flasher/flasher/types' + +// Use vi.hoisted to define mocks that will be available during vi.mock hoisting +const { mockToastr, mockJQuery } = vi.hoisted(() => { + const mockToastr = { + success: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }), + error: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }), + info: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }), + warning: vi.fn().mockReturnValue({ parent: vi.fn().mockReturnValue({ attr: vi.fn() }) }), + options: {} as Record, + } + const mockJQuery = vi.fn() + return { mockToastr, mockJQuery } +}) + +vi.mock('toastr', () => ({ + default: mockToastr, +})) + +// Import after mocks are set up +import ToastrPlugin from '@flasher/flasher-toastr/toastr' + +const createEnvelope = (overrides: Partial = {}): Envelope => ({ + type: 'success', + message: 'Test message', + title: 'Test title', + options: {}, + metadata: { plugin: 'toastr' }, + ...overrides, +}) + +describe('ToastrPlugin', () => { + let plugin: ToastrPlugin + + beforeEach(() => { + plugin = new ToastrPlugin() + vi.clearAllMocks() + mockToastr.options = {} + + // Set up jQuery mock + ;(window as any).jQuery = mockJQuery + ;(window as any).$ = mockJQuery + }) + + describe('renderEnvelopes', () => { + it('should do nothing with empty envelopes', () => { + plugin.renderEnvelopes([]) + + expect(mockToastr.success).not.toHaveBeenCalled() + }) + + it('should do nothing with null/undefined envelopes', () => { + plugin.renderEnvelopes(null as unknown as Envelope[]) + plugin.renderEnvelopes(undefined as unknown as Envelope[]) + + expect(mockToastr.success).not.toHaveBeenCalled() + }) + + it('should call toastr with correct type', () => { + plugin.renderEnvelopes([createEnvelope({ type: 'success' })]) + expect(mockToastr.success).toHaveBeenCalled() + + plugin.renderEnvelopes([createEnvelope({ type: 'error' })]) + expect(mockToastr.error).toHaveBeenCalled() + + plugin.renderEnvelopes([createEnvelope({ type: 'info' })]) + expect(mockToastr.info).toHaveBeenCalled() + + plugin.renderEnvelopes([createEnvelope({ type: 'warning' })]) + expect(mockToastr.warning).toHaveBeenCalled() + }) + + it('should pass message, title, and options to toastr', () => { + plugin.renderEnvelopes([createEnvelope({ + message: 'Hello World', + title: 'Greeting', + options: { timeOut: 5000 }, + })]) + + expect(mockToastr.success).toHaveBeenCalledWith( + 'Hello World', + 'Greeting', + { timeOut: 5000 }, + ) + }) + + it('should render multiple envelopes', () => { + plugin.renderEnvelopes([ + createEnvelope({ type: 'success', message: 'First' }), + createEnvelope({ type: 'error', message: 'Second' }), + ]) + + expect(mockToastr.success).toHaveBeenCalledTimes(1) + expect(mockToastr.error).toHaveBeenCalledTimes(1) + }) + + it('should set Turbo compatibility attribute', () => { + const mockParent = { attr: vi.fn() } + mockToastr.success.mockReturnValue({ parent: () => mockParent }) + + plugin.renderEnvelopes([createEnvelope()]) + + expect(mockParent.attr).toHaveBeenCalledWith('data-turbo-temporary', '') + }) + + it('should log error when jQuery is not available', () => { + delete (window as any).jQuery + delete (window as any).$ + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + plugin.renderEnvelopes([createEnvelope()]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('jQuery is required'), + ) + expect(mockToastr.success).not.toHaveBeenCalled() + + // Restore jQuery for other tests + ;(window as any).jQuery = mockJQuery + ;(window as any).$ = mockJQuery + }) + + it('should handle toastr errors gracefully', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + mockToastr.success.mockImplementationOnce(() => { + throw new Error('Toastr error') + }) + + plugin.renderEnvelopes([createEnvelope()]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error rendering notification'), + expect.any(Error), + expect.any(Object), + ) + }) + + it('should handle missing parent method gracefully', () => { + mockToastr.success.mockReturnValueOnce({}) + + // Should not throw + expect(() => plugin.renderEnvelopes([createEnvelope()])).not.toThrow() + }) + + it('should handle parent() returning null gracefully', () => { + mockToastr.success.mockReturnValueOnce({ parent: () => null }) + + // Should not throw + expect(() => plugin.renderEnvelopes([createEnvelope()])).not.toThrow() + }) + }) + + describe('renderOptions', () => { + it('should set toastr options with defaults', () => { + plugin.renderOptions({ closeButton: true }) + + expect(mockToastr.options).toMatchObject({ + timeOut: 10000, + progressBar: true, + closeButton: true, + }) + }) + + it('should override default timeOut', () => { + plugin.renderOptions({ timeOut: 5000 }) + + expect(mockToastr.options.timeOut).toBe(5000) + }) + + it('should override default progressBar', () => { + plugin.renderOptions({ progressBar: false }) + + expect(mockToastr.options.progressBar).toBe(false) + }) + + it('should log error when jQuery is not available', () => { + delete (window as any).jQuery + delete (window as any).$ + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + plugin.renderOptions({ timeOut: 5000 }) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('jQuery is required'), + ) + + // Restore jQuery + ;(window as any).jQuery = mockJQuery + ;(window as any).$ = mockJQuery + }) + }) + + describe('convenience methods (inherited from AbstractPlugin)', () => { + it('success() should create success notification', () => { + plugin.success('Success message') + + expect(mockToastr.success).toHaveBeenCalled() + }) + + it('error() should create error notification', () => { + plugin.error('Error message') + + expect(mockToastr.error).toHaveBeenCalled() + }) + + it('info() should create info notification', () => { + plugin.info('Info message') + + expect(mockToastr.info).toHaveBeenCalled() + }) + + it('warning() should create warning notification', () => { + plugin.warning('Warning message') + + expect(mockToastr.warning).toHaveBeenCalled() + }) + }) +}) diff --git a/tests/flasher-plugin.test.ts b/tests/flasher-plugin.test.ts new file mode 100644 index 00000000..245c4503 --- /dev/null +++ b/tests/flasher-plugin.test.ts @@ -0,0 +1,491 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import FlasherPlugin from '@flasher/flasher/flasher-plugin' +import type { Envelope, Theme } from '@flasher/flasher/types' + +// Mock SCSS import +vi.mock('@flasher/flasher/themes/index.scss', () => ({})) + +const createMockTheme = (customRender?: (envelope: Envelope) => string): Theme => ({ + render: customRender || ((envelope: Envelope) => ` +
+
+ ${envelope.title} + ${envelope.message} + +
+ +
+ `), +}) + +const createEnvelope = (overrides: Partial = {}): Envelope => ({ + type: 'success', + message: 'Test message', + title: 'Test title', + options: {}, + metadata: { plugin: 'theme.test' }, + ...overrides, +}) + +describe('FlasherPlugin', () => { + let plugin: FlasherPlugin + + beforeEach(() => { + plugin = new FlasherPlugin(createMockTheme()) + vi.useFakeTimers() + + // Mock requestAnimationFrame to execute callback immediately + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + cb(performance.now()) + return 0 + }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describe('constructor', () => { + it('should throw error when theme is missing', () => { + expect(() => new FlasherPlugin(null as unknown as Theme)).toThrow('Theme is required') + }) + + it('should throw error when theme has no render function', () => { + expect(() => new FlasherPlugin({} as Theme)).toThrow('Theme must have a render function') + }) + + it('should throw error when theme render is not a function', () => { + expect(() => new FlasherPlugin({ render: 'not a function' } as unknown as Theme)).toThrow('Theme must have a render function') + }) + + it('should create plugin with valid theme', () => { + const p = new FlasherPlugin(createMockTheme()) + expect(p).toBeInstanceOf(FlasherPlugin) + }) + }) + + describe('renderOptions', () => { + it('should merge options with defaults', () => { + plugin.renderOptions({ timeout: 5000, position: 'bottom-left' }) + plugin.renderEnvelopes([createEnvelope()]) + + const container = document.querySelector('.fl-wrapper') + expect(container?.getAttribute('data-position')).toBe('bottom-left') + }) + + it('should do nothing with null/undefined options', () => { + plugin.renderOptions(null as unknown as Record) + plugin.renderOptions(undefined as unknown as Record) + // Should not throw + }) + }) + + describe('renderEnvelopes', () => { + it('should do nothing with empty envelopes', () => { + plugin.renderEnvelopes([]) + expect(document.querySelector('.fl-wrapper')).toBeNull() + }) + + it('should do nothing with null/undefined envelopes', () => { + plugin.renderEnvelopes(null as unknown as Envelope[]) + plugin.renderEnvelopes(undefined as unknown as Envelope[]) + // Should not throw + }) + + it('should create container with default position', () => { + plugin.renderEnvelopes([createEnvelope()]) + + const container = document.querySelector('.fl-wrapper') + expect(container).toBeTruthy() + expect(container?.getAttribute('data-position')).toBe('top-right') + }) + + it('should create notification inside container', () => { + plugin.renderEnvelopes([createEnvelope()]) + + const notification = document.querySelector('.fl-container') + expect(notification).toBeTruthy() + }) + + it('should add fl-show class after animation frame', () => { + plugin.renderEnvelopes([createEnvelope()]) + + const notification = document.querySelector('.fl-container') + // With mocked requestAnimationFrame, fl-show is added immediately + expect(notification?.classList.contains('fl-show')).toBe(true) + }) + + it('should reuse existing container for same position', () => { + plugin.renderEnvelopes([createEnvelope()]) + plugin.renderEnvelopes([createEnvelope()]) + + const containers = document.querySelectorAll('.fl-wrapper') + expect(containers).toHaveLength(1) + + const notifications = document.querySelectorAll('.fl-container') + expect(notifications).toHaveLength(2) + }) + + it('should create separate containers for different positions', () => { + plugin.renderEnvelopes([createEnvelope({ options: { position: 'top-left' } })]) + plugin.renderEnvelopes([createEnvelope({ options: { position: 'bottom-right' } })]) + + const containers = document.querySelectorAll('.fl-wrapper') + expect(containers).toHaveLength(2) + }) + + it('should prepend notifications when direction is "top"', () => { + plugin.renderOptions({ direction: 'top' }) + + plugin.renderEnvelopes([createEnvelope({ message: 'First' })]) + plugin.renderEnvelopes([createEnvelope({ message: 'Second' })]) + + const container = document.querySelector('.fl-wrapper') + const first = container?.firstElementChild + expect(first?.querySelector('.fl-message')?.textContent).toBe('Second') + }) + + it('should append notifications when direction is "bottom"', () => { + plugin.renderOptions({ direction: 'bottom' }) + + plugin.renderEnvelopes([createEnvelope({ message: 'First' })]) + plugin.renderEnvelopes([createEnvelope({ message: 'Second' })]) + + const container = document.querySelector('.fl-wrapper') + const last = container?.lastElementChild + expect(last?.querySelector('.fl-message')?.textContent).toBe('Second') + }) + + it('should add fl-rtl class when rtl option is true', () => { + plugin.renderOptions({ rtl: true }) + plugin.renderEnvelopes([createEnvelope()]) + + const notification = document.querySelector('.fl-container') + expect(notification?.classList.contains('fl-rtl')).toBe(true) + }) + + it('should set Turbo temporary attribute on container', () => { + plugin.renderEnvelopes([createEnvelope()]) + + const container = document.querySelector('.fl-wrapper') + expect(container?.hasAttribute('data-turbo-temporary')).toBe(true) + }) + }) + + describe('timeout and timer', () => { + it('should auto-remove notification after timeout', () => { + plugin.renderOptions({ timeout: 5000 }) + plugin.renderEnvelopes([createEnvelope()]) + + expect(document.querySelector('.fl-container')).toBeTruthy() + + vi.advanceTimersByTime(5000) + + const notification = document.querySelector('.fl-container') + expect(notification?.classList.contains('fl-show')).toBe(false) + }) + + it('should use type-specific timeout when global timeout is null', () => { + plugin.renderOptions({ + timeout: null, + timeouts: { success: 3000 }, + }) + plugin.renderEnvelopes([createEnvelope({ type: 'success' })]) + + // fl-show is added immediately with mocked requestAnimationFrame + const notification = document.querySelector('.fl-container') as HTMLElement + expect(notification).toBeTruthy() + expect(notification.classList.contains('fl-show')).toBe(true) + + // Advance most of the way (with buffer for timer precision) + vi.advanceTimersByTime(2800) + expect(notification.classList.contains('fl-show')).toBe(true) + + // Advance past the timeout + vi.advanceTimersByTime(300) + expect(notification.classList.contains('fl-show')).toBe(false) + }) + + it('should respect envelope-specific timeout over global', () => { + plugin.renderOptions({ timeout: 10000 }) + plugin.renderEnvelopes([createEnvelope({ options: { timeout: 2000 } })]) + + // fl-show is added immediately with mocked requestAnimationFrame + const notification = document.querySelector('.fl-container') as HTMLElement + expect(notification).toBeTruthy() + expect(notification.classList.contains('fl-show')).toBe(true) + + // Should still be showing before timeout + vi.advanceTimersByTime(1800) + expect(notification.classList.contains('fl-show')).toBe(true) + + // Should be removed after timeout + vi.advanceTimersByTime(300) + expect(notification.classList.contains('fl-show')).toBe(false) + }) + + it('should create sticky notification when timeout is false', () => { + plugin.renderEnvelopes([createEnvelope({ options: { timeout: false } })]) + + const notification = document.querySelector('.fl-container') + expect(notification?.classList.contains('fl-sticky')).toBe(true) + + // Should not auto-remove + vi.advanceTimersByTime(60000) + expect(document.querySelector('.fl-container')).toBeTruthy() + }) + + it('should create sticky notification when timeout is 0', () => { + plugin.renderEnvelopes([createEnvelope({ options: { timeout: 0 } })]) + + const notification = document.querySelector('.fl-container') + expect(notification?.classList.contains('fl-sticky')).toBe(true) + }) + + it('should create sticky notification when timeout is negative', () => { + plugin.renderEnvelopes([createEnvelope({ options: { timeout: -1 } })]) + + const notification = document.querySelector('.fl-container') + expect(notification?.classList.contains('fl-sticky')).toBe(true) + }) + + it('should update progress bar during countdown', () => { + plugin.renderOptions({ timeout: 1000, fps: 10 }) + plugin.renderEnvelopes([createEnvelope()]) + + vi.advanceTimersByTime(100) // First tick + + const progressBar = document.querySelector('.fl-progress') as HTMLElement + expect(progressBar).toBeTruthy() + + // After 500ms, should be around 50% + vi.advanceTimersByTime(400) + const width = Number.parseFloat(progressBar.style.width) + expect(width).toBeLessThan(60) + expect(width).toBeGreaterThan(40) + }) + + it('should create 100% progress bar for sticky notifications', () => { + plugin.renderEnvelopes([createEnvelope({ options: { timeout: false } })]) + + vi.runAllTimers() + + const progressBar = document.querySelector('.fl-progress.fl-sticky-progress') as HTMLElement + expect(progressBar?.style.width).toBe('100%') + }) + }) + + describe('close button', () => { + it('should remove notification when close button is clicked', () => { + plugin.renderEnvelopes([createEnvelope({ options: { timeout: false } })]) + + const closeButton = document.querySelector('.fl-close') as HTMLElement + closeButton.click() + + const notification = document.querySelector('.fl-container') + expect(notification?.classList.contains('fl-show')).toBe(false) + }) + + it('should stop event propagation on close click', () => { + plugin.renderEnvelopes([createEnvelope({ options: { timeout: false } })]) + + const notification = document.querySelector('.fl-container') as HTMLElement + const notificationClickHandler = vi.fn() + notification.addEventListener('click', notificationClickHandler) + + const closeButton = document.querySelector('.fl-close') as HTMLElement + closeButton.click() + + expect(notificationClickHandler).not.toHaveBeenCalled() + }) + }) + + describe('hover pause', () => { + it('should pause timer on mouse over', () => { + plugin.renderOptions({ timeout: 5000 }) + plugin.renderEnvelopes([createEnvelope()]) + + vi.advanceTimersByTime(2000) // Advance halfway + + const notification = document.querySelector('.fl-container') as HTMLElement + notification.dispatchEvent(new MouseEvent('mouseover')) + + vi.advanceTimersByTime(10000) // Advance a lot more + + // Should still be showing because timer was paused + expect(notification.classList.contains('fl-show')).toBe(true) + }) + + it('should resume timer on mouse out', () => { + plugin.renderOptions({ timeout: 5000 }) + plugin.renderEnvelopes([createEnvelope()]) + + // Run requestAnimationFrame to add fl-show + vi.advanceTimersByTime(0) + + const notification = document.querySelector('.fl-container') as HTMLElement + + vi.advanceTimersByTime(2000) // 2s elapsed + notification.dispatchEvent(new MouseEvent('mouseover')) + vi.advanceTimersByTime(5000) // Paused, no change + notification.dispatchEvent(new MouseEvent('mouseout')) + vi.advanceTimersByTime(3500) // Remaining time + buffer + + expect(notification.classList.contains('fl-show')).toBe(false) + }) + }) + + describe('HTML escaping', () => { + it('should escape HTML when escapeHtml option is true', () => { + plugin.renderOptions({ escapeHtml: true }) + plugin.renderEnvelopes([createEnvelope({ + message: '', + title: 'Bold', + })]) + + const message = document.querySelector('.fl-message') + const title = document.querySelector('.fl-title') + + expect(message?.innerHTML).toContain('<script>') + expect(title?.innerHTML).toContain('<b>') + }) + + it('should not escape HTML when escapeHtml is false (default)', () => { + plugin.renderEnvelopes([createEnvelope({ + message: 'Bold message', + })]) + + const message = document.querySelector('.fl-message') + expect(message?.innerHTML).toContain('Bold message') + }) + + it('should escape special characters correctly', () => { + plugin.renderOptions({ escapeHtml: true }) + plugin.renderEnvelopes([createEnvelope({ + message: '& < > " \' ` = /', + })]) + + const message = document.querySelector('.fl-message') + // When innerHTML is serialized, browser only escapes & < > minimally + // The key test is that < and > are escaped (prevents XSS) + expect(message?.innerHTML).toContain('&') + expect(message?.innerHTML).toContain('<') + expect(message?.innerHTML).toContain('>') + // Text content should have all original characters + expect(message?.textContent).toBe('& < > " \' ` = /') + }) + + it('should handle null/undefined message gracefully when escaping', () => { + plugin.renderOptions({ escapeHtml: true }) + + // This tests the escapeHtml method's null handling + plugin.renderEnvelopes([createEnvelope({ + message: null as unknown as string, + })]) + + // Should not throw + }) + + it('should respect per-envelope escapeHtml option', () => { + plugin.renderOptions({ escapeHtml: false }) // Global false + + plugin.renderEnvelopes([createEnvelope({ + message: 'Bold', + options: { escapeHtml: true }, // Per-envelope true + })]) + + const message = document.querySelector('.fl-message') + expect(message?.innerHTML).toContain('<b>') + }) + }) + + describe('custom styles', () => { + it('should apply custom style properties to container', () => { + plugin.renderOptions({ + style: { + zIndex: '9999', + marginTop: '20px', + }, + }) + plugin.renderEnvelopes([createEnvelope()]) + + const container = document.querySelector('.fl-wrapper') as HTMLElement + expect(container.style.getPropertyValue('z-index')).toBe('9999') + expect(container.style.getPropertyValue('margin-top')).toBe('20px') + }) + }) + + describe('container cleanup', () => { + it('should remove empty container after last notification is removed', () => { + plugin.renderOptions({ timeout: 1000 }) + plugin.renderEnvelopes([createEnvelope()]) + + vi.advanceTimersByTime(1000) + + const notification = document.querySelector('.fl-container') as HTMLElement + // Trigger transition end + notification?.ontransitionend?.({} as TransitionEvent) + + expect(document.querySelector('.fl-wrapper')).toBeNull() + }) + + it('should keep container when other notifications remain', () => { + plugin.renderOptions({ timeout: false }) + plugin.renderEnvelopes([createEnvelope({ message: 'First' })]) + plugin.renderEnvelopes([createEnvelope({ message: 'Second' })]) + + // Remove first notification + const closeButtons = document.querySelectorAll('.fl-close') + ;(closeButtons[0] as HTMLElement).click() + + const notification = document.querySelectorAll('.fl-container')[0] as HTMLElement + notification?.ontransitionend?.({} as TransitionEvent) + + expect(document.querySelector('.fl-wrapper')).toBeTruthy() + expect(document.querySelectorAll('.fl-container')).toHaveLength(1) + }) + }) + + describe('DOM ready handling', () => { + it('should defer rendering if DOM is loading', () => { + // Mock document.readyState + Object.defineProperty(document, 'readyState', { + value: 'loading', + writable: true, + }) + + const addEventListenerSpy = vi.spyOn(document, 'addEventListener') + + plugin.renderEnvelopes([createEnvelope()]) + + expect(addEventListenerSpy).toHaveBeenCalledWith('DOMContentLoaded', expect.any(Function)) + + // Reset + Object.defineProperty(document, 'readyState', { + value: 'complete', + writable: true, + }) + }) + }) + + describe('error handling', () => { + it('should log error and continue when envelope rendering fails', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const badTheme: Theme = { + render: () => { + throw new Error('Render error') + }, + } + + const badPlugin = new FlasherPlugin(badTheme) + badPlugin.renderEnvelopes([createEnvelope()]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error rendering envelope'), + expect.any(Error), + expect.any(Object), + ) + }) + }) +}) diff --git a/tests/flasher.test.ts b/tests/flasher.test.ts new file mode 100644 index 00000000..f10f7811 --- /dev/null +++ b/tests/flasher.test.ts @@ -0,0 +1,553 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import Flasher from '@flasher/flasher/flasher' +import type { Envelope, PluginInterface, Theme } from '@flasher/flasher/types' + +// Mock plugin for testing +class MockPlugin implements PluginInterface { + public envelopes: Envelope[] = [] + public options: Record = {} + + success = vi.fn() + error = vi.fn() + info = vi.fn() + warning = vi.fn() + flash = vi.fn() + + renderEnvelopes(envelopes: Envelope[]): void { + this.envelopes.push(...envelopes) + } + + renderOptions(options: Record): void { + this.options = { ...this.options, ...options } + } +} + +// Mock theme for testing +const mockTheme: Theme = { + styles: '/path/to/theme.css', + render: (envelope: Envelope) => `
${envelope.message}
`, +} + +describe('Flasher', () => { + let flasher: Flasher + + beforeEach(() => { + flasher = new Flasher() + }) + + describe('plugin registration', () => { + it('should register a plugin with addPlugin', () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + expect(flasher.use('test')).toBe(plugin) + }) + + it('should throw error when plugin name is missing', () => { + const plugin = new MockPlugin() + + expect(() => flasher.addPlugin('', plugin)).toThrow('Both plugin name and instance are required') + }) + + it('should throw error when plugin instance is missing', () => { + expect(() => flasher.addPlugin('test', null as unknown as PluginInterface)).toThrow('Both plugin name and instance are required') + }) + + it('should throw error when using unregistered plugin', () => { + expect(() => flasher.use('nonexistent')).toThrow('Unable to resolve "nonexistent" plugin') + }) + + it('create() should be an alias for use()', () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + expect(flasher.create('test')).toBe(flasher.use('test')) + }) + }) + + describe('theme registration', () => { + it('should register a theme with addTheme', () => { + flasher.addTheme('custom', mockTheme) + + // Theme creates a FlasherPlugin when accessed + const plugin = flasher.use('theme.custom') + expect(plugin).toBeDefined() + }) + + it('should throw error when theme name is missing', () => { + expect(() => flasher.addTheme('', mockTheme)).toThrow('Both theme name and definition are required') + }) + + it('should throw error when theme definition is missing', () => { + expect(() => flasher.addTheme('test', null as unknown as Theme)).toThrow('Both theme name and definition are required') + }) + }) + + describe('plugin alias resolution', () => { + it('should resolve "flasher" to "theme.flasher"', () => { + flasher.addTheme('flasher', mockTheme) + + const plugin = flasher.use('flasher') + expect(plugin).toBeDefined() + }) + + it('should keep other plugin names unchanged', () => { + const plugin = new MockPlugin() + flasher.addPlugin('toastr', plugin) + + expect(flasher.use('toastr')).toBe(plugin) + }) + }) + + describe('renderEnvelopes', () => { + it('should do nothing with empty envelopes', () => { + flasher.renderEnvelopes([]) + // Should not throw + }) + + it('should do nothing with null/undefined envelopes', () => { + flasher.renderEnvelopes(null as unknown as Envelope[]) + flasher.renderEnvelopes(undefined as unknown as Envelope[]) + // Should not throw + }) + + it('should group envelopes by plugin and render', () => { + const plugin1 = new MockPlugin() + const plugin2 = new MockPlugin() + + flasher.addPlugin('plugin1', plugin1) + flasher.addPlugin('plugin2', plugin2) + + const envelopes: Envelope[] = [ + { type: 'success', message: 'Message 1', title: 'Title 1', options: {}, metadata: { plugin: 'plugin1' } }, + { type: 'error', message: 'Message 2', title: 'Title 2', options: {}, metadata: { plugin: 'plugin2' } }, + { type: 'info', message: 'Message 3', title: 'Title 3', options: {}, metadata: { plugin: 'plugin1' } }, + ] + + flasher.renderEnvelopes(envelopes) + + expect(plugin1.envelopes).toHaveLength(2) + expect(plugin2.envelopes).toHaveLength(1) + }) + + it('should log error and continue when plugin throws', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const badPlugin: PluginInterface = { + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warning: vi.fn(), + flash: vi.fn(), + renderEnvelopes: () => { + throw new Error('Plugin error') + }, + renderOptions: vi.fn(), + } + + flasher.addPlugin('bad', badPlugin) + + flasher.renderEnvelopes([ + { type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'bad' } }, + ]) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error rendering envelopes'), + expect.any(Error), + ) + }) + }) + + describe('renderOptions', () => { + it('should do nothing with null/undefined options', () => { + flasher.renderOptions(null as unknown as Record) + flasher.renderOptions(undefined as unknown as Record) + // Should not throw + }) + + it('should apply options to each plugin', () => { + const plugin1 = new MockPlugin() + const plugin2 = new MockPlugin() + + flasher.addPlugin('plugin1', plugin1) + flasher.addPlugin('plugin2', plugin2) + + flasher.renderOptions({ + plugin1: { timeout: 5000 }, + plugin2: { position: 'top-left' }, + }) + + expect(plugin1.options).toEqual({ timeout: 5000 }) + expect(plugin2.options).toEqual({ position: 'top-left' }) + }) + + it('should log error and continue when plugin throws', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const badPlugin: PluginInterface = { + success: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warning: vi.fn(), + flash: vi.fn(), + renderEnvelopes: vi.fn(), + renderOptions: () => { + throw new Error('Plugin error') + }, + } + + flasher.addPlugin('bad', badPlugin) + + flasher.renderOptions({ bad: { some: 'option' } }) + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Error applying options'), + expect.any(Error), + ) + }) + }) + + describe('render()', () => { + it('should process response and render envelopes', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [ + { type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'test' } }, + ], + options: { test: { timeout: 3000 } }, + }) + + expect(plugin.envelopes).toHaveLength(1) + expect(plugin.options).toEqual({ timeout: 3000 }) + }) + + it('should handle empty response gracefully', async () => { + await flasher.render({}) + // Should not throw + }) + + it('should set default CSP nonces if not provided', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [ + { type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'test' } }, + ], + }) + + expect(plugin.envelopes[0].context).toMatchObject({ + csp_style_nonce: '', + csp_script_nonce: '', + }) + }) + + it('should preserve provided CSP nonces', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [ + { type: 'success', message: 'Test', title: 'Test', options: {}, metadata: { plugin: 'test' } }, + ], + context: { + csp_style_nonce: 'style-nonce-123', + csp_script_nonce: 'script-nonce-456', + }, + }) + + expect(plugin.envelopes[0].context).toMatchObject({ + csp_style_nonce: 'style-nonce-123', + csp_script_nonce: 'script-nonce-456', + }) + }) + }) + + describe('function string conversion', () => { + it('should convert regular function strings to functions', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: { + onClick: 'function(event) { return event.target }', + }, + metadata: { plugin: 'test' }, + }], + }) + + const onClick = plugin.envelopes[0].options.onClick + expect(typeof onClick).toBe('function') + }) + + it('should convert arrow function strings to functions', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: { + callback: '(a, b) => a + b', + }, + metadata: { plugin: 'test' }, + }], + }) + + const callback = plugin.envelopes[0].options.callback as (a: number, b: number) => number + expect(typeof callback).toBe('function') + expect(callback(2, 3)).toBe(5) + }) + + it('should handle arrow functions with single parameter (no parens)', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: { + // Single param with parens (more reliable parsing) + transform: '(x) => x * 2', + }, + metadata: { plugin: 'test' }, + }], + }) + + const transform = plugin.envelopes[0].options.transform as (x: number) => number + expect(typeof transform).toBe('function') + expect(transform(5)).toBe(10) + }) + + it('should leave non-function strings unchanged', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: { + text: 'Hello World', + position: 'top-right', + }, + metadata: { plugin: 'test' }, + }], + }) + + expect(plugin.envelopes[0].options.text).toBe('Hello World') + expect(plugin.envelopes[0].options.position).toBe('top-right') + }) + + it('should leave non-string values unchanged', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: { + timeout: 5000, + enabled: true, + data: { key: 'value' }, + }, + metadata: { plugin: 'test' }, + }], + }) + + expect(plugin.envelopes[0].options.timeout).toBe(5000) + expect(plugin.envelopes[0].options.enabled).toBe(true) + expect(plugin.envelopes[0].options.data).toEqual({ key: 'value' }) + }) + + it('should handle invalid function strings gracefully', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: { + // This matches the regex but has invalid JS syntax + bad: 'function() { throw }', + }, + metadata: { plugin: 'test' }, + }], + }) + + // Should return original string on error + expect(plugin.envelopes[0].options.bad).toBe('function() { throw }') + expect(consoleSpy).toHaveBeenCalled() + }) + }) + + describe('asset loading', () => { + it('should add style elements to document head', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + // Mock the load event + const originalCreateElement = document.createElement.bind(document) + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'link' || tag === 'script') { + setTimeout(() => el.onload?.({} as Event), 0) + } + return el + }) + + await flasher.render({ + envelopes: [], + styles: ['/path/to/style.css'], + }) + + const link = document.head.querySelector('link[href="/path/to/style.css"]') + expect(link).toBeTruthy() + expect(link?.getAttribute('rel')).toBe('stylesheet') + }) + + it('should add script elements to document head', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + const originalCreateElement = document.createElement.bind(document) + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'link' || tag === 'script') { + setTimeout(() => el.onload?.({} as Event), 0) + } + return el + }) + + await flasher.render({ + envelopes: [], + scripts: ['/path/to/script.js'], + }) + + const script = document.head.querySelector('script[src="/path/to/script.js"]') + expect(script).toBeTruthy() + expect(script?.getAttribute('type')).toBe('text/javascript') + }) + + it('should not load duplicate assets', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + const originalCreateElement = document.createElement.bind(document) + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'link' || tag === 'script') { + setTimeout(() => el.onload?.({} as Event), 0) + } + return el + }) + + await flasher.render({ + envelopes: [], + styles: ['/path/to/style.css', '/path/to/style.css'], + }) + + const links = document.head.querySelectorAll('link[href="/path/to/style.css"]') + expect(links).toHaveLength(1) + }) + + it('should apply CSP nonce to loaded assets', async () => { + const plugin = new MockPlugin() + flasher.addPlugin('test', plugin) + + const originalCreateElement = document.createElement.bind(document) + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'link' || tag === 'script') { + setTimeout(() => el.onload?.({} as Event), 0) + } + return el + }) + + await flasher.render({ + envelopes: [], + styles: ['/path/to/style.css'], + context: { + csp_style_nonce: 'test-nonce-123', + }, + }) + + const link = document.head.querySelector('link[href="/path/to/style.css"]') + expect(link?.getAttribute('nonce')).toBe('test-nonce-123') + }) + }) + + describe('theme styles handling', () => { + it('should add theme styles to response when using theme plugin', async () => { + flasher.addTheme('custom', { + styles: '/custom/theme.css', + render: () => '
', + }) + + const originalCreateElement = document.createElement.bind(document) + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'link' || tag === 'script') { + setTimeout(() => el.onload?.({} as Event), 0) + } + return el + }) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: {}, + metadata: { plugin: 'theme.custom' }, + }], + }) + + const link = document.head.querySelector('link[href="/custom/theme.css"]') + expect(link).toBeTruthy() + }) + + it('should handle array of theme styles', async () => { + flasher.addTheme('multi', { + styles: ['/style1.css', '/style2.css'], + render: () => '
', + }) + + const originalCreateElement = document.createElement.bind(document) + vi.spyOn(document, 'createElement').mockImplementation((tag: string) => { + const el = originalCreateElement(tag) + if (tag === 'link' || tag === 'script') { + setTimeout(() => el.onload?.({} as Event), 0) + } + return el + }) + + await flasher.render({ + envelopes: [{ + type: 'success', + message: 'Test', + title: 'Test', + options: {}, + metadata: { plugin: 'theme.multi' }, + }], + }) + + expect(document.head.querySelector('link[href="/style1.css"]')).toBeTruthy() + expect(document.head.querySelector('link[href="/style2.css"]')).toBeTruthy() + }) + }) +}) diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts new file mode 100644 index 00000000..4b9b0e62 --- /dev/null +++ b/tests/plugin.test.ts @@ -0,0 +1,262 @@ +import { describe, expect, it, vi } from 'vitest' +import { AbstractPlugin } from '@flasher/flasher/plugin' +import type { Envelope, Options } from '@flasher/flasher/types' + +// Concrete implementation for testing +class TestPlugin extends AbstractPlugin { + public envelopes: Envelope[] = [] + public options: Options = {} + + renderEnvelopes(envelopes: Envelope[]): void { + this.envelopes = envelopes + } + + renderOptions(options: Options): void { + this.options = options + } +} + +describe('AbstractPlugin', () => { + describe('flash() argument normalization', () => { + it('should handle basic call: flash(type, message)', () => { + const plugin = new TestPlugin() + plugin.flash('success', 'Hello World') + + expect(plugin.envelopes).toHaveLength(1) + expect(plugin.envelopes[0]).toMatchObject({ + type: 'success', + message: 'Hello World', + title: 'Success', + options: {}, + }) + }) + + it('should handle call with title: flash(type, message, title)', () => { + const plugin = new TestPlugin() + plugin.flash('error', 'Something went wrong', 'Error Title') + + expect(plugin.envelopes[0]).toMatchObject({ + type: 'error', + message: 'Something went wrong', + title: 'Error Title', + }) + }) + + it('should handle call with options: flash(type, message, title, options)', () => { + const plugin = new TestPlugin() + plugin.flash('info', 'Info message', 'Info', { timeout: 5000 }) + + expect(plugin.envelopes[0]).toMatchObject({ + type: 'info', + message: 'Info message', + title: 'Info', + options: { timeout: 5000 }, + }) + }) + + it('should handle object as first argument: flash({ type, message, title })', () => { + const plugin = new TestPlugin() + plugin.flash({ + type: 'warning', + message: 'Warning message', + title: 'Custom Title', + customOption: true, + }) + + expect(plugin.envelopes[0]).toMatchObject({ + type: 'warning', + message: 'Warning message', + title: 'Custom Title', + options: { customOption: true }, + }) + }) + + it('should handle object as second argument: flash(type, { message, title })', () => { + const plugin = new TestPlugin() + plugin.flash('success', { + message: 'Success message', + title: 'Custom Title', + extra: 'data', + }) + + expect(plugin.envelopes[0]).toMatchObject({ + type: 'success', + message: 'Success message', + title: 'Custom Title', + options: { extra: 'data' }, + }) + }) + + it('should handle object as third argument: flash(type, message, { title })', () => { + const plugin = new TestPlugin() + plugin.flash('info', 'Info message', { title: 'Object Title', key: 'value' }) + + expect(plugin.envelopes[0]).toMatchObject({ + type: 'info', + message: 'Info message', + title: 'Object Title', + options: { key: 'value' }, + }) + }) + + it('should handle object as third argument without title: flash(type, message, { options })', () => { + const plugin = new TestPlugin() + plugin.flash('info', 'Info message', { timeout: 3000 }) + + expect(plugin.envelopes[0]).toMatchObject({ + type: 'info', + message: 'Info message', + title: 'Info', // Auto-generated from type + options: { timeout: 3000 }, + }) + }) + + it('should merge options when third argument is object and fourth is also provided', () => { + const plugin = new TestPlugin() + plugin.flash('success', 'Message', { key1: 'value1' }, { key2: 'value2' }) + + expect(plugin.envelopes[0].options).toMatchObject({ + key1: 'value1', + key2: 'value2', + }) + }) + + it('should auto-generate title from type when not provided', () => { + const plugin = new TestPlugin() + + plugin.flash('success', 'Message') + expect(plugin.envelopes[0].title).toBe('Success') + + plugin.flash('error', 'Message') + expect(plugin.envelopes[0].title).toBe('Error') + + plugin.flash('warning', 'Message') + expect(plugin.envelopes[0].title).toBe('Warning') + + plugin.flash('info', 'Message') + expect(plugin.envelopes[0].title).toBe('Info') + }) + + it('should handle null title by auto-generating', () => { + const plugin = new TestPlugin() + plugin.flash('success', 'Message', null as unknown as string) + + expect(plugin.envelopes[0].title).toBe('Success') + }) + + it('should handle undefined title by auto-generating', () => { + const plugin = new TestPlugin() + plugin.flash('success', 'Message', undefined) + + expect(plugin.envelopes[0].title).toBe('Success') + }) + + it('should throw error when type is missing', () => { + const plugin = new TestPlugin() + + expect(() => plugin.flash({ + message: 'No type provided', + })).toThrow('Type is required for notifications') + }) + + it('should throw error when message is missing', () => { + const plugin = new TestPlugin() + + expect(() => plugin.flash({ + type: 'success', + })).toThrow('Message is required for notifications') + }) + + it('should throw error when message is null', () => { + const plugin = new TestPlugin() + + expect(() => plugin.flash('success', null as unknown as string)).toThrow('Message is required for notifications') + }) + + it('should include metadata with empty plugin string', () => { + const plugin = new TestPlugin() + plugin.flash('success', 'Message') + + expect(plugin.envelopes[0].metadata).toEqual({ plugin: '' }) + }) + }) + + describe('convenience methods', () => { + it('success() should call flash with type "success"', () => { + const plugin = new TestPlugin() + const flashSpy = vi.spyOn(plugin, 'flash') + + plugin.success('Success message', 'Title', { option: true }) + + expect(flashSpy).toHaveBeenCalledWith('success', 'Success message', 'Title', { option: true }) + }) + + it('error() should call flash with type "error"', () => { + const plugin = new TestPlugin() + const flashSpy = vi.spyOn(plugin, 'flash') + + plugin.error('Error message') + + expect(flashSpy).toHaveBeenCalledWith('error', 'Error message', undefined, undefined) + }) + + it('info() should call flash with type "info"', () => { + const plugin = new TestPlugin() + const flashSpy = vi.spyOn(plugin, 'flash') + + plugin.info('Info message', 'Info Title') + + expect(flashSpy).toHaveBeenCalledWith('info', 'Info message', 'Info Title', undefined) + }) + + it('warning() should call flash with type "warning"', () => { + const plugin = new TestPlugin() + const flashSpy = vi.spyOn(plugin, 'flash') + + plugin.warning({ message: 'Warning', title: 'Warn' }) + + expect(flashSpy).toHaveBeenCalledWith('warning', { message: 'Warning', title: 'Warn' }, undefined, undefined) + }) + }) + + describe('renderEnvelopes and renderOptions calls', () => { + it('should call renderOptions before renderEnvelopes', () => { + const plugin = new TestPlugin() + const callOrder: string[] = [] + + vi.spyOn(plugin, 'renderOptions').mockImplementation(() => { + callOrder.push('renderOptions') + }) + vi.spyOn(plugin, 'renderEnvelopes').mockImplementation(() => { + callOrder.push('renderEnvelopes') + }) + + plugin.flash('success', 'Message') + + expect(callOrder).toEqual(['renderOptions', 'renderEnvelopes']) + }) + + it('should pass empty object to renderOptions', () => { + const plugin = new TestPlugin() + const renderOptionsSpy = vi.spyOn(plugin, 'renderOptions') + + plugin.flash('success', 'Message') + + expect(renderOptionsSpy).toHaveBeenCalledWith({}) + }) + + it('should pass envelope array to renderEnvelopes', () => { + const plugin = new TestPlugin() + const renderEnvelopesSpy = vi.spyOn(plugin, 'renderEnvelopes') + + plugin.flash('success', 'Message') + + expect(renderEnvelopesSpy).toHaveBeenCalledWith([ + expect.objectContaining({ + type: 'success', + message: 'Message', + }), + ]) + }) + }) +}) diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 00000000..9b2a2e71 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,14 @@ +import { afterEach, vi } from 'vitest' + +// Clean up DOM after each test +afterEach(() => { + document.body.innerHTML = '' + document.head.innerHTML = '' + vi.clearAllMocks() + vi.restoreAllMocks() + vi.useRealTimers() +}) + +// Mock CSS imports +vi.mock('*.scss', () => ({})) +vi.mock('*.css', () => ({})) diff --git a/tests/themes.test.ts b/tests/themes.test.ts new file mode 100644 index 00000000..975e3085 --- /dev/null +++ b/tests/themes.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, it, vi } from 'vitest' +import type { Envelope } from '@flasher/flasher/types' + +// Mock SCSS imports +vi.mock('@flasher/flasher/themes/flasher/flasher.scss', () => ({})) + +// Import theme after mocking +import { flasherTheme } from '@flasher/flasher/themes/flasher/flasher' + +const createEnvelope = (overrides: Partial = {}): Envelope => ({ + type: 'success', + message: 'Test message', + title: 'Test title', + options: {}, + metadata: { plugin: 'theme.flasher' }, + ...overrides, +}) + +describe('flasherTheme', () => { + describe('render function', () => { + it('should return valid HTML string', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-flasher') + expect(html).toContain('fl-success') + }) + + it('should include message and title', () => { + const html = flasherTheme.render(createEnvelope({ + title: 'My Title', + message: 'My Message', + })) + + expect(html).toContain('My Title') + expect(html).toContain('My Message') + }) + + it('should apply type-specific class', () => { + const types = ['success', 'error', 'warning', 'info'] + + types.forEach((type) => { + const html = flasherTheme.render(createEnvelope({ type })) + expect(html).toContain(`fl-${type}`) + }) + }) + + it('should capitalize type for default title when title is empty', () => { + const html = flasherTheme.render(createEnvelope({ + type: 'success', + title: '', + })) + + expect(html).toContain('Success') + }) + + it('should include close button', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-close') + expect(html).toContain('×') + }) + + it('should include progress bar container', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-progress-bar') + }) + + it('should include icon container', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-icon') + }) + }) + + describe('accessibility', () => { + it('should have role="alert" for error type', () => { + const html = flasherTheme.render(createEnvelope({ type: 'error' })) + + expect(html).toContain('role="alert"') + }) + + it('should have role="alert" for warning type', () => { + const html = flasherTheme.render(createEnvelope({ type: 'warning' })) + + expect(html).toContain('role="alert"') + }) + + it('should have role="status" for success type', () => { + const html = flasherTheme.render(createEnvelope({ type: 'success' })) + + expect(html).toContain('role="status"') + }) + + it('should have role="status" for info type', () => { + const html = flasherTheme.render(createEnvelope({ type: 'info' })) + + expect(html).toContain('role="status"') + }) + + it('should have aria-live="assertive" for error/warning', () => { + const errorHtml = flasherTheme.render(createEnvelope({ type: 'error' })) + const warningHtml = flasherTheme.render(createEnvelope({ type: 'warning' })) + + expect(errorHtml).toContain('aria-live="assertive"') + expect(warningHtml).toContain('aria-live="assertive"') + }) + + it('should have aria-live="polite" for success/info', () => { + const successHtml = flasherTheme.render(createEnvelope({ type: 'success' })) + const infoHtml = flasherTheme.render(createEnvelope({ type: 'info' })) + + expect(successHtml).toContain('aria-live="polite"') + expect(infoHtml).toContain('aria-live="polite"') + }) + + it('should have aria-atomic="true"', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('aria-atomic="true"') + }) + + it('should have accessible close button label', () => { + const html = flasherTheme.render(createEnvelope({ type: 'success' })) + + expect(html).toContain('aria-label="Close success message"') + }) + }) + + describe('HTML structure', () => { + it('should have content wrapper', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-content') + }) + + it('should have title element with fl-title class', () => { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-title') + expect(html).toContain(' { + const html = flasherTheme.render(createEnvelope()) + + expect(html).toContain('fl-message') + expect(html).toContain(' { + it('flasherTheme should have required render function', () => { + expect(typeof flasherTheme.render).toBe('function') + }) + + it('render should return string', () => { + const result = flasherTheme.render(createEnvelope()) + expect(typeof result).toBe('string') + }) + + it('should handle all standard notification types', () => { + const types = ['success', 'error', 'warning', 'info'] + + types.forEach((type) => { + expect(() => flasherTheme.render(createEnvelope({ type }))).not.toThrow() + }) + }) + + it('should handle custom notification types', () => { + expect(() => flasherTheme.render(createEnvelope({ type: 'custom' }))).not.toThrow() + }) + + it('should handle empty message', () => { + expect(() => flasherTheme.render(createEnvelope({ message: '' }))).not.toThrow() + }) + + it('should handle empty title', () => { + expect(() => flasherTheme.render(createEnvelope({ title: '' }))).not.toThrow() + }) + + it('should handle special characters in message', () => { + const html = flasherTheme.render(createEnvelope({ + message: 'Test ', + })) + + // Theme doesn't escape - that's FlasherPlugin's responsibility + expect(html).toContain('Test ') + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..cbdc3027 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'vitest/config' +import { resolve } from 'path' + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/*/Resources/assets/**/*.ts'], + exclude: [ + '**/index.ts', + '**/*.d.ts', + '**/themes/**', + ], + }, + setupFiles: ['./tests/setup.ts'], + }, + resolve: { + alias: { + '@flasher/flasher/dist/plugin': resolve(__dirname, 'src/Prime/Resources/assets/plugin'), + '@flasher/flasher/dist/types': resolve(__dirname, 'src/Prime/Resources/assets/types'), + '@flasher/flasher': resolve(__dirname, 'src/Prime/Resources/assets'), + '@flasher/flasher-noty': resolve(__dirname, 'src/Noty/Prime/Resources/assets'), + '@flasher/flasher-notyf': resolve(__dirname, 'src/Notyf/Prime/Resources/assets'), + '@flasher/flasher-sweetalert': resolve(__dirname, 'src/SweetAlert/Prime/Resources/assets'), + '@flasher/flasher-toastr': resolve(__dirname, 'src/Toastr/Prime/Resources/assets'), + }, + }, +})