diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 226bcd92..4ca47711 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: - name: Install dependencies run: pnpm install + - name: Install Playwright + run: pnpm run test:setup:ci + - name: Lint run: pnpm run lint @@ -35,10 +38,18 @@ jobs: run: pnpm run typecheck && pnpm run dev:typecheck - name: Test - run: pnpm run test + run: pnpm run test:coverage - name: Build run: pnpm run build - name: Publint run: pnpm run publint + + - name: Upload coverage to Codecov + if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/main' }} + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage/browser/lcov.info + disable_search: true diff --git a/.gitignore b/.gitignore index fb7a6484..67067a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store node_modules /dist +/coverage +/tests/__screenshots__ # local env files diff --git a/AGENTS.md b/AGENTS.md index cf8ce1d4..5445ed70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ This project targets Vue 3 + TypeScript with ECMAScript modules. Follow the exis ## Testing Guidelines -There is no standalone unit-test runner yet; rely on TypeScript, linting, and manual QA in the demo. Before opening a PR, run `pnpm lint`, `pnpm typecheck`, and `pnpm build`. Exercise relevant demos in `demo/src/` and add or update examples that showcase new behaviors. For major fixes, include reproduction and verification steps in the PR description so reviewers can follow along. +For complete and up-to-date testing and CI guidance, see [`tests/TESTING.md`](tests/TESTING.md). ## Commit & Pull Request Guidelines diff --git a/README.md b/README.md index 9143acc0..1542cdef 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,8 @@ pnpm dev Open `http://localhost:5173` to see the demo. +For testing and CI details, see [`tests/TESTING.md`](tests/TESTING.md). + ## Notice The Apache Software Foundation [Apache ECharts, ECharts](https://echarts.apache.org/), Apache, the Apache feather, and the Apache ECharts project logo are either registered trademarks or trademarks of the [Apache Software Foundation](https://www.apache.org/). diff --git a/README.zh-Hans.md b/README.zh-Hans.md index d5b5a83d..b3e4dd64 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -436,6 +436,8 @@ pnpm dev 打开 `http://localhost:5173` 来查看 demo。 +更多测试与 CI 说明请参见 [`tests/TESTING.md`](tests/TESTING.md)。 + ## 声明 The Apache Software Foundation [Apache ECharts, ECharts](https://echarts.apache.org/), Apache, the Apache feather, and the Apache ECharts project logo are either registered trademarks or trademarks of the [Apache Software Foundation](https://www.apache.org/). diff --git a/demo/index.html b/demo/index.html index bfaae294..ee175cba 100644 --- a/demo/index.html +++ b/demo/index.html @@ -4,8 +4,8 @@ - - + + =6.0.0'} + '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} @@ -145,6 +161,10 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -254,6 +274,14 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -270,9 +298,6 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@1.0.5': resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} @@ -288,20 +313,30 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oxc-project/runtime@0.89.0': resolution: {integrity: sha512-vP7SaoF0l09GAYuj4IKjfyJodRWC09KdLy8NmnsdUPAsWhPz+2hPTLfEr5+iObDXSNug1xfTxtkGjBLvtwBOPQ==} engines: {node: '>=6.9.0'} - '@oxc-project/runtime@0.92.0': - resolution: {integrity: sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==} + '@oxc-project/runtime@0.90.0': + resolution: {integrity: sha512-TfWn2tT97Weq1/1kTc+6ZeQ3TTj8350HoovtWaUYkX1nie7ONBqeMvudpluj4rmt2jc+l1QsBV/U70Oqsv1S4A==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.89.0': resolution: {integrity: sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==} + '@oxc-project/types@0.90.0': + resolution: {integrity: sha512-fWvaufWUcLtm/OBKcNmxUkR0kQW5ZKAF0t03BXPqdzpxmnVCmSKzvUDRCOKnSagSfNzG/3ZdKpComH3GMy881g==} + '@oxc-project/types@0.92.0': resolution: {integrity: sha512-PDLfCbwgXjGdTBxzcuDOUxJYNBl6P8dOp3eDKWw54dYvqONan9rwGDRQU0zrkdEMiItfXQQUOI17uOcMX5Zm7A==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -322,6 +357,12 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.0.0-beta.39': + resolution: {integrity: sha512-mjraAJQ3VRLPb3BUgVigHvmAYhiBpEeSM0dhvaO6XHtJ0k1o9Ng1Z6Qvlp4/1wDiUf7a10L5c3yleoGZ2r0Maw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-android-arm64@1.0.0-beta.40': resolution: {integrity: sha512-9Ii9phC7QU6Lb+ncMfG1Xlosq0NBB1N/4sw+EGZ3y0BBWGy02TOb5ghWZalphAKv9rn1goqo5WkBjyd2YvsLmA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -334,6 +375,12 @@ packages: cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.0-beta.39': + resolution: {integrity: sha512-tnuiLq9vd08KsZeFkFgzCXVKsTgSZGn+YBQjHSEiUvXJy5pfUf82X/YyLCG8P6I+WDd2cgrcLilMBQPZgaNwkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.0-beta.40': resolution: {integrity: sha512-5O6d0y2tBQTL+ecQY3qXIwSnF1/Zik8q7LZMKeyF+VJ9l194d0IdMhl2zUF0cqWbYHuF4Pnxplk4OhurPQ/Z9Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -346,6 +393,12 @@ packages: cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-beta.39': + resolution: {integrity: sha512-wLFoB3ZM4AoeBlsP0eVbPzWfkEgvmnibMQEKUgWRfJnKhUWiSxl0kGdSw1fNYdX3KAqIeA5gPJNvSJmf6g5S3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-beta.40': resolution: {integrity: sha512-izB9jygt3miPQbOTZfSu5K51isUplqa8ysByOKQqcJHgrBWmbTU8TM9eouv6tRmBR0kjcEcID9xhmA1CeZ1VIg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -358,6 +411,12 @@ packages: cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.0-beta.39': + resolution: {integrity: sha512-wzFZlixF9VMbyi++rHCU4Cy72SH11aBNnkadmvwTAbokwjYHi8NqxQ3/Lx00c700N6kwwuiTsbcGt5DEA9aROw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.0-beta.40': resolution: {integrity: sha512-2fdpEpKT+wwP0vig9dqxu+toTeWmVSjo3psJQVDeLJ51rO+GXcCJ1IkCXjhMKVEevNtZS7B8T8Z2vvmRV9MAdA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -370,6 +429,12 @@ packages: cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.39': + resolution: {integrity: sha512-eVnZcwGbje1uwdFjeQZQ6918RHgGIK7iTC+AoDsgetgAXQmQpnuWYQ9OWa5oTHNQyCkZbMfiHKgpkUPpceMecw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40': resolution: {integrity: sha512-HP2lo78OWULN+8TewpLbS9PS00jh0CaF04tA2u8z2I+6QgVgrYOYKvX+T0hlO5smgso4+qb3YchzumWJl3yCPQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -382,6 +447,12 @@ packages: cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.39': + resolution: {integrity: sha512-Td96iRQA0nmRZM6kJ3+LDDKWLh4bl0zqeR+IYxXwPZBw4iXSREzXrcZ3QqgFHqnXPgryIJEW1U1Ebh2xf+b2UA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40': resolution: {integrity: sha512-ng00gfr9BhA2NPAOU5RWAlTiL+JcwAD+L+4yUD1sbBy6tgHdLiNBOvKtHISIF9RM9/eQeS0tAiWOYZGIH9JMew==} engines: {node: ^20.19.0 || >=22.12.0} @@ -394,6 +465,12 @@ packages: cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.39': + resolution: {integrity: sha512-bcSIh1TFUoPcexJH+gO1sE6wpSR0j3UpWBnjAwyM1PRKfjtqN4R9Du90ofH5KsR/A35FT3eP4mdnhMDTd5Yt+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.40': resolution: {integrity: sha512-mF0R1l9kLcaag/9cLEiYYdNZ4v1uuX4jklSDZ1s6vJE4RB3LirUney0FavdVRwCJ5sDvfvsPgXgtBXWYr2M2tQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -406,6 +483,12 @@ packages: cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.39': + resolution: {integrity: sha512-tYEcZdVGovEemh7ELr+VUoezGkuBgRZYvDHHW/HVIw9LQW5HKLtBIGLzFlOfu/Lq5b9FlDKl+lrY6weviaNnKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.40': resolution: {integrity: sha512-+wi08S7wT5iLPHRZb0USrS6n+T6m+yY++dePYedE5uvKIpWCJJioFTaRtWjpm0V6dVNLcq2OukrvfdlGtH9Wgg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -418,6 +501,12 @@ packages: cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-beta.39': + resolution: {integrity: sha512-xf9QdMC+qwQxtFAty/9RxgCLFdp9pFl09g86hxGPzlzCtHUjd+BmeUnUTXvVC8CHJLWECLQbFP6/233XHG0blA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-beta.40': resolution: {integrity: sha512-W5qBGAemUocIBKCcOsDjlV9GUt28qhl/+M6etWBeLS5gQK0J6XDg0YVzfOQdvq57ZGjYNP0NvhYzqhOOnEx+4g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -430,6 +519,12 @@ packages: cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.0-beta.39': + resolution: {integrity: sha512-QCvN02VpE6zFYry0zAU+29D5+O9tJELNt+OjuCubilZdD/S8xFdho7qBJaa3YhFYyA9cReOMVH8Z8b3yWb4hcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.0-beta.40': resolution: {integrity: sha512-vJwoDehtt+yqj2zacq1AqNc2uE/oh7mnRGqAUbuldV6pgvU01OSQUJ7Zu+35hTopnjFoDNN6mIezkYlGAv5RFA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -441,6 +536,11 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.0-beta.39': + resolution: {integrity: sha512-LFgshxApyBNiBHFVpun7tPrIQ4TvxW0f/endC5C4RzEHu7mxexBCQEkO5XrZ42Cr5DUY+ERNbkfNTUv+vVCaxQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.0-beta.40': resolution: {integrity: sha512-Oj3YyqVUPurr1FlMpEE/bJmMC+VWAWPM/SGUfklO5KUX97bk5Q/733nPg4RykK8q8/TluJoQYvRc05vL/B74dw==} engines: {node: '>=14.0.0'} @@ -452,6 +552,12 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.39': + resolution: {integrity: sha512-Mykirawg+s1e0uzVSEFhUBTShvXrOghPnyuLYkCfw8gzy8bMYiJuxsAfcopzZIIAVOHeSblJoiA/e7gYFjg8HA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40': resolution: {integrity: sha512-0ZtO6yN8XjVoFfN4HDWQj4nDu3ndMybr7jIM00DJqOmc+yFhly7rdOy7fNR9Sky3leCpBtsXfepVqRmVpYKPVA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -464,6 +570,12 @@ packages: cpu: [ia32] os: [win32] + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.39': + resolution: {integrity: sha512-4PQJfWx7mdzXbAa4y+3OSSo911BZyJ/Is4pJKiwcGUqtvY66MX7BqlNWMr9QAozArAGE2knDubLqCQwZpK631w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40': resolution: {integrity: sha512-BPl1inoJXPpIe38Ja46E4y11vXlJyuleo+9Rmu//pYL5fIDYJkXUj/oAXqjSuwLcssrcwnuPgzvzvlz9++cr3w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -476,6 +588,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.39': + resolution: {integrity: sha512-0zmmPOWbFfp1g9ofieimHwhuclZMcib0HL52Q+JTRpOHChI2f83TtH3duKWtAaxqhLUndTr/Z5sxzb+G2FNL9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.40': resolution: {integrity: sha512-UguA4ltbAk+nbwHRxqaUP/etpTbR0HjyNlsu4Zjbh/ytNbFsbw8CA4tEBkwDyjgI5NIPea6xY11zpl7R2/ddVA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -488,6 +606,9 @@ packages: '@rolldown/pluginutils@1.0.0-beta.38': resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} + '@rolldown/pluginutils@1.0.0-beta.39': + resolution: {integrity: sha512-GkTtNCV8ObWbq3LrJStPBv9jkRPct8WlwotVjx3aU0RwfH3LyheixWK9Zhaj22C4EQj/TJxYyetoX+uOn/MWKw==} + '@rolldown/pluginutils@1.0.0-beta.40': resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} @@ -519,8 +640,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.18.5': - resolution: {integrity: sha512-g9BpPfJvxYBXUWI9bV37j6d6LTMNQ88hPwdWWUeYZnMhlo66FIg9gCc1/DZb15QylJSKwOZjwrckvOTWpOiChg==} + '@types/node@22.18.4': + resolution: {integrity: sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==} '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -669,6 +790,15 @@ packages: webdriverio: optional: true + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -773,6 +903,9 @@ packages: '@vue/shared@3.5.21': resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@vue/tsconfig@0.8.1': resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} peerDependencies: @@ -797,6 +930,10 @@ packages: peerDependencies: vue: ^3.5.0 + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -821,6 +958,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -829,6 +970,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + ansis@4.1.0: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} @@ -850,6 +995,9 @@ packages: resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==} engines: {node: '>=20.18.0'} + ast-v8-to-istanbul@0.3.5: + resolution: {integrity: sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -926,6 +1074,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@14.0.1: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} @@ -940,6 +1092,9 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1037,6 +1192,9 @@ packages: oxc-resolver: optional: true + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + echarts-gl@2.0.9: resolution: {integrity: sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==} peerDependencies: @@ -1050,6 +1208,17 @@ packages: echarts@6.0.0: resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1065,8 +1234,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild-wasm@0.25.10: - resolution: {integrity: sha512-IyyfrTA2iiOh/uhlaJj0aUDgW42lFhr29ZeKouVNOz/8mLyuqWbEuVst+B4RBH18pb3AcOHnaOgyskAbsVOe3A==} + esbuild-wasm@0.25.9: + resolution: {integrity: sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA==} engines: {node: '>=18'} hasBin: true @@ -1219,6 +1388,10 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1244,6 +1417,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1270,6 +1447,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1298,10 +1478,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1320,10 +1507,38 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1441,6 +1656,9 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} engines: {node: 20 || >=22} @@ -1458,6 +1676,10 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} @@ -1472,10 +1694,18 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} @@ -1504,6 +1734,11 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -1527,6 +1762,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.3.0: resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} @@ -1548,6 +1786,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1626,6 +1868,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + publint@0.3.12: resolution: {integrity: sha512-1w3MMtL9iotBjm1mmXtG3Nk06wnq9UhGNRpQ2j6n1Zq7YAD6gnxMMZMIxlRPAydVjVbjSm+n0lhwqsD1m4LD5w==} engines: {node: '>=18'} @@ -1733,8 +1978,8 @@ packages: yaml: optional: true - rolldown-vite@7.1.13: - resolution: {integrity: sha512-wYRnqlO+nKcvZitHjwXCnGy+xaFW8mBWL6zScZWJK/ZtEs9Be4ngabaDN05l7t+xFgSzZbPYbWdORBVTfWm7uA==} + rolldown-vite@7.1.12: + resolution: {integrity: sha512-JREtUS+Lpa3s5Ha3ajf2F4LMS4BFxlVjpGz0k0ZR8rV3ZO3tzk5hukqyi9yRBcrvnTUg/BEForyCDahALFYAZA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1778,6 +2023,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.0.0-beta.39: + resolution: {integrity: sha512-05bTT0CJU9dvCRC0Uc4zwB79W5N9MV9OG/Inyx8KNE2pSrrApJoWxEEArW6rmjx113HIx5IreCoTjzLfgvXTdg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rolldown@1.0.0-beta.40: resolution: {integrity: sha512-VqEHbKpOgTPmQrZ4fVn4eshDQS/6g/fRpNE7cFSJY+eQLDZn4B9X61J6L+hnlt1u2uRI+pF7r1USs6S5fuWCvw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1816,6 +2066,10 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -1834,6 +2088,22 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1856,6 +2126,10 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1968,8 +2242,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unplugin-raw@0.6.2: - resolution: {integrity: sha512-mS3mHxNAzKsPlE2sNx0fsNoVvGh2yt5x+F+XvAvymN11GCHCK7G0/5xBFvazWt+oH3Q5/66pICB/JYCZu1pGTg==} + unplugin-raw@0.6.1: + resolution: {integrity: sha512-hXp1acn3YcqJqljk+6sO7KY0OQtp15gEKdxQ6judFTwu+n/I65CIxj1EnRkCh4rWXKcIBg9XEN7FR+XSVI8fAQ==} engines: {node: '>=20.18.0'} peerDependencies: esbuild: '>=0.25.0' @@ -1977,9 +2251,9 @@ packages: esbuild: optional: true - unplugin-utils@0.3.0: - resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==} - engines: {node: '>=20.19.0'} + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} unplugin@2.3.10: resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} @@ -1996,6 +2270,14 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vitest-browser-vue@1.1.0: + resolution: {integrity: sha512-zeMJ0fXRmvG225dXx4sMf5rb7vQC4OCRK7tuVRPCca4x93e2E7VRuPiKSwHRWDvCQQoA3VV09mJPO0xGm9VEEA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + '@vitest/browser': ^2.1.0 || ^3.0.0 || ^4.0.0-0 + vitest: ^2.1.0 || ^3.0.0 || ^4.0.0-0 + vue: ^3.0.0 + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2027,6 +2309,9 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + vue-eslint-parser@10.2.0: resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2084,6 +2369,14 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -2124,6 +2417,11 @@ packages: snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + '@asamuzakjp/css-color@4.0.5': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -2150,7 +2448,6 @@ snapshots: '@babel/helper-validator-identifier': 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 - optional: true '@babel/generator@7.28.3': dependencies: @@ -2168,14 +2465,15 @@ snapshots: dependencies: '@babel/types': 7.28.4 - '@babel/runtime@7.28.4': - optional: true + '@babel/runtime@7.28.4': {} '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@bcoe/v8-coverage@1.0.2': {} + '@csstools/color-helpers@5.1.0': optional: true @@ -2287,15 +2585,26 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/resolve-uri@3.1.2': {} @@ -2306,11 +2615,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@1.0.5': dependencies: '@emnapi/core': 1.5.0 @@ -2330,18 +2634,24 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@one-ini/wasm@0.1.1': {} + '@oxc-project/runtime@0.89.0': {} - '@oxc-project/runtime@0.92.0': {} + '@oxc-project/runtime@0.90.0': {} '@oxc-project/types@0.89.0': {} + '@oxc-project/types@0.90.0': {} + '@oxc-project/types@0.92.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.2.9': {} - '@polka/url@1.0.0-next.29': - optional: true + '@polka/url@1.0.0-next.29': {} '@publint/pack@0.1.2': {} @@ -2352,60 +2662,90 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-beta.38': optional: true + '@rolldown/binding-android-arm64@1.0.0-beta.39': + optional: true + '@rolldown/binding-android-arm64@1.0.0-beta.40': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.38': optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.39': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.40': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.38': optional: true + '@rolldown/binding-darwin-x64@1.0.0-beta.39': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-beta.40': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.38': optional: true + '@rolldown/binding-freebsd-x64@1.0.0-beta.39': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-beta.40': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.39': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.39': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.39': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.40': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.39': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.40': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-beta.39': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-beta.40': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-beta.39': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-beta.40': optional: true @@ -2414,6 +2754,11 @@ snapshots: '@napi-rs/wasm-runtime': 1.0.5 optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-beta.39': + dependencies: + '@napi-rs/wasm-runtime': 1.0.5 + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-beta.40': dependencies: '@napi-rs/wasm-runtime': 1.0.5 @@ -2422,18 +2767,27 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.39': + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40': optional: true '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': optional: true + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.39': + optional: true + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.39': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.40': optional: true @@ -2441,6 +2795,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.38': {} + '@rolldown/pluginutils@1.0.0-beta.39': {} + '@rolldown/pluginutils@1.0.0-beta.40': {} '@testing-library/dom@10.4.1': @@ -2453,20 +2809,17 @@ snapshots: lz-string: 1.5.0 picocolors: 1.1.1 pretty-format: 27.5.1 - optional: true '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 - optional: true '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true - '@types/aria-query@5.0.4': - optional: true + '@types/aria-query@5.0.4': {} '@types/chai@5.2.2': dependencies: @@ -2478,7 +2831,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.18.5': + '@types/node@22.18.4': dependencies: undici-types: 6.21.0 @@ -2633,22 +2986,22 @@ snapshots: optionalDependencies: vue: 3.5.21(typescript@5.9.2) - '@vitejs/plugin-vue@6.0.1(rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vitejs/plugin-vue@6.0.1(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1) + vite: rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) - '@vitest/browser@3.2.4(playwright@1.55.0)(rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.55.0)(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1)) '@vitest/utils': 3.2.4 magic-string: 0.30.19 sirv: 3.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.18.5)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.18.4)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1) ws: 8.18.3 optionalDependencies: playwright: 1.55.0 @@ -2657,7 +3010,27 @@ snapshots: - msw - utf-8-validate - vite - optional: true + + '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.5 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.19 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.18.4)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1) + optionalDependencies: + '@vitest/browser': 3.2.4(playwright@1.55.0)(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))(vitest@3.2.4) + transitivePeerDependencies: + - supports-color '@vitest/expect@3.2.4': dependencies: @@ -2667,22 +3040,21 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(rolldown-vite@7.1.11(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(rolldown-vite@7.1.11(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.18 + magic-string: 0.30.19 optionalDependencies: - vite: rolldown-vite@7.1.11(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1) + vite: rolldown-vite@7.1.11(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1) - '@vitest/mocker@3.2.4(rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.18 + magic-string: 0.30.19 optionalDependencies: - vite: rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1) - optional: true + vite: rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2834,6 +3206,11 @@ snapshots: '@vue/shared@3.5.21': {} + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + '@vue/tsconfig@0.8.1(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))': optionalDependencies: typescript: 5.9.2 @@ -2852,6 +3229,8 @@ snapshots: dependencies: vue: 3.5.21(typescript@5.9.2) + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2870,15 +3249,17 @@ snapshots: alien-signals@2.0.7: {} - ansi-regex@5.0.1: - optional: true + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: - optional: true + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} ansis@4.1.0: {} @@ -2889,7 +3270,6 @@ snapshots: aria-query@5.3.0: dependencies: dequal: 2.0.3 - optional: true assertion-error@2.0.1: {} @@ -2898,6 +3278,12 @@ snapshots: '@babel/parser': 7.28.4 pathe: 2.0.3 + ast-v8-to-istanbul@0.3.5: + dependencies: + '@jridgewell/trace-mapping': 0.3.30 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + balanced-match@1.0.2: {} bidi-js@1.0.3: @@ -2992,6 +3378,8 @@ snapshots: color-name@1.1.4: {} + commander@10.0.1: {} + commander@14.0.1: {} comment-mark@2.0.1: {} @@ -3000,6 +3388,11 @@ snapshots: confbox@0.2.2: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + consola@3.4.2: {} copy-anything@3.0.5: @@ -3056,8 +3449,7 @@ snapshots: defu@6.1.4: {} - dequal@2.0.3: - optional: true + dequal@2.0.3: {} destr@2.0.5: {} @@ -3065,13 +3457,14 @@ snapshots: diff@8.0.2: {} - dom-accessibility-api@0.5.16: - optional: true + dom-accessibility-api@0.5.16: {} dotenv@17.2.1: {} dts-resolver@2.1.2: {} + eastasianwidth@0.2.0: {} + echarts-gl@2.0.9(echarts@6.0.0): dependencies: claygl: 1.3.0 @@ -3087,6 +3480,17 @@ snapshots: tslib: 2.3.0 zrender: 6.0.0 + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.2 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + empathic@2.0.0: {} entities@4.5.0: {} @@ -3096,7 +3500,7 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild-wasm@0.25.10: {} + esbuild-wasm@0.25.9: {} escalade@3.2.0: {} @@ -3255,6 +3659,11 @@ snapshots: flatted@3.3.3: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fsevents@2.3.2: optional: true @@ -3282,6 +3691,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@14.0.0: {} graphemer@1.4.0: {} @@ -3299,6 +3717,8 @@ snapshots: whatwg-encoding: 3.1.1 optional: true + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -3331,8 +3751,12 @@ snapshots: imurmurhash@0.1.4: {} + ini@1.3.8: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3346,10 +3770,46 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.30 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.5.1: {} - js-tokens@4.0.0: - optional: true + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -3458,11 +3918,12 @@ snapshots: loupe@3.2.1: {} + lru-cache@10.4.3: {} + lru-cache@11.2.2: optional: true - lz-string@1.5.0: - optional: true + lz-string@1.5.0: {} magic-string@0.30.18: dependencies: @@ -3477,7 +3938,10 @@ snapshots: '@babel/parser': 7.28.4 '@babel/types': 7.28.4 source-map-js: 1.2.1 - optional: true + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 mdn-data@2.12.2: optional: true @@ -3493,16 +3957,21 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 + minipass@7.1.2: {} + mitt@3.0.1: {} mri@1.2.0: {} - mrmime@2.0.1: - optional: true + mrmime@2.0.1: {} ms@2.1.3: {} @@ -3514,6 +3983,10 @@ snapshots: node-fetch-native@1.6.7: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -3545,6 +4018,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + package-manager-detector@1.3.0: {} parent-module@1.0.1: @@ -3562,6 +4037,11 @@ snapshots: path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + pathe@2.0.3: {} pathval@2.0.1: {} @@ -3587,15 +4067,13 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 - playwright-core@1.55.0: - optional: true + playwright-core@1.55.0: {} playwright@1.55.0: dependencies: playwright-core: 1.55.0 optionalDependencies: fsevents: 2.3.2 - optional: true postcss-nested@7.0.2(postcss@8.5.6): dependencies: @@ -3631,7 +4109,8 @@ snapshots: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 17.0.2 - optional: true + + proto-list@1.2.4: {} publint@0.3.12: dependencies: @@ -3651,8 +4130,7 @@ snapshots: defu: 6.1.4 destr: 2.0.5 - react-is@17.0.2: - optional: true + react-is@17.0.2: {} readdirp@4.1.2: {} @@ -3691,7 +4169,7 @@ snapshots: - oxc-resolver - supports-color - rolldown-vite@7.1.11(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1): + rolldown-vite@7.1.11(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1): dependencies: '@oxc-project/runtime': 0.89.0 fdir: 6.5.0(picomatch@4.0.3) @@ -3701,22 +4179,22 @@ snapshots: rolldown: 1.0.0-beta.38 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.5 + '@types/node': 22.18.4 fsevents: 2.3.3 jiti: 2.5.1 yaml: 2.8.1 - rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1): + rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1): dependencies: - '@oxc-project/runtime': 0.92.0 + '@oxc-project/runtime': 0.90.0 fdir: 6.5.0(picomatch@4.0.3) lightningcss: 1.30.1 picomatch: 4.0.3 postcss: 8.5.6 - rolldown: 1.0.0-beta.40 + rolldown: 1.0.0-beta.39 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.5 + '@types/node': 22.18.4 fsevents: 2.3.3 jiti: 2.5.1 yaml: 2.8.1 @@ -3742,6 +4220,27 @@ snapshots: '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.38 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.38 + rolldown@1.0.0-beta.39: + dependencies: + '@oxc-project/types': 0.90.0 + '@rolldown/pluginutils': 1.0.0-beta.39 + ansis: 4.1.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.39 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.39 + '@rolldown/binding-darwin-x64': 1.0.0-beta.39 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.39 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.39 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.39 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.39 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.39 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.39 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.39 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.39 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.39 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.39 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.39 + rolldown@1.0.0-beta.40: dependencies: '@oxc-project/types': 0.92.0 @@ -3792,12 +4291,13 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 - optional: true source-map-js@1.2.1: {} @@ -3807,6 +4307,26 @@ snapshots: std-env@3.9.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + strip-json-comments@3.1.1: {} strip-literal@3.0.0: @@ -3828,6 +4348,12 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -3862,8 +4388,7 @@ snapshots: dependencies: is-number: 7.0.0 - totalist@3.0.1: - optional: true + totalist@3.0.1: {} tough-cookie@6.0.0: dependencies: @@ -3938,12 +4463,12 @@ snapshots: undici-types@6.21.0: {} - unplugin-raw@0.6.2: + unplugin-raw@0.6.1: dependencies: unplugin: 2.3.10 - unplugin-utils: 0.3.0 + unplugin-utils: 0.2.5 - unplugin-utils@0.3.0: + unplugin-utils@0.2.5: dependencies: pathe: 2.0.3 picomatch: 4.0.3 @@ -3961,13 +4486,13 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@3.2.4(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1): + vite-node@3.2.4(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: rolldown-vite@7.1.11(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1) + vite: rolldown-vite@7.1.11(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - esbuild @@ -3982,11 +4507,18 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@22.18.5)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1): + vitest-browser-vue@1.1.0(@vitest/browser@3.2.4)(vitest@3.2.4)(vue@3.5.21(typescript@5.9.2)): + dependencies: + '@vitest/browser': 3.2.4(playwright@1.55.0)(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))(vitest@3.2.4) + '@vue/test-utils': 2.4.6 + vitest: 3.2.4(@types/node@22.18.4)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1) + vue: 3.5.21(typescript@5.9.2) + + vitest@3.2.4(@types/node@22.18.4)(@vitest/browser@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(rolldown-vite@7.1.11(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(rolldown-vite@7.1.11(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4004,12 +4536,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: rolldown-vite@7.1.11(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1) + vite: rolldown-vite@7.1.11(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.18.5 - '@vitest/browser': 3.2.4(playwright@1.55.0)(rolldown-vite@7.1.13(@types/node@22.18.5)(jiti@2.5.1)(yaml@2.8.1))(vitest@3.2.4) + '@types/node': 22.18.4 + '@vitest/browser': 3.2.4(playwright@1.55.0)(rolldown-vite@7.1.12(@types/node@22.18.4)(jiti@2.5.1)(yaml@2.8.1))(vitest@3.2.4) jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: - esbuild @@ -4027,6 +4559,8 @@ snapshots: vscode-uri@3.1.0: {} + vue-component-type-helpers@2.2.12: {} + vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.5.1)): dependencies: debug: 4.4.3 @@ -4090,8 +4624,19 @@ snapshots: word-wrap@1.2.5: {} - ws@8.18.3: - optional: true + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + ws@8.18.3: {} xml-name-validator@4.0.0: {} diff --git a/src/ECharts.ts b/src/ECharts.ts index 5154dcd4..d99c5205 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -103,31 +103,21 @@ export default defineComponent({ let lastSignature: Signature | undefined; - function resolveUpdateOptions( - plan?: UpdatePlan, - override?: UpdateOptions, - ): UpdateOptions { - const base = realUpdateOptions.value; - const result: UpdateOptions = { ...override }; - - const replacements = [ - ...(plan?.replaceMerge ?? []), - ...(override?.replaceMerge ?? []), - ].filter((key): key is string => key != null); + function resolveUpdateOptions(plan?: UpdatePlan): UpdateOptions { + const result: UpdateOptions = {}; + + const replacements = (plan?.replaceMerge ?? []).filter( + (key): key is string => key != null, + ); if (replacements.length > 0) { result.replaceMerge = [...new Set(replacements)]; - } else { - delete result.replaceMerge; } - const notMerge = override?.notMerge ?? plan?.notMerge; - if (notMerge !== undefined) { - result.notMerge = notMerge; - } else { - delete result.notMerge; + if (plan?.notMerge !== undefined) { + result.notMerge = plan.notMerge; } - return base ? { ...base, ...result } : result; + return result; } function applyOption( @@ -156,7 +146,7 @@ export default defineComponent({ patched as unknown as EChartsOption, ); - const updateOptions = resolveUpdateOptions(planned.plan, override); + const updateOptions = resolveUpdateOptions(planned.plan); instance.setOption(planned.option, updateOptions); lastSignature = planned.signature; } @@ -220,8 +210,13 @@ export default defineComponent({ if (once) { const raw = handler; + let called = false; handler = (...args: any[]) => { + if (called) { + return; + } + called = true; raw(...args); target.off(event, handler); }; @@ -274,10 +269,10 @@ export default defineComponent({ typeof notMerge === "boolean" ? { notMerge, lazyUpdate } : notMerge; if (!chart.value) { - init(option, true, updateOptions ?? undefined); - } else { - applyOption(chart.value, option, updateOptions ?? undefined, true); + return; } + + applyOption(chart.value, option, updateOptions ?? undefined, true); }; function cleanup() { @@ -306,10 +301,10 @@ export default defineComponent({ return; } if (!chart.value) { - init(); - } else { - applyOption(chart.value, option); + return; } + + applyOption(chart.value, option); }, { deep: true }, ); diff --git a/src/composables/slot.ts b/src/composables/slot.ts index b3555267..e8ce0d64 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -10,7 +10,7 @@ import { } from "vue"; import type { Slots, SlotsType } from "vue"; import type { Option } from "../types"; -import { isValidArrayIndex, isSameSet } from "../utils"; +import { isBrowser, isValidArrayIndex, isSameSet } from "../utils"; import type { TooltipComponentFormatterCallbackParams } from "echarts"; const SLOT_OPTION_PATHS = { @@ -29,8 +29,7 @@ function isValidSlotName(key: string): key is SlotName { } export function useSlotOption(slots: Slots, onSlotsChange: () => void) { - const detachedRoot = - typeof window !== "undefined" ? document.createElement("div") : undefined; + const detachedRoot = isBrowser() ? document.createElement("div") : undefined; const containers = shallowReactive>({}); const initialized = shallowReactive>({}); const params = shallowReactive>({}); @@ -39,7 +38,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { // Teleport the slots to a detached root const teleportedSlots = () => { // Make slots client-side only to avoid SSR hydration mismatch - return isMounted.value + return isMounted.value && detachedRoot ? h( Teleport, { to: detachedRoot }, diff --git a/src/smart-update.ts b/src/smart-update.ts index a36bf394..636a3588 100644 --- a/src/smart-update.ts +++ b/src/smart-update.ts @@ -1,4 +1,4 @@ -import type { EChartsOption } from "echarts"; +import type { Option } from "./types"; import { isPlainObject } from "./utils"; export interface UpdatePlan { @@ -49,7 +49,7 @@ function readId(item: unknown): string | undefined { * Build a minimal signature from a full ECharts option. * Only top-level keys are inspected. */ -export function buildSignature(option: EChartsOption): Signature { +export function buildSignature(option: Option): Signature { const opt = option as Record; const optionsLength = Array.isArray(opt.options) @@ -152,7 +152,7 @@ function hasMissingIds( } export interface PlannedUpdate { - option: EChartsOption; + option: Option; signature: Signature; plan: UpdatePlan; } @@ -163,7 +163,7 @@ export interface PlannedUpdate { */ export function planUpdate( prev: Signature | undefined, - option: EChartsOption, + option: Option, ): PlannedUpdate { const next = buildSignature(option); @@ -224,7 +224,7 @@ export function planUpdate( overrides.forEach((value, key) => { clone[key] = value; }); - normalizedOption = clone as EChartsOption; + normalizedOption = clone as Option; signature = buildSignature(normalizedOption); } diff --git a/src/utils.ts b/src/utils.ts index ee53c71b..dd75110c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,9 @@ type Attrs = Record; +export function isBrowser(): boolean { + return typeof window !== "undefined" && typeof document !== "undefined"; +} + // Copied from // https://github.com/vuejs/vue-next/blob/5a7a1b8293822219283d6e267496bec02234b0bc/packages/shared/src/index.ts#L40-L41 const onRE = /^on[^a-z]/; diff --git a/src/wc.ts b/src/wc.ts index 424397c7..738326ab 100644 --- a/src/wc.ts +++ b/src/wc.ts @@ -1,3 +1,5 @@ +import { isBrowser } from "./utils"; + let registered: boolean | null = null; export const TAG_NAME = "x-vue-echarts"; @@ -11,30 +13,33 @@ export function register(): boolean { return registered; } - if ( - typeof HTMLElement === "undefined" || - typeof customElements === "undefined" - ) { - return (registered = false); + const registry = globalThis.customElements; + + if (!isBrowser() || !registry?.get) { + registered = false; + return registered; } - try { - class ECElement extends HTMLElement implements EChartsElement { - __dispose: (() => void) | null = null; + if (!registry.get(TAG_NAME)) { + try { + class ECElement extends HTMLElement implements EChartsElement { + __dispose: (() => void) | null = null; - disconnectedCallback() { - if (this.__dispose) { - this.__dispose(); - this.__dispose = null; + disconnectedCallback(): void { + if (this.__dispose) { + this.__dispose(); + this.__dispose = null; + } } } + + registry.define(TAG_NAME, ECElement); + } catch { + registered = false; + return registered; } - if (customElements.get(TAG_NAME) == null) { - customElements.define(TAG_NAME, ECElement); - } - } catch { - return (registered = false); } - return (registered = true); + registered = true; + return registered; } diff --git a/tests/TESTING.md b/tests/TESTING.md new file mode 100644 index 00000000..ec34489e --- /dev/null +++ b/tests/TESTING.md @@ -0,0 +1,23 @@ +# Testing + +We run Vitest in browser mode using Playwright (Chromium) with `vitest-browser-vue` to mount Vue components. + +- Global setup: see `tests/setup.ts` (mocks `echarts/core`, resets DOM after each test). +- Prefer shared helpers under `tests/helpers/` to avoid duplicated setup. +- Test only public behavior; avoid internal implementation details. +- Keep tests deterministic: silence console noise and flush updates/animation frames with provided helpers. + +## Run locally + +- Install dependencies: `pnpm install` +- Install Chromium: `pnpm test:setup` +- Run tests: `pnpm test` +- Coverage (V8): `pnpm test:coverage` + - HTML report: `coverage/browser/index.html` + - LCOV: `coverage/browser/lcov.info` + +## CI + +- CI runs tests with coverage and uploads LCOV to Codecov (non-blocking). +- Chromium is installed via Playwright CLI with system deps: `pnpm exec playwright install --with-deps chromium`. +- Optional: restrict Codecov uploads to PRs and `main` via a workflow condition. diff --git a/tests/api.test.ts b/tests/api.test.ts new file mode 100644 index 00000000..f8bd7066 --- /dev/null +++ b/tests/api.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect, vi } from "vitest"; +import { ref, type Ref } from "vue"; + +import { usePublicAPI, type PublicMethods } from "../src/composables/api"; +import type { EChartsType } from "../src/types"; + +describe("usePublicAPI", () => { + it("throws until chart instance is available", () => { + const chart = ref(undefined); + const api = usePublicAPI(chart as Ref); + + expect(() => api.getWidth()).toThrowError( + "ECharts is not initialized yet.", + ); + + const chartImpl = { + getWidth: vi.fn(() => 320), + getHeight: vi.fn(() => 180), + }; + chart.value = chartImpl as unknown as EChartsType; + + let width: number | undefined; + expect(() => { + width = api.getWidth(); + }).not.toThrow(); + expect(width).toBe(320); + expect(chartImpl.getWidth).toHaveBeenCalledTimes(1); + expect(chartImpl.getHeight).not.toHaveBeenCalled(); + expect(api.getHeight()).toBe(180); + expect(chartImpl.getHeight).toHaveBeenCalledTimes(1); + }); + + it("forwards public calls to the ECharts instance", () => { + const methodNames = [ + "getWidth", + "getHeight", + "getDom", + "getOption", + "resize", + "dispatchAction", + "convertToPixel", + "convertFromPixel", + "containPixel", + "getDataURL", + "getConnectedDataURL", + "appendData", + "clear", + "isDisposed", + "dispose", + ] as const; + + const chartImpl: Record = { marker: "chart-instance" }; + const callArgs: Record = {}; + + methodNames.forEach((name) => { + chartImpl[name] = vi.fn(function ( + this: Record, + ...args: any[] + ) { + callArgs[name] = args; + expect(this.marker).toBe("chart-instance"); + return `result:${name}`; + }); + }); + + const chart = ref(); + chart.value = chartImpl as unknown as EChartsType; + const api = usePublicAPI(chart as Ref); + + const argsByName: Record<(typeof methodNames)[number], any[]> = { + getWidth: [], + getHeight: [], + getDom: [], + getOption: [], + resize: [{ width: 200, height: 100 }], + dispatchAction: [{ type: "highlight" }], + convertToPixel: ["grid", [0, 1]], + convertFromPixel: ["grid", [10, 20]], + containPixel: ["series", [1, 2]], + getDataURL: [], + getConnectedDataURL: [], + appendData: [{ seriesIndex: 0, data: [1, 2, 3] }], + clear: [], + isDisposed: [], + dispose: [], + }; + + methodNames.forEach((name) => { + const result = ( + api[name as keyof PublicMethods] as (...args: any[]) => any + )(...argsByName[name]); + expect(result).toBe(`result:${name}`); + expect(chartImpl[name]).toHaveBeenCalledTimes(1); + expect(callArgs[name]).toEqual(argsByName[name]); + }); + }); + + it("throws again if the chart instance is cleared after initialization", () => { + const chart = ref(); + const api = usePublicAPI(chart as Ref); + + const chartImpl = { + getWidth: vi.fn(() => 240), + }; + + chart.value = chartImpl as unknown as EChartsType; + + expect(api.getWidth()).toBe(240); + expect(chartImpl.getWidth).toHaveBeenCalledTimes(1); + + chart.value = undefined; + + expect(() => api.getWidth()).toThrowError( + "ECharts is not initialized yet.", + ); + }); +}); diff --git a/tests/autoresize.test.ts b/tests/autoresize.test.ts new file mode 100644 index 00000000..05be0957 --- /dev/null +++ b/tests/autoresize.test.ts @@ -0,0 +1,184 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { ref, effectScope, nextTick, type Ref } from "vue"; + +import { throttle, resetECharts } from "./helpers/mock"; +import { createSizedContainer, flushAnimationFrame } from "./helpers/dom"; +import { useAutoresize } from "../src/composables/autoresize"; +import type { AutoResize, EChartsType } from "../src/types"; + +describe("useAutoresize", () => { + beforeEach(() => { + resetECharts(); + }); + + it("observes the root element and triggers resize on size change", async () => { + const resize = vi.fn(); + const chart = ref(); + const autoresize = ref(true); + const root = ref(); + + const observeSpy = vi.spyOn(window.ResizeObserver.prototype, "observe"); + const disconnectSpy = vi.spyOn( + window.ResizeObserver.prototype, + "disconnect", + ); + + const container = createSizedContainer(120, 80); + + const scope = effectScope(); + scope.run(() => { + useAutoresize( + chart as Ref, + autoresize as Ref, + root as Ref, + ); + }); + + chart.value = { resize } as unknown as EChartsType; + root.value = container; + await nextTick(); + + expect(observeSpy).toHaveBeenCalledWith(container); + + await flushAnimationFrame(); + expect(resize).not.toHaveBeenCalled(); + + container.style.width = "200px"; + await flushAnimationFrame(); + + expect(resize).toHaveBeenCalledTimes(1); + + scope.stop(); + await flushAnimationFrame(); + expect(disconnectSpy).toHaveBeenCalledTimes(1); + }); + + it("skips resize when autoresize is disabled or container is empty", async () => { + const resize = vi.fn(); + const chart = ref(); + const autoresize = ref(); + const root = ref(); + + const observeSpy = vi.spyOn(window.ResizeObserver.prototype, "observe"); + + const container = createSizedContainer(0, 0); + + const scope = effectScope(); + scope.run(() => { + useAutoresize( + chart as Ref, + autoresize as Ref, + root as Ref, + ); + }); + + chart.value = { resize } as unknown as EChartsType; + root.value = container; + await nextTick(); + + expect(observeSpy).not.toHaveBeenCalled(); + expect(resize).not.toHaveBeenCalled(); + + autoresize.value = true; + await nextTick(); + + expect(observeSpy).toHaveBeenCalledWith(container); + + container.style.height = "120px"; + await flushAnimationFrame(); + expect(resize).not.toHaveBeenCalled(); + + container.style.width = "160px"; + await flushAnimationFrame(); + expect(resize).toHaveBeenCalledTimes(1); + + scope.stop(); + }); + + it("invokes onResize callbacks and respects throttle options", async () => { + const resize = vi.fn(); + const chart = ref(); + const onResize = vi.fn(); + const autoresize = ref({ throttle: 0, onResize }); + const root = ref(); + + const container = createSizedContainer(80, 60); + + const scope = effectScope(); + scope.run(() => { + useAutoresize( + chart as Ref, + autoresize as Ref, + root as Ref, + ); + }); + + chart.value = { resize } as unknown as EChartsType; + root.value = container; + await nextTick(); + + expect(vi.mocked(throttle)).not.toHaveBeenCalled(); + + container.style.height = "100px"; + await flushAnimationFrame(); + + expect(resize).toHaveBeenCalledTimes(1); + expect(onResize).toHaveBeenCalledTimes(1); + + autoresize.value = { throttle: 150 }; + await nextTick(); + + expect(vi.mocked(throttle)).toHaveBeenCalledTimes(1); + const [, wait] = vi.mocked(throttle).mock.calls[0]; + expect(wait).toBe(150); + + scope.stop(); + }); + + it("disconnects observer when autoresize toggles off and reactivates cleanly", async () => { + const resize = vi.fn(); + const chart = ref(); + const autoresize = ref(true); + const root = ref(); + + const observeSpy = vi.spyOn(window.ResizeObserver.prototype, "observe"); + const disconnectSpy = vi.spyOn( + window.ResizeObserver.prototype, + "disconnect", + ); + + const container = createSizedContainer(140, 90); + + const scope = effectScope(); + scope.run(() => { + useAutoresize( + chart as Ref, + autoresize as Ref, + root as Ref, + ); + }); + + chart.value = { resize } as unknown as EChartsType; + root.value = container; + await nextTick(); + + expect(observeSpy).toHaveBeenCalledTimes(1); + + autoresize.value = false; + await nextTick(); + + expect(disconnectSpy).toHaveBeenCalledTimes(1); + expect(resize).not.toHaveBeenCalled(); + + autoresize.value = true; + await nextTick(); + + expect(observeSpy).toHaveBeenCalledTimes(2); + + container.style.height = "120px"; + await flushAnimationFrame(); + expect(resize).toHaveBeenCalledTimes(1); + + scope.stop(); + }); +}); diff --git a/tests/echarts.test.ts b/tests/echarts.test.ts new file mode 100644 index 00000000..9bf49a8f --- /dev/null +++ b/tests/echarts.test.ts @@ -0,0 +1,685 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { defineComponent, h, nextTick, provide, ref, shallowRef } from "vue"; +import { render } from "./helpers/testing"; +import { + init, + enqueueChart, + resetECharts, + type ChartStub, +} from "./helpers/mock"; +import type { UpdateOptions } from "../src/types"; +import { withConsoleWarn } from "./helpers/dom"; +import ECharts, { UPDATE_OPTIONS_KEY } from "../src/ECharts"; +import { renderChart } from "./helpers/renderChart"; + +let chartStub: ChartStub; + +beforeEach(() => { + resetECharts(); + chartStub = enqueueChart(); +}); + +describe("ECharts component", () => { + it("initializes and reacts to reactive props", async () => { + const option = ref({ title: { text: "coffee" } }); + const group = ref("group-a"); + const exposed = shallowRef(); + + const screen = renderChart( + () => ({ option: option.value, group: group.value }), + exposed, + ); + await nextTick(); + + expect(init).toHaveBeenCalledTimes(1); + const [rootEl, theme, initOptions] = init.mock.calls[0]; + expect(rootEl).toBeInstanceOf(HTMLElement); + expect(theme).toBeNull(); + expect(initOptions).toBeUndefined(); + + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + expect(chartStub.setOption.mock.calls[0][0]).toMatchObject({ + title: { text: "coffee" }, + }); + expect(chartStub.group).toBe("group-a"); + + option.value = { title: { text: "latte" } }; + await nextTick(); + expect(chartStub.setOption).toHaveBeenCalledTimes(2); + expect(chartStub.setOption.mock.calls[1][0]).toMatchObject({ + title: { text: "latte" }, + }); + + group.value = "group-b"; + await nextTick(); + expect(chartStub.group).toBe("group-b"); + + screen.unmount(); + await nextTick(); + expect(chartStub.dispose).toHaveBeenCalledTimes(1); + }); + + it("exposes setOption for manual updates", async () => { + const optionRef = ref(); + const exposed = shallowRef(); + + renderChart( + () => ({ option: optionRef.value, manualUpdate: true }), + exposed, + ); + await nextTick(); + + expect(typeof exposed.value?.setOption).toBe("function"); + + const manualOption = { series: [{ type: "bar", data: [1, 2, 3] }] }; + exposed.value.setOption(manualOption); + + expect(chartStub.setOption).toHaveBeenCalledTimes(2); + expect(chartStub.setOption.mock.calls[1][0]).toMatchObject(manualOption); + expect(chartStub.setOption.mock.calls[1][1]).toEqual({}); + }); + + it("ignores setOption when manual-update is false", async () => { + const option = ref({ title: { text: "initial" } }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + + const initialCalls = chartStub.setOption.mock.calls.length; + withConsoleWarn((warnSpy) => { + exposed.value.setOption({ title: { text: "ignored" } }, true); + expect(chartStub.setOption).toHaveBeenCalledTimes(initialCalls); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("[vue-echarts] setOption is only available"), + ); + }); + }); + + it("passes theme and initOptions props and reacts to theme changes", async () => { + const option = ref({ title: { text: "brew" } }); + const theme = ref("dark"); + const initOptions = ref({ renderer: "svg" }); + const exposed = shallowRef(); + + renderChart( + () => ({ + option: option.value, + theme: theme.value, + initOptions: initOptions.value, + }), + exposed, + ); + await nextTick(); + + const [rootEl, passedTheme, passedInit] = init.mock.calls[0]; + expect(rootEl).toBeInstanceOf(HTMLElement); + expect(passedTheme).toBe("dark"); + expect(passedInit).toEqual({ renderer: "svg" }); + + const currentStub = chartStub; + theme.value = { palette: ["#fff"] } as any; + await nextTick(); + expect(currentStub.setTheme).toHaveBeenCalledWith({ palette: ["#fff"] }); + }); + + it("re-initializes when initOptions change", async () => { + const option = ref({ title: { text: "coffee" } }); + const initOptions = ref({ useDirtyRect: true }); + const exposed = shallowRef(); + + renderChart( + () => ({ option: option.value, initOptions: initOptions.value }), + exposed, + ); + await nextTick(); + + const firstStub = chartStub; + const secondStub = enqueueChart(); + chartStub = secondStub; + + initOptions.value = { useDirtyRect: false }; + await nextTick(); + + expect(firstStub.dispose).toHaveBeenCalledTimes(1); + expect(init).toHaveBeenCalledTimes(2); + expect(secondStub.setOption).toHaveBeenCalledTimes(1); + expect(secondStub.setOption.mock.calls[0][0]).toMatchObject({ + title: { text: "coffee" }, + }); + }); + + it("passes updateOptions when provided", async () => { + const option = ref({ title: { text: "first" } }); + const updateOptions = ref({ notMerge: true, replaceMerge: ["series"] }); + const exposed = shallowRef(); + + renderChart( + () => ({ option: option.value, updateOptions: updateOptions.value }), + exposed, + ); + await nextTick(); + + expect(chartStub.setOption.mock.calls[0][1]).toBe(updateOptions.value); + chartStub.setOption.mockClear(); + + option.value = { title: { text: "second" } }; + await nextTick(); + + expect(chartStub.setOption.mock.calls[0][1]).toBe(updateOptions.value); + }); + + it("switches between manual and reactive updates", async () => { + const option = ref({ title: { text: "initial" } }); + const manualUpdate = ref(true); + const exposed = shallowRef(); + + renderChart( + () => ({ + option: option.value, + manualUpdate: manualUpdate.value, + }), + exposed, + ); + await nextTick(); + + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + + option.value = { title: { text: "manual" } }; + await nextTick(); + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + + manualUpdate.value = false; + await nextTick(); + + option.value = { title: { text: "reactive" } }; + await nextTick(); + + expect(chartStub.setOption).toHaveBeenCalledTimes(2); + expect(chartStub.setOption.mock.calls[1][0]).toMatchObject({ + title: { text: "reactive" }, + }); + }); + + it("uses injected updateOptions defaults when not provided via props", async () => { + const option = ref({ series: [{ type: "bar", data: [1, 2] }] }); + const defaults = ref({ + lazyUpdate: true, + replaceMerge: ["dataset"], + }); + const exposed = shallowRef(); + + const Root = defineComponent({ + setup() { + provide(UPDATE_OPTIONS_KEY, () => defaults.value); + return () => + h(ECharts, { + option: option.value, + ref: (value: unknown) => { + exposed.value = value; + }, + }); + }, + }); + + render(Root); + + await nextTick(); + + expect(chartStub.setOption.mock.calls[0][1]).toEqual({ + lazyUpdate: true, + replaceMerge: ["dataset"], + }); + + chartStub.setOption.mockClear(); + + defaults.value = { notMerge: true }; + option.value = { series: [{ type: "line", data: [3, 4] }] }; + await nextTick(); + + expect(chartStub.setOption.mock.calls[0][1]).toEqual({ notMerge: true }); + }); + + it("handles manual setOption when chart instance is missing", async () => { + const optionRef = ref({ title: { text: "initial" } }); + const exposed = shallowRef(); + + renderChart( + () => ({ option: optionRef.value, manualUpdate: true }), + exposed, + ); + await nextTick(); + + const replacement = enqueueChart(); + const initCallsBefore = init.mock.calls.length; + exposed.value.chart.value = undefined; + await nextTick(); + + const manualOption = { title: { text: "rehydrate" } }; + exposed.value.setOption(manualOption); + + expect(init.mock.calls.length).toBe(initCallsBefore); + expect(replacement.setOption).not.toHaveBeenCalled(); + expect(exposed.value.chart.value).toBeUndefined(); + }); + + it("ignores falsy reactive options", async () => { + const option = ref({ title: { text: "present" } }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + + const replacementStub = chartStub; + expect(replacementStub.setOption.mock.calls.length).toBeGreaterThan(0); + replacementStub.setOption.mockClear(); + + option.value = undefined as any; + await nextTick(); + await nextTick(); + + expect(replacementStub.setOption).not.toHaveBeenCalled(); + }); + + it("disposes chart on unmount when root element is unavailable", async () => { + const option = ref({ title: { text: "cleanup" } }); + const exposed = shallowRef(); + + const screen = renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + + chartStub.dispose.mockClear(); + exposed.value.root.value = undefined; + + screen.unmount(); + await nextTick(); + + expect(chartStub.dispose).toHaveBeenCalledTimes(1); + }); + + it("shows and hides loading based on props", async () => { + const option = ref({}); + const loading = ref(true); + const loadingOptions = ref({ text: "Loading" }); + const exposed = shallowRef(); + + renderChart( + () => ({ + option: option.value, + loading: loading.value, + loadingOptions: loadingOptions.value, + }), + exposed, + ); + await nextTick(); + + expect(chartStub.showLoading).toHaveBeenCalledWith( + expect.objectContaining({ text: "Loading" }), + ); + + loading.value = false; + await nextTick(); + expect(chartStub.hideLoading).toHaveBeenCalledTimes(1); + }); + + it("binds chart, zr, and native event listeners", async () => { + const clickHandler = vi.fn(); + const nativeClick = vi.fn(); + const zrMove = vi.fn(); + const option = ref({}); + const exposed = shallowRef(); + + renderChart( + () => ({ + option: option.value, + onClick: clickHandler, + "onNative:click": nativeClick, + "onZr:mousemoveOnce": zrMove, + }), + exposed, + ); + await nextTick(); + + expect(chartStub.on).toHaveBeenCalledWith("click", expect.any(Function)); + const chartListener = chartStub.on.mock.calls[0][1]; + chartListener("payload"); + expect(clickHandler).toHaveBeenCalledWith("payload"); + + const zr = chartStub.getZr(); + expect(zr.on).toHaveBeenCalledWith("mousemove", expect.any(Function)); + const zrListener = zr.on.mock.calls[0][1]; + zrListener("zr-payload"); + expect(zrMove).toHaveBeenCalledWith("zr-payload"); + expect(zr.off).toHaveBeenCalledWith("mousemove", zrListener); + + await nextTick(); + const rootEl = + (exposed.value?.root?.value as HTMLElement | undefined) ?? + (document.querySelector("x-vue-echarts") as HTMLElement | null); + expect(rootEl).toBeInstanceOf(HTMLElement); + rootEl!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(nativeClick).toHaveBeenCalledTimes(1); + }); + + it("removes once listeners after first invocation", async () => { + const clickOnce = vi.fn(); + const zrOnce = vi.fn(); + const option = ref({}); + const exposed = shallowRef(); + + renderChart( + () => ({ + option: option.value, + onClickOnce: clickOnce, + "onZr:clickOnce": zrOnce, + }), + exposed, + ); + await nextTick(); + + const chartCall = chartStub.on.mock.calls.find( + (call: any[]) => call[0] === "click", + ); + expect(chartCall).toBeTruthy(); + const chartListener = chartCall?.[1]; + + chartListener?.("payload"); + chartListener?.("again"); + expect(clickOnce).toHaveBeenCalledTimes(1); + expect(chartStub.off).toHaveBeenCalledWith("click", chartListener); + + const zr = chartStub.getZr(); + const zrCall = zr.on.mock.calls.find((call: any[]) => call[0] === "click"); + expect(zrCall).toBeTruthy(); + const zrListener = zrCall?.[1]; + + zrListener?.("zr"); + zrListener?.("zr-again"); + expect(zrOnce).toHaveBeenCalledTimes(1); + expect(zr.off).toHaveBeenCalledWith("click", zrListener); + }); + + it("plans replaceMerge when series id is removed", async () => { + const option = ref({ + series: [ + { id: "a", type: "bar", data: [1] }, + { id: "b", type: "bar", data: [2] }, + ], + }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + chartStub.setOption.mockClear(); + + // Remove one id to trigger replaceMerge planning + option.value = { + series: [{ id: "b", type: "bar", data: [3] }], + } as any; + await nextTick(); + + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + const updateOptions = chartStub.setOption.mock.calls[0][1]; + expect(updateOptions).toEqual( + expect.objectContaining({ replaceMerge: ["series"] }), + ); + }); + + it("calls resize before commit when autoresize is true", async () => { + const option = ref({ title: { text: "auto" } }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value, autoresize: true }), exposed); + await nextTick(); + + expect(chartStub.resize).toHaveBeenCalled(); + }); + + it("supports boolean notMerge in manual setOption", async () => { + const option = ref({ title: { text: "manual" } }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value, manualUpdate: true }), exposed); + await nextTick(); + + chartStub.setOption.mockClear(); + exposed.value.setOption({ title: { text: "b" } }, true, false); + + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + const updateOptions = chartStub.setOption.mock.calls[0][1]; + expect(updateOptions).toEqual({ notMerge: true, lazyUpdate: false }); + }); + + it("applies empty object when theme becomes falsy", async () => { + const option = ref({}); + const theme = ref({ palette: ["#000"] } as any); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value, theme: theme.value }), exposed); + await nextTick(); + + const current = chartStub; + theme.value = undefined as any; + await nextTick(); + + expect(current.setTheme).toHaveBeenCalledWith({}); + }); + + it("sets notMerge when options array shrinks", async () => { + const option = ref({ options: [{}, {}] } as any); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + + chartStub.setOption.mockClear(); + option.value = { options: [{}] } as any; + await nextTick(); + + const updateOptions = chartStub.setOption.mock.calls[0][1]; + expect(updateOptions).toEqual(expect.objectContaining({ notMerge: true })); + }); + + it("does not re-initialize when calling setOption with an existing instance (manual)", async () => { + const option = ref({ title: { text: "init-manual" } }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value, manualUpdate: true }), exposed); + + init.mockClear(); + chartStub.setOption.mockClear(); + + exposed.value.setOption({ title: { text: "after" } }); + await nextTick(); + + expect(init).not.toHaveBeenCalled(); + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + }); + + it("applies option reactively without re-initialization when option becomes defined", async () => { + const option = ref(null); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value }), exposed); + init.mockClear(); + chartStub.setOption.mockClear(); + + option.value = { title: { text: "now-defined" } }; + await nextTick(); + + expect(init).not.toHaveBeenCalled(); + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + }); + + it("honors override.replaceMerge in update options", async () => { + const option = ref({ series: [{ type: "bar", data: [1] }] }); + const exposed = shallowRef(); + + renderChart(() => ({ option: option.value, manualUpdate: true }), exposed); + await nextTick(); + + chartStub.setOption.mockClear(); + exposed.value.setOption({ series: [{ type: "bar", data: [2] }] }, { + replaceMerge: ["series"], + } as any); + + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + const updateOptions = chartStub.setOption.mock.calls[0][1]; + expect(updateOptions).toEqual( + expect.objectContaining({ replaceMerge: ["series"] }), + ); + }); + + it("merges base updateOptions from props during reactive updates", async () => { + const option = ref({ title: { text: "merge-base" } }); + const exposed = shallowRef(); + + renderChart( + () => ({ option: option.value, updateOptions: { lazyUpdate: true } }), + exposed, + ); + await nextTick(); + + chartStub.setOption.mockClear(); + // Change option to trigger reactive update without special plan flags + option.value = { title: { text: "merge-base-2" } }; + await nextTick(); + + const updateOptions = chartStub.setOption.mock.calls[0][1]; + expect(updateOptions).toEqual( + expect.objectContaining({ lazyUpdate: true }), + ); + }); + + it("sets __dispose on root during unmount when wcRegistered and cleanup runs via disconnectedCallback", async () => { + const option = ref({ title: { text: "wc-dispose" } }); + const exposed = shallowRef(); + + const screen = renderChart(() => ({ option: option.value }), exposed); + await nextTick(); + + const el: any = + (exposed.value?.root?.value as HTMLElement | undefined) ?? + (document.querySelector("x-vue-echarts") as HTMLElement | null); + expect(el).toBeInstanceOf(HTMLElement); + chartStub.dispose.mockClear(); + + // Unmount triggers custom element disconnectedCallback, which invokes __dispose immediately + screen.unmount(); + await nextTick(); + + expect(chartStub.dispose).toHaveBeenCalledTimes(1); + // wc disconnectedCallback should null out the hook after calling it + expect(el.__dispose).toBeNull(); + }); + + it("setOption after unmount is a safe no-op (manual)", async () => { + const option = ref({ title: { text: "mounted" } }); + const exposed = shallowRef(); + + const screen = renderChart( + () => ({ option: option.value, manualUpdate: true }), + exposed, + ); + await nextTick(); + + const callsBefore = chartStub.setOption.mock.calls.length; + + // Capture the function reference before unmount; template ref becomes null on unmount + const callSetOption = exposed.value.setOption as ( + opt: any, + notMerge?: any, + lazyUpdate?: any, + ) => void; + + // Unmount disposes and clears chart.value internally + screen.unmount(); + await nextTick(); + + // Calling setOption after unmount should be a no-op and not throw + expect(() => callSetOption({ title: { text: "after" } })).not.toThrow(); + + expect(chartStub.setOption.mock.calls.length).toBe(callsBefore); + }); + + it("re-applies option when slot set changes (auto mode)", async () => { + const option = ref({ title: { text: "with-slots" } }); + const showExtra = ref(true); + const exposed = shallowRef(); + + const Root = defineComponent({ + setup() { + return () => + h( + ECharts, + { + option: option.value, + ref: (v: any) => (exposed.value = v), + }, + showExtra.value + ? { + tooltip: () => [h("span", "t")], + "tooltip-extra": () => [h("span", "x")], + } + : { + tooltip: () => [h("span", "t")], + }, + ); + }, + }); + + render(Root); + await nextTick(); + + // One initial setOption from mount + const initialCalls = chartStub.setOption.mock.calls.length; + + // Changing slot set triggers useSlotOption onChange, which applies current option again + showExtra.value = false; + await nextTick(); + await nextTick(); + + expect(chartStub.setOption.mock.calls.length).toBeGreaterThan(initialCalls); + }); + + it("skips resize when instance is disposed in autoresize path", async () => { + const option = ref({}); + const exposed = shallowRef(); + + // Force the disposed branch in resize() + chartStub.isDisposed.mockReturnValue(true as any); + + renderChart(() => ({ option: option.value, autoresize: true }), exposed); + await nextTick(); + + // resize should be skipped, commit should still apply option + expect(chartStub.resize).not.toHaveBeenCalled(); + expect(chartStub.setOption).toHaveBeenCalled(); + }); + + it("stops reactive updates after toggling manualUpdate to true", async () => { + const option = ref({ title: { text: "start" } }); + const manual = ref(false); + const exposed = shallowRef(); + + renderChart( + () => ({ option: option.value, manualUpdate: manual.value }), + exposed, + ); + await nextTick(); + + chartStub.setOption.mockClear(); + option.value = { title: { text: "reactive-1" } } as any; + await nextTick(); + expect(chartStub.setOption).toHaveBeenCalledTimes(1); + + // Toggle to manual mode; watcher should be cleaned up (unwatchOption branch) + manual.value = true; + await nextTick(); + + chartStub.setOption.mockClear(); + option.value = { title: { text: "reactive-2" } } as any; + await nextTick(); + expect(chartStub.setOption).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/global.test.ts b/tests/global.test.ts new file mode 100644 index 00000000..2b6857ef --- /dev/null +++ b/tests/global.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "vitest"; + +import entry, * as moduleExports from "../src/index"; +import globalEntry from "../src/global"; + +import ECharts from "../src/ECharts"; + +describe("entry points", () => { + it("re-export ECharts correctly from src/index.ts", () => { + expect(entry).toBe(ECharts); + expect(moduleExports.default).toBe(ECharts); + }); + + it("global entry merges default and named exports", () => { + expect(globalEntry.default).toBe(ECharts); + expect(Object.keys(globalEntry)).toEqual( + expect.arrayContaining(Object.keys(moduleExports)), + ); + }); +}); diff --git a/tests/helpers/dom.ts b/tests/helpers/dom.ts new file mode 100644 index 00000000..88503d0a --- /dev/null +++ b/tests/helpers/dom.ts @@ -0,0 +1,36 @@ +import { vi } from "vitest"; + +export function createSizedContainer( + width = 100, + height = 100, +): HTMLDivElement { + const element = document.createElement("div"); + element.style.width = `${width}px`; + element.style.height = `${height}px`; + element.style.display = "block"; + element.style.position = "relative"; + document.body.appendChild(element); + return element; +} + +export async function flushAnimationFrame(): Promise { + await Promise.resolve(); + await new Promise((resolve) => requestAnimationFrame(() => resolve())); + await Promise.resolve(); +} + +export function withConsoleWarn( + callback: (warnSpy: ReturnType) => T, +): T { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); + + try { + return callback(warnSpy); + } finally { + warnSpy.mockRestore(); + } +} + +export function resetDocumentBody(): void { + document.body.innerHTML = ""; +} diff --git a/tests/helpers/mock.ts b/tests/helpers/mock.ts new file mode 100644 index 00000000..38e650cc --- /dev/null +++ b/tests/helpers/mock.ts @@ -0,0 +1,90 @@ +import { vi } from "vitest"; + +type InitFn = (typeof import("echarts/core"))["init"]; +type ThrottleFn = (typeof import("echarts/core"))["throttle"]; +type Throttled = ReturnType; + +export const init = vi.fn(); +export const throttle = vi.fn(); + +export function createEChartsModule() { + return { + init, + throttle, + } satisfies Partial>; +} + +export interface ChartStub { + setOption: ReturnType; + resize: ReturnType; + dispose: ReturnType; + isDisposed: ReturnType; + getZr: ReturnType; + on: ReturnType; + off: ReturnType; + setTheme: ReturnType; + showLoading: ReturnType; + hideLoading: ReturnType; + group: string | undefined; +} + +const queue: ChartStub[] = []; +let cursor = 0; + +export function createChartStub(): ChartStub { + const zr = { + on: vi.fn(), + off: vi.fn(), + }; + + return { + setOption: vi.fn(), + resize: vi.fn(), + dispose: vi.fn(), + isDisposed: vi.fn(() => false), + getZr: vi.fn(() => zr), + on: vi.fn(), + off: vi.fn(), + setTheme: vi.fn(), + showLoading: vi.fn(), + hideLoading: vi.fn(), + group: undefined, + }; +} + +function ensureStub(): ChartStub { + if (cursor >= queue.length) { + queue.push(createChartStub()); + } + return queue[cursor++]; +} + +const defaultThrottleImplementation: ThrottleFn = ((fn: any) => { + const wrapped = ((...args: any[]) => fn(...args)) as Throttled; + (wrapped as any).clear = vi.fn(); + (wrapped as any).dispose = vi.fn(); + (wrapped as any).pending = vi.fn(() => false); + return wrapped; +}) as ThrottleFn; + +export function resetECharts(): void { + queue.length = 0; + cursor = 0; + + init.mockReset(); + throttle.mockReset(); + + init.mockImplementation(((...args: Parameters) => { + void args; + return ensureStub() as unknown as ReturnType; + }) as InitFn); + throttle.mockImplementation(defaultThrottleImplementation); +} + +export function enqueueChart(): ChartStub { + const stub = createChartStub(); + queue.push(stub); + return stub; +} + +resetECharts(); diff --git a/tests/helpers/renderChart.ts b/tests/helpers/renderChart.ts new file mode 100644 index 00000000..83e505c1 --- /dev/null +++ b/tests/helpers/renderChart.ts @@ -0,0 +1,22 @@ +import { defineComponent, h, type Ref } from "vue"; +import { render } from "vitest-browser-vue/pure"; + +import ECharts from "../../src/ECharts"; + +export type RenderChartProps = () => Record; + +export function renderChart(propsFactory: RenderChartProps, exposes: Ref) { + const Root = defineComponent({ + setup() { + return () => + h(ECharts, { + ...propsFactory(), + ref: (value: unknown) => { + exposes.value = value; + }, + }); + }, + }); + + return render(Root); +} diff --git a/tests/helpers/testing.ts b/tests/helpers/testing.ts new file mode 100644 index 00000000..2341da46 --- /dev/null +++ b/tests/helpers/testing.ts @@ -0,0 +1 @@ +export { cleanup, render } from "vitest-browser-vue/pure"; diff --git a/tests/helpers/wc-disabled.ts b/tests/helpers/wc-disabled.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/loading.test.ts b/tests/loading.test.ts new file mode 100644 index 00000000..da2a7696 --- /dev/null +++ b/tests/loading.test.ts @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { ref, nextTick, type Ref, defineComponent } from "vue"; +import { cleanup, render } from "vitest-browser-vue/pure"; + +import { useLoading, LOADING_OPTIONS_KEY } from "../src/composables/loading"; +import type { + EChartsType, + LoadingOptions, + LoadingOptionsInjection, +} from "../src/types"; + +afterEach(() => { + cleanup(); +}); + +function renderUseLoading( + chart: Ref, + loading: Ref, + loadingOptions: Ref, + defaults?: LoadingOptionsInjection, +) { + const Host = defineComponent({ + setup() { + useLoading(chart, loading, loadingOptions); + return () => null; + }, + }); + + const renderOptions = defaults + ? { + global: { + provide: { + [LOADING_OPTIONS_KEY as symbol]: defaults, + }, + }, + } + : undefined; + + return render(Host, renderOptions); +} + +describe("useLoading", () => { + it("merges injected defaults with explicit options when showing loading", async () => { + const showLoading = vi.fn(); + const hideLoading = vi.fn(); + const chart = ref(); + const loading = ref(false); + const loadingOptions = ref({ + text: "Loading...", + }); + + renderUseLoading(chart, loading, loadingOptions, () => ({ + maskColor: "rgba(0,0,0,0.5)", + })); + + chart.value = { showLoading, hideLoading } as unknown as EChartsType; + await nextTick(); + + expect(showLoading).not.toHaveBeenCalled(); + expect(hideLoading).toHaveBeenCalledTimes(1); + hideLoading.mockClear(); + + loading.value = true; + await nextTick(); + + expect(showLoading).toHaveBeenCalledTimes(1); + expect(showLoading).toHaveBeenCalledWith({ + maskColor: "rgba(0,0,0,0.5)", + text: "Loading...", + }); + + loading.value = false; + await nextTick(); + + expect(hideLoading).toHaveBeenCalledTimes(1); + }); + + it("does nothing until an instance is available", async () => { + const showLoading = vi.fn(); + const hideLoading = vi.fn(); + const chart = ref(); + const loading = ref(true); + const loadingOptions = ref({}); + + renderUseLoading(chart, loading, loadingOptions); + + await nextTick(); + expect(showLoading).not.toHaveBeenCalled(); + expect(hideLoading).not.toHaveBeenCalled(); + + chart.value = { showLoading, hideLoading } as unknown as EChartsType; + await nextTick(); + + expect(showLoading).toHaveBeenCalledTimes(1); + expect(hideLoading).not.toHaveBeenCalled(); + }); + + it("replays showLoading when injected defaults change while active", async () => { + const showLoading = vi.fn(); + const hideLoading = vi.fn(); + const chart = ref(); + const loading = ref(true); + const loadingOptions = ref({ text: "Loading" }); + const defaults = ref({ color: "#fff" }); + + renderUseLoading(chart, loading, loadingOptions, () => defaults.value); + + chart.value = { showLoading, hideLoading } as unknown as EChartsType; + await nextTick(); + + expect(showLoading).toHaveBeenCalledTimes(1); + expect(showLoading).toHaveBeenLastCalledWith({ + color: "#fff", + text: "Loading", + }); + expect(hideLoading).not.toHaveBeenCalled(); + + showLoading.mockClear(); + defaults.value = { color: "#000" }; + await nextTick(); + + expect(showLoading).toHaveBeenCalledTimes(1); + expect(showLoading).toHaveBeenLastCalledWith({ + color: "#000", + text: "Loading", + }); + expect(hideLoading).not.toHaveBeenCalled(); + + loading.value = false; + await nextTick(); + + expect(hideLoading).toHaveBeenCalledTimes(1); + expect(showLoading).not.toHaveBeenCalledTimes(2); + }); + + it("replays showLoading when explicit options change while active", async () => { + const showLoading = vi.fn(); + const hideLoading = vi.fn(); + const chart = ref(); + const loading = ref(true); + const loadingOptions = ref({ + text: "Initial", + color: "#fff", + }); + const defaults = ref({ maskColor: "rgba(0, 0, 0, 0.5)" }); + + renderUseLoading(chart, loading, loadingOptions, () => defaults.value); + + chart.value = { showLoading, hideLoading } as unknown as EChartsType; + await nextTick(); + + expect(showLoading).toHaveBeenCalledTimes(1); + expect(showLoading).toHaveBeenLastCalledWith({ + maskColor: "rgba(0, 0, 0, 0.5)", + text: "Initial", + color: "#fff", + }); + expect(hideLoading).not.toHaveBeenCalled(); + + showLoading.mockClear(); + loadingOptions.value = { text: "Updated", color: "#0f0" }; + await nextTick(); + + expect(showLoading).toHaveBeenCalledTimes(1); + expect(showLoading).toHaveBeenLastCalledWith({ + maskColor: "rgba(0, 0, 0, 0.5)", + text: "Updated", + color: "#0f0", + }); + expect(hideLoading).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 00000000..d10207c3 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,14 @@ +import { afterEach, vi } from "vitest"; +import { cleanup } from "vitest-browser-vue/pure"; + +import { createEChartsModule } from "./helpers/mock"; +import { resetDocumentBody } from "./helpers/dom"; + +// Mock echarts/core globally for browser tests +vi.mock("echarts/core", () => createEChartsModule()); + +// Centralized cleanup for all browser tests +afterEach(() => { + cleanup(); + resetDocumentBody(); +}); diff --git a/tests/slot.test.ts b/tests/slot.test.ts new file mode 100644 index 00000000..fe41c382 --- /dev/null +++ b/tests/slot.test.ts @@ -0,0 +1,245 @@ +import { describe, it, expect, vi } from "vitest"; +import { + defineComponent, + h, + nextTick, + ref, + shallowRef, + watchEffect, + type PropType, +} from "vue"; +import { render } from "./helpers/testing"; + +import { useSlotOption } from "../src/composables/slot"; +import { withConsoleWarn } from "./helpers/dom"; + +type SlotTestHandle = { + patchOption: ReturnType["patchOption"]; + teleportedSlots: ReturnType["teleportedSlots"]; +}; + +const SlotTestComponent = defineComponent({ + props: { + onChange: { + type: Function as PropType<() => void>, + default: undefined, + }, + }, + setup(props, ctx) { + const { teleportedSlots, patchOption } = useSlotOption( + ctx.slots, + props.onChange ?? (() => {}), + ); + + ctx.expose({ patchOption, teleportedSlots }); + + return () => h("div", teleportedSlots()); + }, +}); + +type SlotDictionary = Record any>; + +// cleanup and document reset are handled in tests/setup.ts + +function renderSlotComponent( + slotFactory: () => SlotDictionary, + onChange?: () => void, +): { exposed: ReturnType> } { + const exposed = shallowRef(); + + const Root = defineComponent({ + setup() { + const componentRef = shallowRef(); + + watchEffect(() => { + if (componentRef.value) { + exposed.value = componentRef.value; + } + }); + + return () => + h( + SlotTestComponent, + { + ref: (value: unknown) => { + componentRef.value = value as SlotTestHandle; + }, + onChange, + }, + slotFactory(), + ); + }, + }); + + render(Root); + + return { + exposed, + }; +} + +describe("useSlotOption", () => { + it("returns a Teleport vnode after mount", async () => { + const { exposed } = renderSlotComponent(() => ({ + tooltip: () => [h("span", "t")], + })); + + // Component is mounted by the test renderer synchronously; teleportedSlots should return a Teleport VNode + const vnode: any = exposed.value!.teleportedSlots(); + expect(vnode).toBeTruthy(); + expect(vnode.type?.__isTeleport).toBe(true); + }); + + it("patches tooltip slots and renders teleported content", async () => { + const changeSpy = vi.fn(); + + const { exposed } = renderSlotComponent( + () => ({ + tooltip: (params: any) => [h("span", `tooltip-${params?.dataIndex}`)], + }), + changeSpy, + ); + + await nextTick(); + changeSpy.mockClear(); + + const patched: any = exposed.value!.patchOption({}); + expect(changeSpy).not.toHaveBeenCalled(); + + expect(typeof patched.tooltip?.formatter).toBe("function"); + const container = patched.tooltip!.formatter!({ dataIndex: 42 }); + expect(container).toBeInstanceOf(HTMLElement); + + await nextTick(); + expect(container.textContent).toBe("tooltip-42"); + }); + + it("patches dataView slots and renders teleported content", async () => { + const changeSpy = vi.fn(); + + const { exposed } = renderSlotComponent( + () => ({ + dataView: () => [h("span", "data-view")], + }), + changeSpy, + ); + + await nextTick(); + changeSpy.mockClear(); + + const patched: any = exposed.value!.patchOption({ + toolbox: { feature: {} }, + }); + expect(changeSpy).not.toHaveBeenCalled(); + + const optionToContent = patched.toolbox?.feature?.dataView?.optionToContent; + expect(typeof optionToContent).toBe("function"); + const container = optionToContent?.({}); + expect(container).toBeInstanceOf(HTMLElement); + + await nextTick(); + expect(container?.textContent).toBe("data-view"); + }); + + it("notifies when slot set changes and cleans state", async () => { + const changeSpy = vi.fn(); + const showExtra = ref(true); + + const { exposed } = renderSlotComponent(() => { + const slots: SlotDictionary = { + tooltip: (params: any) => [h("span", `tooltip-${params?.dataIndex}`)], + }; + if (showExtra.value) { + slots["tooltip-extra"] = () => [h("span", "extra")]; + } + return slots; + }, changeSpy); + + await nextTick(); + changeSpy.mockClear(); + + const patched: any = exposed.value!.patchOption({}); + expect(typeof patched.tooltip?.formatter).toBe("function"); + patched.tooltip!.formatter!({ dataIndex: 1 }); + await nextTick(); + + showExtra.value = false; + await nextTick(); + + expect(changeSpy).toHaveBeenCalledTimes(1); + + const patchedAfterRemoval: any = exposed.value!.patchOption({}); + expect(patchedAfterRemoval["tooltip-extra"]).toBeUndefined(); + }); + + it("warns and skips invalid slot names", async () => { + const changeSpy = vi.fn(); + const { exposed } = renderSlotComponent( + () => ({ + legend: () => [h("span", "legend")], + }), + changeSpy, + ); + + await nextTick(); + changeSpy.mockClear(); + + withConsoleWarn((warnSpy) => { + const patched: any = exposed.value!.patchOption({}); + const flattened = warnSpy.mock.calls.flat().join(" "); + + expect(flattened).toContain("Invalid vue-echarts slot name: legend"); + expect(patched.legend).toBeUndefined(); + expect(changeSpy).not.toHaveBeenCalled(); + }); + }); + + it("clones existing array branches when patching series tooltip slots", async () => { + const { exposed } = renderSlotComponent(() => ({ + "tooltip-series-0": () => [h("span", "series-0")], + })); + + await nextTick(); + + const originalOption = { + series: [ + { + tooltip: {}, + }, + ], + }; + + const patched: any = exposed.value!.patchOption(originalOption); + + expect(patched).not.toBe(originalOption); + expect(patched.series).not.toBe(originalOption.series); + + const formatter = patched.series?.[0]?.tooltip?.formatter; + expect(typeof formatter).toBe("function"); + + const container = formatter?.({ dataIndex: 7 }); + expect(container).toBeInstanceOf(HTMLElement); + + await nextTick(); + expect(container?.textContent).toBe("series-0"); + }); + + it("creates array shells when target slot path is missing", async () => { + const { exposed } = renderSlotComponent(() => ({ + "tooltip-series-1": () => [h("span", "series-1")], + })); + + await nextTick(); + + const patched: any = exposed.value!.patchOption({}); + + const formatter = patched.series?.[1]?.tooltip?.formatter; + expect(typeof formatter).toBe("function"); + + const container = formatter?.({ dataIndex: 3 }); + expect(container).toBeInstanceOf(HTMLElement); + + await nextTick(); + expect(container?.textContent).toBe("series-1"); + }); +}); diff --git a/tests/smart-update.test.ts b/tests/smart-update.test.ts index 8e5a122b..f45d73a8 100644 --- a/tests/smart-update.test.ts +++ b/tests/smart-update.test.ts @@ -10,10 +10,7 @@ describe("smart-update", () => { tooltip: { show: true }, color: "#000", dataset: [{ id: "ds1", source: [] }, { source: [] }], - series: [ - { id: "a", type: "bar" }, - { type: "line" }, - ], + series: [{ id: "a", type: "bar" }, { type: "line" }], }; const signature = buildSignature(option); @@ -24,6 +21,27 @@ describe("smart-update", () => { expect(signature.arrays.dataset?.noIdCount).toBe(1); expect(signature.arrays.series?.idsSorted).toEqual(["a"]); expect(signature.arrays.series?.noIdCount).toBe(1); + expect(signature.objects).not.toContain("color"); + expect(signature.scalars).not.toContain("title"); + expect(signature.arrays.tooltip).toBeUndefined(); + }); + + it("treats numeric ids as strings and ignores unsupported ids", () => { + const option: EChartsOption = { + series: [ + { id: 2, type: "bar" }, + { id: 1, type: "line" }, + { id: { nested: true } as unknown, type: "pie" }, + { id: true as unknown as string, type: "scatter" }, + { type: "area" }, + ] as unknown as EChartsOption["series"], + }; + + const signature = buildSignature(option); + const summary = signature.arrays.series; + + expect(summary?.idsSorted).toEqual(["1", "2"]); + expect(summary?.noIdCount).toBe(3); }); }); @@ -83,19 +101,59 @@ describe("smart-update", () => { expect(result.plan.replaceMerge).toBeUndefined(); expect(result.option.series).toEqual(update.series); }); + + it("keeps merge when dataset items reorder without shrink", () => { + const prev = buildSignature({ dataset: [{ id: "a" }, { id: "b" }] }); + const update: EChartsOption = { + dataset: [{ id: "b" }, { id: "a" }], + }; + + const result = planUpdate(prev, update); + + expect(result.plan.notMerge).toBe(false); + expect(result.plan.replaceMerge).toBeUndefined(); + expect(result.option.dataset).toEqual(update.dataset); + }); }); describe("shrink detection", () => { + it("does not mark replace when previously empty array is removed", () => { + const base: EChartsOption = { + // empty array previously present + series: [] as any, + }; + const update: EChartsOption = { + title: { text: "noop" }, + // series key removed entirely + } as any; + + const result = planUpdate(buildSignature(base), update); + + expect(result.plan.notMerge).toBe(false); + expect(result.plan.replaceMerge).toBeUndefined(); + // Should not inject [] override since it was empty before + expect((result.option as any).series).toBeUndefined(); + }); it("forces rebuild when options shrink", () => { const prev = buildSignature({ options: [{}, {}] }); const { plan } = planUpdate(prev, { options: [{}] }); expect(plan.notMerge).toBe(true); + expect(plan.replaceMerge).toBeUndefined(); + }); + + it("forces rebuild when media entries shrink", () => { + const prev = buildSignature({ media: [{}, {}] as any }); + const { plan } = planUpdate(prev, { media: [{}] as any }); + + expect(plan.notMerge).toBe(true); + expect(plan.replaceMerge).toBeUndefined(); }); it("forces rebuild when scalars disappear", () => { const prev = buildSignature({ color: "red", title: { text: "foo" } }); const { plan } = planUpdate(prev, { title: { text: "foo" } }); expect(plan.notMerge).toBe(true); + expect(plan.replaceMerge).toBeUndefined(); }); it("injects null for removed objects", () => { @@ -104,6 +162,7 @@ describe("smart-update", () => { expect(next.option.legend).toBeNull(); expect(next.plan.notMerge).toBe(false); + expect(next.plan.replaceMerge).toBeUndefined(); }); it("injects empty array and replaceMerge when array removed", () => { @@ -112,6 +171,7 @@ describe("smart-update", () => { expect(next.option.series).toEqual([]); expect(next.plan.replaceMerge).toEqual(["series"]); + expect(next.plan.notMerge).toBe(false); }); it("adds replaceMerge when ids shrink", () => { @@ -119,6 +179,8 @@ describe("smart-update", () => { const next = planUpdate(prev, { series: [{ id: "a" }] }); expect(next.plan.replaceMerge).toEqual(["series"]); + expect(next.plan.notMerge).toBe(false); + expect(next.option.series).toEqual([{ id: "a" }]); }); it("adds replaceMerge when anonymous count shrinks", () => { @@ -126,6 +188,8 @@ describe("smart-update", () => { const next = planUpdate(prev, { series: [{}] }); expect(next.plan.replaceMerge).toEqual(["series"]); + expect(next.plan.notMerge).toBe(false); + expect(next.option.series).toEqual([{}]); }); }); @@ -167,6 +231,7 @@ describe("smart-update", () => { expect(result.option.series).toEqual(update.series); expect(result.plan.notMerge).toBe(false); expect(result.plan.replaceMerge).toEqual(["series"]); + expect(result.plan.replaceMerge).not.toContain("dataset"); }); it("clears dataset when removed entirely", () => { @@ -192,6 +257,7 @@ describe("smart-update", () => { expect(result.option.dataset).toEqual([]); expect(result.plan.notMerge).toBe(false); expect(result.plan.replaceMerge).toContain("dataset"); + expect(result.plan.replaceMerge).not.toContain("series"); }); it("tracks multiple array shrink operations", () => { @@ -229,6 +295,7 @@ describe("smart-update", () => { expect(result.option.dataset).toEqual([]); expect(result.plan.notMerge).toBe(false); expect(result.plan.replaceMerge).toEqual(["dataset", "series"]); + expect(result.plan.replaceMerge).not.toContain("legend"); }); it("injects null for tooltip removal while keeping explicit arrays", () => { @@ -274,6 +341,8 @@ describe("smart-update", () => { expect(result.option.dataset).toEqual([]); expect(result.option.series).toEqual(update.series); expect(result.plan.replaceMerge).toEqual(["dataset"]); + expect(result.plan.notMerge).toBe(false); + expect(result.plan.replaceMerge).not.toContain("series"); }); it("tracks series ID removal while keeping modifications", () => { @@ -292,6 +361,8 @@ describe("smart-update", () => { expect(result.option.series).toEqual(update.series); expect(result.plan.replaceMerge).toEqual(["series"]); + expect(result.plan.notMerge).toBe(false); + expect(result.option.series).not.toEqual(base.series); }); }); }); diff --git a/tests/ssr.test.ts b/tests/ssr.test.ts new file mode 100644 index 00000000..fcfd6043 --- /dev/null +++ b/tests/ssr.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, vi } from "vitest"; + +// Mock non-browser environment for this file only +vi.mock("/src/utils.ts", async (importOriginal: any) => { + const actual: any = await importOriginal(); + return { ...actual, isBrowser: () => false }; +}); + +import { h, defineComponent, shallowRef, watchEffect } from "vue"; +import { render, cleanup } from "./helpers/testing"; +import { useSlotOption } from "../src/composables/slot"; + +describe("SSR environment", () => { + it("slot: teleportedSlots undefined and formatter returns undefined", async () => { + const exposed = shallowRef(); + const Probe = defineComponent({ + setup(_, ctx) { + const { teleportedSlots, patchOption } = useSlotOption( + ctx.slots, + () => {}, + ); + (ctx as any).expose({ teleportedSlots, patchOption }); + return () => h("div", teleportedSlots()); + }, + }); + + const Root = defineComponent({ + setup() { + const r = shallowRef(); + watchEffect(() => { + if (r.value) exposed.value = r.value; + }); + return () => + h( + Probe, + { ref: (v: any) => (r.value = v) }, + { tooltip: () => [h("span", "x")] }, + ); + }, + }); + + render(Root); + + const vnode = exposed.value!.teleportedSlots(); + expect(vnode).toBeUndefined(); + + const patched: any = exposed.value!.patchOption({}); + const container = patched.tooltip?.formatter?.({ dataIndex: 0 }); + expect(container).toBeUndefined(); + + cleanup(); + }); +}); diff --git a/tests/style.test.ts b/tests/style.test.ts new file mode 100644 index 00000000..3e8f5e2b --- /dev/null +++ b/tests/style.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +describe("style entry", () => { + const adoptedDescriptor = Object.getOwnPropertyDescriptor( + Document.prototype, + "adoptedStyleSheets", + ); + + beforeEach(() => { + vi.resetModules(); + document.head.innerHTML = ""; + }); + + afterEach(() => { + if (adoptedDescriptor) { + Object.defineProperty(document, "adoptedStyleSheets", adoptedDescriptor); + } else { + delete (document as any).adoptedStyleSheets; + } + }); + + it("falls back to style tag when adoptedStyleSheets is unavailable", async () => { + Object.defineProperty(document, "adoptedStyleSheets", { + configurable: true, + value: undefined, + }); + + const replaceSpy = vi.spyOn(CSSStyleSheet.prototype, "replaceSync"); + + await import("../src/style"); + + const styleEl = document.head.querySelector("style"); + + expect(replaceSpy).not.toHaveBeenCalled(); + expect(styleEl).not.toBeNull(); + expect(styleEl?.textContent).not.toBe(""); + }); +}); diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 00000000..86d8a7d9 --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; + +import { + isOn, + omitOn, + isValidArrayIndex, + isSameSet, + isPlainObject, +} from "../src/utils"; + +describe("utils", () => { + describe("isOn", () => { + it("recognizes vue-style event props", () => { + expect(isOn("onClick")).toBe(true); + expect(isOn("onNative:click")).toBe(true); + expect(isOn("onZr:mouseover")).toBe(true); + expect(isOn("onUpdate:modelValue")).toBe(true); + expect(isOn("on")).toBe(false); + }); + + it("ignores non-event keys", () => { + expect(isOn("onclick")).toBe(false); + expect(isOn("onupdate:modelValue")).toBe(false); + expect(isOn("foo")).toBe(false); + }); + }); + + describe("omitOn", () => { + it("returns attrs without event handlers", () => { + const attrs = { + id: "chart", + onClick: () => void 0, + onNative: () => void 0, + class: "foo", + }; + + const result = omitOn(attrs); + + expect(result).toEqual({ id: "chart", class: "foo" }); + expect("onClick" in result).toBe(false); + expect(attrs).toHaveProperty("onClick"); + expect(result).not.toBe(attrs); + }); + }); + + describe("isValidArrayIndex", () => { + it("accepts non-negative integer strings", () => { + expect(isValidArrayIndex("0")).toBe(true); + expect(isValidArrayIndex("42")).toBe(true); + expect(isValidArrayIndex("4294967294")).toBe(true); + expect(isValidArrayIndex(" 1")).toBe(false); + }); + + it("rejects invalid inputs", () => { + expect(isValidArrayIndex("-1")).toBe(false); + expect(isValidArrayIndex("3.14")).toBe(false); + expect(isValidArrayIndex("1e3")).toBe(false); + expect(isValidArrayIndex("foo")).toBe(false); + }); + }); + + describe("isSameSet", () => { + it("detects identical sets regardless of order", () => { + expect(isSameSet([1, 2, 2, 3], [3, 2, 1])).toBe(true); + expect(isSameSet([1, 2, 2, 3], [3, 4, 1])).toBe(false); + }); + + it("detects differing sets", () => { + expect(isSameSet([1, 2], [1, 2, 3])).toBe(false); + expect(isSameSet([1, 2], [1, 3])).toBe(false); + }); + }); + + describe("isPlainObject", () => { + it("accepts plain objects", () => { + expect(isPlainObject({ foo: "bar" })).toBe(true); + expect(isPlainObject(() => ({ foo: "bar" }))).toBe(false); + }); + + it("rejects arrays and primitives", () => { + expect(isPlainObject([])).toBe(false); + expect(isPlainObject(null)).toBe(false); + expect(isPlainObject("foo")).toBe(false); + }); + }); +}); diff --git a/tests/wc.test.ts b/tests/wc.test.ts new file mode 100644 index 00000000..6a9ad66a --- /dev/null +++ b/tests/wc.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +declare global { + interface HTMLElement { + __dispose?: (() => void) | null; + } +} + +type LoadOptions = { suffix?: string }; + +const loadModule = (() => { + let counter = 0; + return async (mode: "stub" | "native", options?: LoadOptions) => { + const suffix = options?.suffix ?? `${mode}-${++counter}`; + return import(/* @vite-ignore */ `../src/wc?${suffix}`); + }; +})(); + +describe("register", () => { + describe("with stubbed customElements", () => { + class CustomElementRegistryStub { + private readonly registry = new Map(); + + define(name: string, ctor: CustomElementConstructor): void { + if (this.registry.has(name)) { + throw new DOMException("already defined", "NotSupportedError"); + } + this.registry.set(name, ctor); + } + + get(name: string): CustomElementConstructor | undefined { + return this.registry.get(name); + } + } + + let registry: CustomElementRegistryStub; + + beforeEach(() => { + vi.resetModules(); + vi.unstubAllGlobals(); + + registry = new CustomElementRegistryStub(); + vi.stubGlobal( + "customElements", + registry as unknown as CustomElementRegistry, + ); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("returns false when custom elements are unavailable", async () => { + vi.unstubAllGlobals(); + vi.stubGlobal( + "customElements", + undefined as unknown as CustomElementRegistry, + ); + + const { register } = await loadModule("stub"); + + expect(register()).toBe(false); + expect(register()).toBe(false); + }); + + it("returns false when browser APIs are disabled", async () => { + vi.resetModules(); + // Simulate missing browser API by providing a registry without `get` + vi.stubGlobal("customElements", { + define() {}, + } as unknown as CustomElementRegistry); + + const { register } = await loadModule("stub", { suffix: "no-get" }); + expect(register()).toBe(false); + expect(register()).toBe(false); + }); + + it("registers the custom element once", async () => { + const defineSpy = vi.spyOn(registry, "define"); + + const { register, TAG_NAME } = await loadModule("stub"); + + expect(register()).toBe(true); + expect(defineSpy).toHaveBeenCalledTimes(1); + expect(registry.get(TAG_NAME)).toBeTypeOf("function"); + + defineSpy.mockClear(); + expect(register()).toBe(true); + expect(defineSpy).not.toHaveBeenCalled(); + }); + + it("handles definition failures gracefully", async () => { + const defineSpy = vi.spyOn(registry, "define").mockImplementation(() => { + throw new Error("boom"); + }); + + const { register, TAG_NAME } = await loadModule("stub"); + + expect(register()).toBe(false); + expect(register()).toBe(false); + expect(defineSpy).toHaveBeenCalledTimes(1); + expect(registry.get(TAG_NAME)).toBeUndefined(); + }); + + it("skips redefinition when element already registered", async () => { + const existing = class extends HTMLElement {}; + const { register, TAG_NAME } = await loadModule("stub"); + registry.define(TAG_NAME, existing); + + const defineSpy = vi.spyOn(registry, "define"); + + expect(register()).toBe(true); + expect(defineSpy).not.toHaveBeenCalled(); + expect(registry.get(TAG_NAME)).toBe(existing); + }); + + it("exposes a constructor with disconnect hook", async () => { + const { register, TAG_NAME } = await loadModule("stub"); + + expect(register()).toBe(true); + + const ctor = registry.get(TAG_NAME); + expect(typeof ctor).toBe("function"); + expect("disconnectedCallback" in (ctor?.prototype ?? {})).toBe(true); + }); + }); + + describe("with native customElements", () => { + let original: CustomElementConstructor | undefined; + + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + original = customElements.get("x-vue-echarts"); + document.body.innerHTML = ""; + }); + + afterEach(() => { + document.body.innerHTML = ""; + if (original) { + customElements.define("x-vue-echarts", original); + } + }); + + it("disposes chart when element is removed from DOM", async () => { + const { register, TAG_NAME } = await loadModule("native"); + + expect(register()).toBe(true); + + const element = document.createElement(TAG_NAME) as HTMLElement & { + __dispose: (() => void) | null; + }; + const dispose = vi.fn(); + element.__dispose = dispose; + + document.body.appendChild(element); + document.body.removeChild(element); + + await Promise.resolve(); + + expect(dispose).toHaveBeenCalledTimes(1); + expect(element.__dispose).toBeNull(); + }); + }); +}); diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json index bee6b54b..5ceb9ca5 100644 --- a/tsconfig.vitest.json +++ b/tsconfig.vitest.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": ["vitest/globals", "node"], + "types": ["vitest/globals", "vite/client"], "noEmit": true }, - "include": ["tests/**/*.ts", "src/**/*.ts"] + "include": ["tests/**/*.ts", "tests/types/**/*.d.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index 8cb65284..ec13a821 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,12 +1,30 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig, mergeConfig } from "vitest/config"; +import viteConfig from "./vite.config"; -export default defineConfig({ - test: { - globals: true, - environment: "node", - include: ["tests/**/*.test.ts"], - coverage: { - reporter: ["text", "lcov"], +export default mergeConfig( + viteConfig, + defineConfig({ + root: ".", + test: { + globals: true, + setupFiles: ["./tests/setup.ts"], + include: ["tests/**/*.test.ts"], + coverage: { + provider: "v8", + reporter: ["text", "lcov", "html"], + include: ["src/**/*.{ts,tsx,js,jsx,vue}"], + reportsDirectory: "coverage/browser", + }, + browser: { + enabled: true, + provider: "playwright", + headless: true, + instances: [ + { + browser: "chromium", + }, + ], + }, }, - }, -}); + }), +);