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",
+ },
+ ],
+ },
},
- },
-});
+ }),
+);